Belajar Membuat Live Wallpaper Untuk Android

Selain wallpaper dalam bentuk gambar statis, Android juga mendukung wallpaper animasi dan interaktif yang disebut sebagai live wallpaper. Sebagai latihan, pada kesempatan ini, saya akan mencoba membuat sebuah live wallpaper sederhana. Saya akan mulai dengan membuat sebuah proyek baru di Android Studio. Sebuah live wallpaper pada dasarnya adalah sebuah service. Oleh sebab itu, saya kemudian membuat service baru dengan nama LiveWallpaperService yang isinya seperti berikut ini:

public class LiveWallpaperService extends WallpaperService {

    @Override
    public Engine onCreateEngine() {
        return new MyAnimationEngine();
    }

}

Service yang mewakili live wallpaper harus diturunkan dari WallpaperService. Satu-satunya method yang perlu dibuat disini adalah onCreateEngine() yang menghasilkan sebuah WallpaperService.Engine.

Kode program yang berhubungan dengan live wallpaper justru lebih banyak terdapat di turunan WallpaperService.Engine ini. Sebagai contoh, saya membuat sebuah nested class dengan nama MyAnimationEngine dengan isi seperti berikut ini:

public class LiveWallpaperService extends WallpaperService {

    @Override
    public Engine onCreateEngine() {
        return new MyAnimationEngine();
    }

    class MyAnimationEngine extends Engine {

        private AnimasiThread animasiThread;
        private int width, height;

        private void startAnimation() {
            animasiThread = new AnimasiThread(getSurfaceHolder(), width, height, getApplicationContext());
            animasiThread.setRunning(true);
            animasiThread.start();
        }

        private void stopAnimation() {
            if (animasiThread != null) {
                animasiThread.setRunning(false);
                try {
                    animasiThread.join();
                } catch (InterruptedException e) {
                }
            }
        }

        private void restartAnimation() {
            if (animasiThread != null) {
                stopAnimation();
            }
            startAnimation();
        }

        @Override
        public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            this.width = width;
            this.height = height;
            restartAnimation();
        }

        @Override
        public void onSurfaceCreated(SurfaceHolder holder) {
            startAnimation();
        }

        @Override
        public void onSurfaceDestroyed(SurfaceHolder holder) {
            stopAnimation();
        }

        @Override
        public void onVisibilityChanged(boolean visible) {
            if (!visible) {
                stopAnimation();
            } else {
                restartAnimation();
            }
        }

    }

}   

Sama seperti saat menggambar pada SurfaceView, saya memulai proses penggambaran pada onSurfaceCreated() yang akan dipanggil setelah SurfaceHolder dibuat. Selain itu, saya juga perlu memperbaharui layar bila terjadi perubahan ukuran layar yang akan memanggil onSurfaceChanged(). Bila proses penggambaran selesai, onSurfaceDestroyed() akan dipanggil.

Pada saat menggambar untuk live wallpaper, saya perlu menghemat sumber daya dan tidak mengerjakan sesuatu yang tidak perlu. Bila terlalu berlebihan, bukan saja CPU akan habis dikonsumsi kode program yang menggambar, tetapi baterai juga akan cepat habis. Oleh sebab itu, saya men-override method onVisiblityChanged() untuk mematikan proses penggambaran bila live wallpaper tidak visible.

Pada implementasi WallpaperService.Engine di atas, saya menggambar pada sebuah thread baru yang diwakili oleh sebuah class seperti berikut ini:

public class AnimasiThread extends Thread {

    private SurfaceHolder surfaceHolder;
    private boolean running = false;
    private int width, height;
    private List<Gambar> gambars;
    private final Object lock = new Object();

    public AnimasiThread(SurfaceHolder surfaceHolder, int width, int height, Context context) {
        this.surfaceHolder = surfaceHolder;
        this.width = width;
        this.height = height;
        gambars = new ArrayList<>();
        for (int i=0; i<20; i++) {
            gambars.add(new Gambar(width, height, BitmapFactory.decodeResource(context.getResources(), R.drawable.hati)));
        }
    }

    public void setRunning(boolean running) {
        this.running = running;
    }

    @Override
    public void run() {
        while (running) {
            Canvas c = null;
            try {
                c = surfaceHolder.lockCanvas();
                if (running) {
                    c.drawColor(Color.BLACK);
                    synchronized (lock) {
                        for (Gambar gambar: gambars) {
                            gambar.gambar(c);
                            gambar.gerak();
                        }
                    }
                }
            } finally {
                if (c != null) {
                    surfaceHolder.unlockCanvasAndPost(c);
                }
            }
        }
    }

}

Class di atas membutuhkan sebuah gambar yang saya letakkan dalam folder drawable. Saya memberi nama file tersebut sebagai hati.png.

Tidak ada yang spesial pada class AnimasiThread di atas. Saya hanya menciptakan beberapa object Gambar dan memanggil method gerak() pada setiap object yang ada secara terus menerus berulang kali. Isi dari class Gambar terlihat seperti berikut ini:

public class Gambar {

    private float x, y, xmax, ymax;
    private Bitmap gambar;
    private float kecepatan;

    public Gambar(float xmax, float ymax, Bitmap gambar) {
        this.xmax = xmax;
        this.ymax = ymax;
        this.gambar = gambar;
        this.kecepatan = 0.1f;
        Random random = new Random();
        this.x = random.nextFloat() * xmax;
        this.y = random.nextFloat() * ymax;
    }

    public void gerak() {
        y += kecepatan;
        kecepatan += 0.1f;
        if (y > ymax) {
            y = (float)(Math.random() * -ymax);
            x = (float)(Math.random() * xmax);
            kecepatan = 0.1f;
        }
    }

    public void gambar(Canvas c) {
        c.drawBitmap(gambar, x, y, null);
    }

}

Sebagai langkah terakhir, agar live wallpaper ini bisa dikenali, saya perlu membuat sebuah file XML yang berisi <wallpaper>. Untuk itu, saya men-klik kanan nama proyek dan memilih menu New, Android Resource File. Pada dialog yang muncul, saya mengisi File name dengan mylivewallpaper. Pada Resource type, saya memilih XML. Saya juga mengganti Root element dengan wallpaper. Setelah itu, saya men-klik tombol Ok. Saya kemudian mengubah file yang dihasilkan menjadi seperti berikut ini:

<?xml version="1.0" encoding="utf-8"?>
<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/app_name">
</wallpaper>

Setelah itu, saya mengubah deklarasi service di AndroidManifest.xml menjadi seperti berikut ini:

<?xml version="1.0" encoding="utf-8"?>
<manifest... >

    <application ...>
        <service
            android:name=".LiveWallpaperService"
            android:permission="android.permission.BIND_WALLPAPER">
            <intent-filter>
                <action android:name="android.service.wallpaper.WallpaperService" />
            </intent-filter>
            <meta-data
                android:name="android.service.wallpaper"
                android:resource="@xml/mylivewallpaper" />
        </service>
    </application>

</manifest>

Saya tidak bisa begitu saja menjalankan aplikasi ini karena tidak ada activity yang berfungsi sebagai launcher. Hal ini terlihat dari tanda silang pada tombol yang biasa saya gunakan untuk menjalankan aplikasi di Android Studio:

Tidak ada launcher activity

Tidak ada launcher activity

Yang bisa saya lakukan hanya men-deploy aplikasi ke perangkat. Oleh sebab itu, saya memilih Edit Configurations…. Pada dialog yang muncul, saya mengubah Launch default Activity menjadi Do not launch Activity seperti pada gambar berikut ini:

Mendeploy project tanpa menjalankan activity

Mendeploy project tanpa menjalankan activity

Setelah men-klik OK, saya kini bisa men-deploy aplikasi ke perangkat Android.

Untuk menggunakan live wallpaper yang telah saya buat, saya perlu membuka Settings di perangkat Android lalu memilih menu Display, Wallpaper, Home screen. Pada daftar pilihan yang muncul, saya memilih Live wallpapers seperti pada gambar berikut ini:

Memilih live wallpaper untuk dipakai

Memilih live wallpaper untuk dipakai

Setelah itu, saya dapat memilih live wallpaper yang telah saya buat seperti pada gambar berikut ini:

Memilih live wallpaper untuk dipakai

Memilih live wallpaper untuk dipakai

Sekarang, tampilan live wallpaper akan muncul dalam bentuk animasi seperti pada video berikut ini:

Tampilan live wallpaper setelah dipakai

Tampilan live wallpaper setelah dipakai

Iklan

Menggambar Dengan SurfaceView Di Android

Pada artikel Menggambar Dengan Canvas Di Android, saya menggambar bebas dengan membuat sebuah turunan baru dari View. Teknik tersebut tepat dipakai untuk gambar yang statis. Untuk gambar yang perlu diperbaharui secara cepat dan terus menerus (misalnya pada animasi game), Android SDK menyediakan SurfaceView. Salah satu kelebihan SurfaceView adalah ia dapat di-update secara cepat kapan saja, sementara pada View biasa, pemanggilan invalidate() tidak selalu segera memperbaharui tampilan. Walaupun lebih cepat, SurfaceView lebih rumit dan membutuhkan lebih banyak resource dibandingkan View biasa.

Sebagai latihan, saya akan membuat sebuah proyek baru di Android Studio. Proyek latihan ini akan menampilkan animasi lingkaran kecil yang bergerak dari atas layar ke bawah. Untuk itu, sebagai langkah pertama, saya akan membuat domain class yang mewakili sebuah lingkaran dengan nama Lingkaran.java yang isinya seperti berikut ini:

public class Lingkaran {

    private float x, y;
    private float kecepatan;
    private float ukuran;
    private Paint paint;

    public Lingkaran(int xmax, int ymax) {
        Random random = new Random();
        x = random.nextFloat() * xmax;
        y = random.nextFloat() * ymax;
        kecepatan = 0.1f;
        paint = new Paint();
        paint.setColor(Color.rgb(random.nextInt(255), random.nextInt(255), random.nextInt(255)));
        ukuran = 10.0f;
    }

    public void gerak() {
        y += kecepatan;
        kecepatan += 0.1f;
    }

    public void reset() {
        y = 0;
        kecepatan = 0.1f;
    }

    public void gambar(Canvas c) {
        c.drawCircle(x, y, ukuran, paint);
    }

    public float getX() {
        return x;
    }

    public float getY() {
        return y;
    }

}

Class Lingkaran menyimpan informasi posisi dan kecepatan untuk sebuah lingkaran yang ada di layar. Class ini juga menyediakan method gambar() untuk menggambar dirinya pada sebuah Canvas.

Salah satu hal yang unik pada SurfaceView adalah ia tidak harus selalu diperbaharui pada UI Thread. Saya boleh memanipulasi layar dari sebuah thread yang berbeda melalui SurfaceHolder dari SurfaceView tersebut. Sebagai contoh, saya membuat sebuah class yang mewakili thread baru dengan nama AnimasiThread.java yang isinya seperti berikut ini:

public class AnimasiThread extends Thread {

    private SurfaceHolder surfaceHolder;
    private Context context;
    private boolean running = false;
    private List<Lingkaran> lingkarans;
    private int width, height;
    private final Object lock = new Object();

    public AnimasiThread(SurfaceHolder surfaceHolder, Context context) {
        this.surfaceHolder = surfaceHolder;
        this.context = context;
        lingkarans = new ArrayList<>();
    }

    public boolean isRunning() {
        return running;
    }

    public void setRunning(boolean running) {
        this.running = running;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public void buatLingkaran() {
        synchronized (lock) {
            lingkarans.clear();
            for (int i = 0; i < 100; i++) {
                lingkarans.add(new Lingkaran(width, height));
            }
        }
    }

    @Override
    public void run() {
        while (running) {
            Canvas c = null;
            try {
                c = surfaceHolder.lockCanvas();
                if (running) {
                    c.drawColor(Color.BLACK);
                    synchronized (lock) {
                        for (Lingkaran lingkaran : lingkarans) {
                            lingkaran.gambar(c);
                            lingkaran.gerak();
                            if (lingkaran.getY() > height) {
                                lingkaran.reset();
                            }
                        }
                    }
                }
            } finally {
                if (c != null) {
                    surfaceHolder.unlockCanvasAndPost(c);
                }
            }
        }
    }

}

Untuk membuat class AnimasiThread, dibutuhkan sebuah SurfaceHolder dan Context yang diperoleh dari SurfaceView. Bagian yang paling penting dari class ini adalah method run(). Saya menggunakan lockCanvas() dari SurfaceHolder untuk memperoleh sebuah Canvas. Setelah selesai menggambar dengan Canvas, saya wajib memanggil unlockCanvasAndPost() dari SurfaceHolder.

Sekarang, saya siap untuk membuat sebuah turunan dari SurfaceHolder yang saya beri nama AnimasiView.java dengan isi seperti berikut ini:

package com.snake.mygame.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class AnimasiView extends SurfaceView implements SurfaceHolder.Callback {

    private AnimasiThread animasiThread;

    public AnimasiView(Context context, AttributeSet attrs) {
        super(context, attrs);
        SurfaceHolder holder = getHolder();
        holder.addCallback(this);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        animasiThread = new AnimasiThread(holder, getContext());
        animasiThread.setRunning(true);
        animasiThread.start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        animasiThread.setWidth(width);
        animasiThread.setHeight(height);
        animasiThread.buatLingkaran();
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        animasiThread.setRunning(false);
        try {
            animasiThread.join();
        } catch (InterruptedException e) {}
    }

}

Pada sebuah SurfaceView, saya dapat memanggil getHolder() untuk memperoleh SurfaceHolder yang diasosiasikan dengannya. Selain itu, saya juga mendaftarkan SurfaceHolder.Callback dengan memanggil addCallback() milik SurfaceHolder. Method surfaceCreated() akan dipanggil pada saat SurfaceView sudah selesai dibuat dan saya sudah boleh mulai menggambar. Method surfaceChanged() akan dipanggil bila terdapat perubahan ukuran SurfaceView. Method surfaceDestroyed() akan dipanggil pada saat SurfaceView dihapus (tidak dibutuhkan lagi).

Sebagai langkah terakhir, saya akan membuat sebuah activity yang menampilkan SurfaceView yang telah saya buat:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new AnimasiView(this, null));
    }

}

Agar activity ini ditampilkan secara satu layar penuh (full-screen), saya mengubah isi AndroidManifest.xml menjadi seperti berikut ini:

<manifest ...>
    <application
        ...
        android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Sekarang, bila saya menjalankan aplikasi, saya akan memperoleh hasil seperti pada gambar berikut ini:

Tampilan Pada Saat Aplikasi Dijalankan

Tampilan Pada Saat Aplikasi Dijalankan

Menggambar Dengan Canvas Di Android

Saya sudah terbiasa menggunakan Graphics dan Graphics2D untuk menggambar secara bebas di Java Swing. Lalu, bagaimana dengan menggambar di aplikasi Android? Saya dapat menggunakan Canvas untuk keperluan tersebut. Mirip seperti Graphics di Swing, Canvas di Android juga menyediakan method seperti drawLine(), drawRect(), drawPoint(), drawText(), dan sebagainya untuk menghasilkan gambar.

Sebagai latihan, saya akan mulai dengan membuat sebuah proyek baru di Android Studio. Saya akan menggunakan bahasa Groovy pada latihan ini. Langkah pertama yang saya lakukan adalah membuat sebuah turunan dari View dengan nama PlotterView seperti:

class PlotterView extends View {

    float xmin = -10, xmax = 10, ymin = -10, ymax = 10

    float step

    Paint warnaLatar, warnaGaris

    PlotterView(Context context) {
        super(context)
        warnaLatar = new Paint(color: Color.BLACK)
        warnaGaris = new Paint(color: Color.YELLOW)
    }

    void setWarnaLatar(int warna) {
        warnaLatar.color = warna
        invalidate()
    }

    void setWarnaGaris(int warna) {
        warnaGaris.color = warna
        invalidate()
    }

}

Pada method setWarnaLatar() dan setWarnaGaris(), saya memanggil invalidate() agar View ini digambar ulang setelah terdapat perubahan nilai warna latar dan warga garis. Saya akan membuat sumbu x selalu tetap sementara nilai sumbu y bisa bervariasi tergantung pada ukuran layar. Untuk itu, saya perlu menghitung ulang nilai skala, nilai y minimal dan nilai y maksimal setiap kali terdapat perubahan ukuran komponen. Saya dapat melakukannya dengan men-override method onSizeChanged() seperti pada kode program berikut ini:

class PlotterView extends View {

   ...

   @Override
   protected void onSizeChanged(int w, int h, int oldw, int oldh) {
      step = w / (xmax - xmin)
      int jumlahY = h / step
      ymax = jumlahY / 2
      ymin = -ymax
   }

}

Pekerjaan utama dalam menggambar adalah men-override method onDraw(). Sebagai contoh, saya membuat kode program yang menggambar sumbu x dan sumbu y seperti:

class PlotterView extends View {

   ...

   @Override
   protected void onDraw(Canvas canvas) {
     super.onDraw(canvas)
     if (isInEditMode()) {
       canvas.drawColor(warnaLatar.color)
     } else {
       canvas.drawPaint(warnaLatar)
     }

     // Gambar sumbu
     float x0 = translateX(0)
     float y0 = translateY(0)
     canvas.drawLine(0, y0, getWidth(), y0, warnaGaris)
     for (float x=xmin; x <= xmax; x++) {
        canvas.drawText(x.intValue().toString(), translateX(x), (float) (y0 + 12), warnaGaris)
     }
     canvas.drawLine(x0, 0, x0, getHeight(), warnaGaris)
     for (float y=ymax; y >= ymin; y--) {
        if (y != 0) {
          canvas.drawText(y.intValue().toString(), x0, translateY(y), warnaGaris)
        }
     }
   }

   private float translateX(float x) {
     if (x == 0) {
       x = (xmax < 0)? xmax: ((xmin > 0)? xmin: 0)
     }
     ((x - xmin) * step) - 12
   }

   private float translateY(float y) {
     if (y == 0) {
       y = (ymax < 0)? ymax: ((ymin > 0? ymin: 0))
     }
     if (ymax < 0) {
       return (ymax - y) * step
     } else {
       return (getHeight() - ((y - ymin) * step)) - 12
     }
   }
}

Pada kode program di atas, saya menggunakan drawColor() atau drawPaint() milik Canvas untuk mengisi layar dengan sebuah warna yang sama. Untuk menggambar garis sumbu x dan sumbu y, saya menggunakan drawLine() milik Canvas. Untuk menulis angka koordinat, saya menggunakan drawText() milik Canvas.

Satu hal unik yang tidak saya jumpai di Swing adalah method isInEditMode() milik View yang akan mengembalikan nilai true bila View ini sedang ditampilkan oleh visual editor. Dengan menggunakan method ini, saya bisa menghasilkan gambar yang berbeda (yang lebih ringan atau cepat diproses) pada saat View ditampilkan di layout editor Android Studio secara visual (modus preview).

Agar lebih mudah dipakai, saya ingin View baru ini dapat dikonfigurasi melalui XML. Untuk itu, saya perlu mendeklarasikan atribut apa saja yang dapat diatur oleh penggunanya melalui visual designer. Saya akan mulai dengan men-klik kanan pada folder res dan memilih menu New, Android Resource File. Setelah itu, saya mengisi dialog yang muncul seperti pada gambar berikut ini:

Menambah atribut XML untuk custom view

Menambah atribut XML untuk custom view

Saya kemudian mengubah isi file attrs.xml tersebut menjadi seperti berikut ini:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="PlotterView">
        <attr name="warnaLatar" format="color" />
        <attr name="warnaGaris" format="color" />
    </declare-styleable>
</resources>

Berikutnya, saya membuat sebuah layout baru dengan men-klik kanan pada folder res/layout dan memilih menu New, Layout Resource File. Saya mengisi File name dengan nama layout_main dan Root element dengan RelativeLayout. Untuk menambahkan View yang telah dibuat, saya dapat memilih Custom, CustomView pada Palette:

Menambah custom view melalui visual designer

Menambah custom view melalui visual designer

Saat meletakkan komponen tersebut, Android Studio akan memunculkan dialog yang berisi daftar custom view yang dijumpainya. Saya memilih PlotterView dan men-klik Ok. Sekarang, saya dapat meletakkan View tersebut pada rancangan visual. Sebagai latihan, saya mengubah isi layout_main.xml menjadi seperti berikut ini:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <view
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        class="com.snake.mygraph.PlotterView"
        android:id="@+id/output"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/layout_rumus"
        android:layout_alignParentBottom="true" />

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:id="@+id/layout_x"
        android:layout_alignParentTop="true"
        android:layout_alignParentStart="true">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:text="@string/x"
            android:id="@+id/textView3" />

        <EditText
            android:layout_width="80dp"
            android:layout_height="wrap_content"
            android:inputType="number"
            android:ems="10"
            android:id="@+id/xmin"
            android:layout_weight="1" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:text="@string/sampai"
            android:id="@+id/textView4" />

        <EditText
            android:layout_width="80dp"
            android:layout_height="wrap_content"
            android:inputType="number"
            android:ems="10"
            android:id="@+id/xmax"
            android:layout_weight="1" />
    </LinearLayout>

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:id="@+id/layout_rumus"
        android:layout_alignParentStart="true"
        android:weightSum="1"
        android:layout_below="@+id/layout_x">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:text="@string/rumus"
            android:id="@+id/textView5" />

        <EditText
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:inputType="textFilter"
            android:id="@+id/rumus"
            android:layout_weight="1" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/refresh"
            android:onClick="refresh"
            android:id="@+id/refresh" />

    </LinearLayout>

</RelativeLayout>

Hasil preview layout ini pada Android Studio akan terlihat seperti:

Hasil preview di visual designer

Hasil preview di visual designer

Bila saya melakukan perubahan warnaLatar untuk PlotterView di layout_main.xml seperti pada:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <view
        ...
        class="com.snake.mygraph.PlotterView"
        app:warnaLatar="@android:color/darker_gray"
        ... />

    ...

</RelativeLayout>

maka saya akan memperoleh preview seperti pada:

Mengubah property melalui atribut XML

Mengubah property melalui atribut XML

Terlihat bahwa saya bisa dengan mudah mengisi property dari PlotterView melalui XML.

Selanjutnya, saya akan menampilkan grafis yang mewakili sebuah persamaan yang ditentukan oleh pengguna. Saya ingin pengguna bisa memasukkan sebuah ekspresi Groovy dimana nilai variabel x mewakili nilai di sumbu x. Bila ini adalah aplikasi Java, saya bisa mengevaluasi ekspresi dalam bentuk string dengan menggunakan GroovyShell. Akan tetapi, saat mencoba menerapkannya pada Android, saya menemukan pesan kesalahan berupa ‘General error during class generation: can’t load this type of class file’.

Mengapa demikian? GroovyShell menghasilkan bytecode (berdasarkan string) yang kemudian dibaca dan dikerjakan oleh JDK. Ini bekerja dengan baik pada Java (misalnya pada saat dijalankan di JDK 7). Akan tetapi Android hanya meminjam bahasa Java, sementara arsitektur internalnya sangat berbeda. Salah satu perbedaannya adalah bytecode tidak bisa langsung dikerjakan. Bytecode perlu diterjemahkan lagi ke dalam format Dex (Dalvik EXecutable format). Perlu diketahui bahwa Dex adalah format yang spesifik untuk Android dan bukan bagian dari Java 🙂 Dengan demikian, untuk menghasilkan class secara dinamis di Android, bytecode yang dihasilkan oleh GroovyShell perlu diubah ke dalam format Dex dengan menggunakan tool seperti dx bawaan Android SDK. Beruntungnya, tool dx dapat dipakai secara programatis dengan menambahkan baris berikut ini pada build.gradle:

dependencies {
    ...
    compile 'com.google.android.tools:dx:1.7'
}

Saya bisa menemukan contoh kode program yang menerjemahkan bytecode dari Groovy menjadi Dex kemudian membacanya kembali sebagai class di https://github.com/melix/grooidshell-example. Saya tinggal men-copy file src/main/java/me/champeau/groovydroid/GrooidShell.java dan src/main/java/groovy/lang/GroodClassLoader.java pada sebuah folder seperti src/main/java/helper di proyek saya. Informasi lebih lanjut mengenai teknik ini dapat dibaca di http://melix.github.io/blog/2014/06/grooid2.html.

Berdasarkan kode program yang ada di method evaluate(), saya menambahkan method parse() pada GrooidShell yang isinya seperti berikut ini:

public class GrooidShell {

  ...

  public Script parse(String scriptText) {
    //
    // Bagian ini akan menghasilkan bytecode Java berdasarkan kode program yang
    // dimasukkan oleh pengguna.
    //
    final Set<String> classNames = new LinkedHashSet<String>();
    final DexFile dexFile = new DexFile(dexOptions);
    CompilerConfiguration config = new CompilerConfiguration();
    config.setBytecodePostprocessor(new BytecodeProcessor() {
      @Override
      public byte[] processBytecode(String s, byte[] bytes) {
        ClassDefItem classDefItem = CfTranslator.translate(s+".class", bytes, cfOptions, dexOptions);
        dexFile.add(classDefItem);
        classNames.add(s);
        return bytes;
      }
    });
    GrooidClassLoader gcl = new GrooidClassLoader(this.classLoader, config);
    try {
      gcl.parseClass(scriptText);
    }  catch (Throwable e) {
      Log.e("GrooidShell","Dynamic loading failed!",e);
    }

    //
    // Bagian ini akan menghasilkan format Dex lalu
    // men-load class yang ada.
    //
    byte[] dalvikBytecode = new byte[0];
    try {
      dalvikBytecode = dexFile.toDex(new OutputStreamWriter(new ByteArrayOutputStream()), false);
    } catch (IOException e) {
      Log.e("GrooidShell", "Unable to convert to Dalvik", e);
    }
    Map<String, Class> classes = defineDynamic(classNames, dalvikBytecode);
    Script script = null;
    for (Class scriptClass : classes.values()) {
      if (Script.class.isAssignableFrom(scriptClass)) {
        try {
          script = (Script) scriptClass.newInstance();
        } catch (InstantiationException e) {
          Log.e("GroovyDroidShell", "Unable to create script",e);
        } catch (IllegalAccessException e) {
          Log.e("GroovyDroidShell", "Unable to create script", e);
        }
        break;
      }
    }
    return script;
  }
}

Setelah itu, saya bisa memakai GrooidShell di PlotterView, misalnya seperti pada contoh berikut ini:

class PlotterView extends View {

  ...

  Script script;

  ...

  void setFormula(String formula) {
    GrooidShell shell = new GrooidShell(context.getDir("dynclasses", 0), this.class.classLoader)
    script = shell.parse(formula)
    invalidate()
  }


  @Override
  protected void onDraw(Canvas canvas) {
    ...
    // Gambar grafis
    if (script) {
      Float prevX = null, prevY = null
      for (float x = xmin; x < xmax; x += 0.1) {
        script.x = x
        float y = script.run()
        float trX = translateX(x), trY = translateY(y)
        if (!prevX && !prevY) {
          canvas.drawPoint(trX, trY, warnaGaris)
        } else {
          canvas.drawLine(prevX, prevY, trX, trY, warnaGaris)
        }
        prevX = trX
        prevY = trY
      }
    }
  }    

  ...

}

Sebagai langkah terakhir, saya akan membuat sebuah activity untuk menjalankan aplikasi dengan nama MainActivity.groovy yang isinya seperti berikut ini:

@CompileStatic
class MainActivity extends Activity {

  @Override
  void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.layout_main)
      if (savedInstanceState) {
        findViewById(R.id.output).invalidate()
      }
  }

  void refresh(View view) {
    PlotterView plotter = (PlotterView) findViewById(R.id.output)
    String strXmin = ((EditText) findViewById(R.id.xmin)).getText().toString()
    String strXmax = ((EditText) findViewById(R.id.xmax)).getText().toString()
    String formula = ((EditText) findViewById(R.id.rumus)).getText()
    if (strXmin.length() > 0) {
      plotter.xmin = strXmin.toFloat()
    }
    if (strXmax.length() > 0) {
      plotter.xmax = strXmax.toFloat()
    }
    if (formula.length() > 0) {
      plotter.formula = formula
    }
  }

}

Sekarang, bila saya menjalankan aplikasi, saya akan memperoleh hasil seperti pada gambar berikut ini:

Tampilan aplikasi saat dijalankan

Tampilan aplikasi saat dijalankan

Bila saya mengubah orientasi layar, saya akan memperoleh hasil seperti pada gambar berikut ini:

Tampilan view setelah perubahan orientasi

Tampilan view setelah perubahan orientasi

Pada kode program saat ini, setiap kali saya mengubah orientasi layar, saya harus menekan tombol untuk kembali menggambar grafis. Agar nilai property dari PlotterView tetap disimpan saat terjadi perubahan orientasi layar, saya akan men-override method onSaveInstanceState() dan onRestoreInstanceState() seperti pada contoh berikut ini:

class PlotterView extends View {

  String formulaFx

  ...

  void setFormula(String formulaFx) {
    this.formulaFx = formulaFx
    ...
  }


  @Override
  protected Parcelable onSaveInstanceState() {
    Parcelable superState = super.onSaveInstanceState()
    SavedState ss = new SavedState(superState)
    ss.xmin = xmin
    ss.xmax = xmax
    ss.ymin = ymin
    ss.ymax = ymax
    ss.formulaFx = formulaFx
    ss
  }

  @Override
  protected void onRestoreInstanceState(Parcelable state) {
    if (!(state instanceof SavedState)) {
      super.onRestoreInstanceState(state)
      return
    }
    SavedState ss = (SavedState) state
    super.onRestoreInstanceState(ss.getSuperState())
    xmin = ss.xmin
    xmax = ss.xmax
    ymin = ss.ymin
    ymax = ss.ymax
    setFormula(ss.formulaFx)
  }

  ...

  public static class SavedState extends BaseSavedState {

    String formulaFx
    float xmin, xmax
    float ymin, ymax

    SavedState(Parcelable superState) {
      super(superState)
    }

    SavedState(Parcel parcel) {
      super(parcel)
      formulaFx = parcel.readString()
      xmin = parcel.readFloat()
      xmax = parcel.readFloat()
      ymin = parcel.readFloat()
      ymax = parcel.readFloat()
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
      super.writeToParcel(out, flags)
      out.writeString(formulaFx)
      out.writeFloat(xmin)
      out.writeFloat(xmax)
      out.writeFloat(ymin)
      out.writeFloat(ymax)
    }

    public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
      public SavedState createFromParcel(Parcel p) {
        return new SavedState(p)
      }

      public SavedState[] newArray(int size) {
        return new SavedState[size]
      }
    }

  }

}

Tidak dapat dipungkiri bahwa dibutuhkan kode program yang cukup panjang untuk menyimpan dan membaca kembali state dari View setelah activity dihentikan (misalnya saat orientasi layar diubah) 🙂

Membuat Animasi Pada View Di Android

Salah satu hal unik di pemograman Android adalah ia dilengkapi dengan banyak class di package android.animation untuk mengimplementasikan animasi secara mudah. Bila programmer tidak ingin membuat kode program secara langsung, Android SDK bahkan memperbolehkan animasi didefinisikan dalam bentuk XML. Walaupun animasi bisa diterapkan pada seluruh komponen UI (seperti Button, EditText, dan sebagainya), contoh kasus yang lebih realistis adalah penggunaan animasi pada perpindahan activity dan fragment.

Sebagai latihan, saya akan menambahkan animasi pada sebuah program penjelajah file yang sudah saya buat sebelumnya. Pada saat pengguna menyentuh sebuah folder, saya membuat fragment baru yang menampilkan isi folder tersebut, seperti pada kode program berikut ini:

@Override
public void onFolderClick(File file) {
    ExplorerFragment explorerFragment = new ExplorerFragment();
    Bundle args = new Bundle();
    args.putString(ExplorerFragment.DATA_LOKASI, file.getAbsolutePath());
    explorerFragment.setArguments(args);
    FragmentTransaction transaction = getFragmentManager().beginTransaction();
    transaction.replace(R.id.container, explorerFragment);
    transaction.addToBackStack(null);
    transaction.commit();
}

Perpindahan fragment (membuka folder baru) akan terlihat seperti pada animasi berikut ini:

Perpindahan fragment tanpa animasi

Perpindahan fragment tanpa animasi

Untuk menambahkan animasi saat beralih dari fragment ke fragment lainnya, saya dapat memanggil method setTransition() milik FragmentTransaction. Method ini menerima animasi standar berupa FragmentTransaction.TRANSIT_NONE, FragmentTransaction.TRANSIT_FRAGMENT_OPEN, dan FragmentTransaction.TRANSIT_FRAGMENT_CLOSE. Sebagai contoh, saya menambahkan baris berikut ini sebelum transaction.commit():

transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);

Sekarang, saya akan menemukan animasi pada saat peralihan fragment seperti berikut ini:

Perpindahan fragment dengan animasi standar

Perpindahan fragment dengan animasi standar

Perhatikan bahwa saat saya menggunakan tombol Back di perangkat mobile untuk kembali ke fragment sebelumnya, Android juga membuat animasi yang merupakan kebalikan saat membuka sebuah fragment. Hal ini dilakukan secara otomatis tanpa perlu kode program tambahan.

Program penjelajah file yang saya buat juga bisa menampilkan isi sebuah file dalam bentuk sebuah activity baru dengan kode program seperti berikut ini:

Intent intent = new Intent(getActivity(), DisplayTextActivity.class);
intent.putExtra(DisplayTextActivity.DATA_FILE, selectedFile.getAbsolutePath());
FileAdapter fileAdapter = (FileAdapter) filesView.getAdapter();
ArrayList<String> daftarFile = fileAdapter.getDaftarFile();
intent.putStringArrayListExtra(DisplayTextActivity.DATA_DAFTAR_FILE, daftarFile);
intent.putExtra(DisplayTextActivity.DATA_INDEX_FILE, daftarFile.indexOf(selectedFile.getAbsolutePath()));            
startActivity(intent);

Android secara otomatis akan melakukan animasi pada saat perpindahan activity. Walaupun demikian, saya tetap bisa memakai animasi yang berbeda dengan melewatkannya dalam bentuk parameter Bundle seperti pada contoh berikut ini:

View selectedView = filesView.getChildAt(position - filesView.getFirstVisiblePosition());
Bundle bundle = ActivityOptions.makeScaleUpAnimation(filesView, selectedView.getLeft(), selectedView.getTop(), 0, 0).toBundle();
startActivity(intent, bundle);

Sekarang, bila saya membuka activity baru, saya akan menemukan animasi seperti berikut ini:

Animasi pada perpindahan activity

Animasi pada perpindahan activity

Selain memakai animasi yang sudah disediakan, saya juga bisa mendefinisikan sebuah animasi dalam bentuk XML. Sebagai contoh, pada penjelajah file yang saya buat, pengguna bisa melihat isi file sebelumnya atau berikutnya (dalam folder yang sama) dengan menyapu layar ke kiri dan ke kanan. Saya akan mendefinisikan sebuah animasi untuk perpindahan file dengan men-klik kanan folder res dan memilih menu New, Android Resource File. Saya kemudian mengisi dialog yang muncul seperti pada gambar berikut ini:

Mendefinisikan animasi dalam bentuk XML

Mendefinisikan animasi dalam bentuk XML

Saya kemudian mengisi XML yang dihasilkan dengan:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <objectAnimator
        android:valueFrom="1"
        android:valueTo="0"
        android:propertyName="alpha"
        android:duration="0" />

    <objectAnimator
        android:valueFrom="300"
        android:valueTo="0"
        android:propertyName="translationX"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="300" />

    <objectAnimator
        android:valueFrom="0"
        android:valueTo="1"
        android:propertyName="alpha"
        android:duration="300" />

</set>

XML di atas pada dasarnya adalah cara deklaratif untuk membuat object dari class ObjectAnimator (https://developer.android.com/reference/android/animation/ObjectAnimator.html). Pada deklarasi di atas, saya membuat beberapa animator yang akan mengubah nilai property translationX dan alpha milik target. Nilai translationX akan mempengaruhi posisi X dari target. Nilai alpha akan mempengaruhi transparansi target (nilai 0 membuat target tidak terlihat sama sekali). Karena animasi saat ini tidak terikat pada sebuah target tertentu, saya bisa memakai ulang animasi yang sama pada banyak View berbeda.

Saya kemudian membuat XML dengan nama kiri_keluar.xml yang isinya seperti berikut ini:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <objectAnimator
        android:valueFrom="1"
        android:valueTo="0"
        android:propertyName="scaleX"
        android:interpolator="@android:interpolator/accelerate_quad"
        android:duration="300" />

    <objectAnimator
        android:valueFrom="1"
        android:valueTo="0"
        android:propertyName="scaleY"
        android:interpolator="@android:interpolator/accelerate_quad"
        android:duration="300" />

    <objectAnimator
        android:valueFrom="0"
        android:valueTo="-200"
        android:propertyName="translationX"
        android:interpolator="@android:interpolator/accelerate_quad"
        android:duration="300" />

    <objectAnimator
        android:valueFrom="1"
        android:valueTo="0"
        android:propertyName="alpha"
        android:duration="300" />

</set>

Kali ini, saya melakukan animasi pada property scaleX dan scaleY yang mempengaruhi ukuran dari sebuah target.

Berikutnya, saya mendeklarasikan animasi dengan nama kanan_masuk.xml seperti berikut ini:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <objectAnimator
        android:valueFrom="1"
        android:valueTo="0"
        android:propertyName="alpha"
        android:duration="0" />

    <objectAnimator
        android:valueFrom="-300"
        android:valueTo="0"
        android:propertyName="translationX"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="300" />

    <objectAnimator
        android:valueFrom="0"
        android:valueTo="1"
        android:propertyName="alpha"
        android:duration="300" />

</set>

Saya juga mendeklarasikan animasi dengan nama kanan_keluar.xml yang isinya seperti:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <objectAnimator
        android:valueFrom="1"
        android:valueTo="0"
        android:propertyName="scaleX"
        android:interpolator="@android:interpolator/accelerate_quad"
        android:duration="300" />

    <objectAnimator
        android:valueFrom="1"
        android:valueTo="0"
        android:propertyName="scaleY"
        android:interpolator="@android:interpolator/accelerate_quad"
        android:duration="300" />

    <objectAnimator
        android:valueFrom="0"
        android:valueTo="200"
        android:propertyName="translationX"
        android:interpolator="@android:interpolator/accelerate_quad"
        android:duration="300" />

    <objectAnimator
        android:valueFrom="1"
        android:valueTo="0"
        android:propertyName="alpha"
        android:duration="300" />

</set>

Sekarang, saya siap untuk memakai mereka pada saat membuat fragment baru. Sebagai contoh, saya mengubah kode program saya menjadi seperti berikut ini:

@Override
public void onNextFile() {
    if ((daftarFile != null) && (index < daftarFile.size() -1)) {
        index++;
        tampilkanFile(new File(daftarFile.get(index)), R.animator.kiri_masuk, R.animator.kiri_keluar);
    }
}

@Override
public void onPreviousFile() {
    if ((daftarFile != null) && (index > 0)) {
        index--;
        tampilkanFile(new File(daftarFile.get(index)), R.animator.kanan_masuk, R.animator.kanan_keluar);
    }
}

private void tampilkanFile(File file, int animasiMasuk, int animasiKeluar) {
    DisplayTextFragment displayTextFragment = new DisplayTextFragment();
    Bundle args = new Bundle();
    args.putString(DisplayTextFragment.DATA_FILE, file.getAbsolutePath());
    displayTextFragment.setArguments(args);
    FragmentTransaction transaction = getFragmentManager().beginTransaction();
    transaction.setCustomAnimations(animasiMasuk, animasiKeluar);
    transaction.replace(R.id.container, displayTextFragment);
    transaction.addToBackStack(null);
    transaction.commit();
}

Bila saya menyapu layar ke kiri dan ke kanan, saya akan memperoleh animasi seperti yang terlihat pada:

Contoh animasi buatan sendiri

Contoh animasi buatan sendiri

Game API di Java Micro Edition

Sebentar lagi masa kuliah semester genap akan berakhir, saatnya untuk liburan panjang… Aku akui liburan panjang hingga bulan September mendatang menyebabkan hampir gaji tiga bulan menjadi ‘kosong’..  Tapi dari sisi lain, liburan ini berarti aku bisa berpesta ria dengan aneka kode program tanpa diganggu sedikit pun.  Bayangkan, waktu kerja kantoran dulu, jatah cuti hanya 12 hari selama setahun.. Dan sekarang, sebagai dosen, aku bisa cuti selama dua bulan!!!

Selama mengajar dua semester ini, aku lebih banyak menghabiskan waktu menulis materi di modul kuliah ketimbang menulis di blog.  Maklum, itu merupakan salah satu tugas seorang dosen..  Salah satu mata kuliah menarik yang harus aku ajarkan adalah pemograman mobile dengan Java Micro Edition..  Aku sudah sering bermain-main dengan Java Enterprise Edition & Java Standard Edition, tapi baru kali ini menyentuh JME..  Sebagai informasi, aplikasi Android juga diprogram dengan Java, tetapi bukan versi JME..  Aplikasi BlackBerry sendiri mendukung JME, tetapi juga punya pengembangan aplikasi berbasis Java-nya sendiri yang khas untuk BlackBerry.

Salah satu hal menarik yang dari JME adalah dukungan Game API bawaan.  Hal ini karena JME banyak dipakai untuk membuat aplikasi game, istilah gaulnya: “game java” sehingga ia disertai dengan peralatan yang sangat membantu dalam pembuatan game. Inti dari Game API di JME adalah class GameCanvas yang merupakan turunan dari class Canvas.

Class GameCanvas menyediakan method getGraphics() dan flushGraphics() untuk mendukung double buffering.  Programmer bisa mendapatkan sebuah Graphics dengan memanggil getGraphics(), kemudian menggambar dengan menggunakan context tersebut, baru setelah selesai menggambar dan ingin menampilkan ke layar, ia tinggal memanggil flushGraphics().

Class lain dalam Game API adalah class TiledLayer dan class Sprite.  Kedua class tersebut mewakili objek dalam game yang dapat digambar secara otomatis oleh class LayerManager.

Salah satu cara untuk memahami konsep Layer di Game API adalah dengan memanfaatkan Game Builder dari NetBeans IDE.  Game Builder bukanlah bagian dari spesifikasi JME, melainkan alat bantu yang ditawarkan oleh NetBeans IDE.  Versi terbaru dari NetBeans IDE, yaitu versi 7.0, dapat didownload di http://netbeans.org/community/releases/70/ .

Untuk membuat sebuah Game Builder baru, klik kanan pada nama package, kemudian pilih New, Game Builder…  Berikut ini adalah contoh tampilan sample Game Builder dari NetBeans:

Tampilan Game Builder

Tampilan Game Builder

Scenes mewakili apa yang akan ditampilkan di layar nantinya, terdiri atas satu atau lebih TiledLayer dan Sprite.

TiledLayer adalah sebuah layer yang terdiri atas kotak-kotak dimana setiap ‘kotak’ ini diisi dengan gambar tertentu.  Perancang game harus membuat sebuah file gambar yang merupakan gabungan dari seluruh elemen yang dapat di-isi-kan ke dalam TiledLayer tersebut.  Contoh file gambarnya seperti:

Contoh Gambar Untuk Game API

Contoh Gambar Untuk Game API

Game Builder menawarkan kemudahan untuk mengisi setiap kotak atau ’tile’ yang ada di gambar secara visual, seperti yang terlihat di gambar berikut:

Tiled Layer Editor

Tiled Layer Editor

Pembuat game dapat memisahkan setiap elemen interaksi yang berbeda ke dalam TiledLayer yang berbeda.  Misalnya sebuah TiledLayer untuk background game, dan sebuah TiledLayer lagi untuk mewakili bebatuan atau penghalang.  Dengan demikian, pada saat karakter utama bergerak, programmer dapat memanggil method collidesWith() untuk memeriksa apakah karakter bersinggungan dengan TiledLayer tertentu.

Karakter yang ada di game diwakili oleh class Sprite.  Game Builder menyediakan editor visual untuk merancang dan memutar animasi sebuah Sprite seperti pada contoh tampilan berikut ini:

Sprite Editor

Sprite Editor

Setelah selesai merancang TiledLayer dan Sprite, yang harus dilakukan berikutnya adalah menggambar mereka di layar dengan menggunakan method paint() dari LayerManager.  Jangan khawatir soal pergeseran layar karena programmer dapat memanggil method setViewWindow() milik LayerManager untuk mengkliping layar.

Berikut ini adalah contoh tampilan hasil game yang dibuat dengan Game Builder bila dijalankan menggunakan emulator bawaan JME:

Contoh Tampilan

Contoh Tampilan

Full-screen Exclusive API: Menggambar Satu Layar Penuh

Tidak semua program puas menggambar di komponen Swing. Passive rendering yang memakai method paint(Graphics g) tidak sesuai untuk program grafis yang harus memiliki performance tinggi. Contohnya adalah program game, yang umumnya memakai full-screen active rendering. Umumnya, program seperti game memakai bantuan API DirectX atau OpenGL. Java juga mendukung penggambaran full-screen dengan double-buffering berupa page-flipping. Tapi karena Java dirancang untuk multi-platform, tidak seluruh platform mendukung fitur ini. Aku dapat memeriksanya dengan kode seperti berikut:

GraphicsEnvironment env = GraphicsEnvironment.
  getLocalGraphicsEnvironment();
GraphicsDevice gd = env.getDefaultScreenDevice();
if (gd.isFullScreenSupported()) {
  System.out.println("Fullscreen API is supported.");
} else {
  System.out.println("Fullscreen API is not supported.");
}
if (gd.isDisplayChangeSupported()) {
  System.out.println("Display resolution change is supported.");
} else {
  System.out.println("Display resolution change is not supported.");
}

DisplayMode dm = gd.getDisplayMode();
System.out.format("Display resolution %d x %d (%d bit, refresh rate %d Hz)",
  dm.getWidth(), dm.getHeight(), dm.getBitDepth(), dm.getRefreshRate());

Sekarang aku akan latihan membuat sebuah program yang menggambar full screen sekumpulan lingkaran dengan posisi dan warna yang acak, seperti berikut:

import java.awt.Color;
import java.awt.Graphics;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.image.BufferStrategy;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;

public class Main extends JFrame {
  
  private static final long serialVersionUID = 4648172894076113183L;
  
  public Main() {
    GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
    GraphicsDevice device = env.getDefaultScreenDevice();
  
    setUndecorated(false);
    setResizable(false);
    setIgnoreRepaint(true);
    device.setFullScreenWindow(this);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    createBufferStrategy(2);
    BufferStrategy buffer = getBufferStrategy();
    
    Graphics g;
    List<LingkaranWarna> lstLingkaran = new ArrayList<LingkaranWarna>();
    while (true) {
      if (lstLingkaran.size()<100) {
        lstLingkaran.add(new LingkaranWarna());
      } else {
        lstLingkaran.clear();
      }
      g = buffer.getDrawGraphics();
      g.clearRect(0,0, getWidth(), getHeight());
      for (LingkaranWarna b : lstLingkaran) {
        b.gambar(g);
      }
      g.dispose();
      buffer.show();
      
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      } 
    }
    
  }

  protected class LingkaranWarna {

    private int lokasiX;
    private int lokasiY;
    private Color warna;
    private int sizeX;
    private int sizeY;
    
    public LingkaranWarna() {
      atributBaru();
    }
    
    private void atributBaru() {
      Random rand = new Random();
      lokasiX = rand.nextInt(1000);
      lokasiY = rand.nextInt(600);
      warna = new Color(rand.nextInt(255), rand.nextInt(255), rand.nextInt(255));
      sizeX = rand.nextInt(100);
      sizeY = rand.nextInt(100);
    }
    
    private void gambar(Graphics g) {
      g.setColor(warna);
      g.fillOval(lokasiX, lokasiY, sizeX, sizeY);
      atributBaru();
    }
  }
  public static void main(String[] args) {
    new Main();
  }

}