Gambar Kustom

Bagian terpenting dari tampilan kustom adalah penampilannya. Gambar kustom dapat mudah atau kompleks tergantung kebutuhan aplikasi Anda. Pelajaran ini mencakup beberapa operasi yang paling umum.

Selain dalam pelajaran ini, Anda dapat menemukan informasi terkait lainnya di bagian Canvas dan Drawable.

Mengganti onDraw()

Langkah terpenting dalam menggambar tampilan kustom adalah mengganti metode onDraw(). Parameter untuk onDraw() adalah objek Canvas yang dapat digunakan oleh tampilan untuk menggambar. Class Canvas menentukan metode untuk menggambar teks, garis, bitmap, dan banyak elemen grafis dasar lainnya. Anda dapat menggunakan metode ini di onDraw() untuk membuat antarmuka pengguna (UI) kustom.

Namun, sebelum dapat memanggil metode menggambar apa pun, Anda perlu membuat objek Paint. Bagian selanjutnya dalam artikel ini membahas Paint lebih mendalam.

Membuat Objek Gambar

Framework android.graphics membagi gambar menjadi dua area:

  • Apa yang digambar, ditangani oleh Canvas
  • Bagaimana menggambarnya, ditangani oleh Paint.

Misalnya, Canvas menyediakan metode untuk menggambar garis, sedangkan Paint menyediakan metode untuk menentukan warna garis tersebut. Canvas memiliki metode untuk menggambar persegi panjang, sedangkan Paint menentukan apakah persegi panjang itu akan diisi warna atau dibiarkan kosong. Sederhananya, Canvas menentukan bentuk yang dapat Anda gambar di layar, sedangkan Paint menentukan warna, gaya, font, dan sebagainya dari setiap bentuk yang Anda gambar.

Jadi, sebelum menggambar apa pun, Anda perlu membuat satu atau beberapa objek Paint. Contoh PieChart melakukan hal ini dalam metode yang disebut init, yang dipanggil dari konstruktor dari Java, tetapi kita dapat menginisialisasi secara inline di Kotlin:

Kotlin

    private val textPaint = Paint(ANTI_ALIAS_FLAG).apply {
        color = textColor
        if (textHeight == 0f) {
            textHeight = textSize
        } else {
            textSize = textHeight
        }
    }

    private val piePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        style = Paint.Style.FILL
        textSize = textHeight
    }

    private val shadowPaint = Paint(0).apply {
        color = 0x101010
        maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.NORMAL)
    }
    

Java

    private void init() {
       textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
       textPaint.setColor(textColor);
       if (textHeight == 0) {
           textHeight = textPaint.getTextSize();
       } else {
           textPaint.setTextSize(textHeight);
       }

       piePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
       piePaint.setStyle(Paint.Style.FILL);
       piePaint.setTextSize(textHeight);

       shadowPaint = new Paint(0);
       shadowPaint.setColor(0xff101010);
       shadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL));

       ...
    

Membuat objek di awal merupakan langkah pengoptimalan yang penting. Tampilan sangat sering digambar ulang, dan banyak objek gambar memerlukan inisialisasi yang mahal. Membuat objek gambar dalam metode onDraw() mengurangi performa secara signifikan dan dapat membuat UI Anda terasa lamban.

Menangani Peristiwa Tata Letak

Agar dapat menggambar tampilan kustom dengan baik, Anda perlu mengetahui ukurannya. Tampilan kustom yang kompleks sering kali perlu melakukan beberapa kalkulasi tata letak yang bergantung pada ukuran dan bentuk areanya di layar. Anda tidak boleh mengasumsikan ukuran tampilan di layar. Meskipun hanya ada satu aplikasi yang menggunakan tampilan Anda, aplikasi tersebut perlu menangani berbagai ukuran layar, berbagai kepadatan layar, dan berbagai rasio tinggi lebar dalam mode potret dan lanskap.

Meskipun View memiliki banyak metode untuk menangani pengukuran, sebagian besarnya tidak perlu diganti. Jika tampilan Anda tidak memerlukan kontrol khusus atas ukurannya, Anda hanya perlu mengganti satu metode: onSizeChanged().

onSizeChanged() dipanggil saat tampilan Anda pertama kali diberi ukuran, dan dipanggil sekali lagi jika ukuran tampilan berubah karena alasan apa pun. Hitung posisi, dimensi, dan nilai lain apa pun yang terkait dengan ukuran tampilan Anda di onSizeChanged(), alih-alih menghitungnya ulang setiap kali Anda menggambar. Dalam contoh PieChart, onSizeChanged() adalah tempat tampilan PieChart menghitung persegi panjang pembatas diagram lingkaran serta posisi relatif label teks dan elemen visual lainnya.

Saat tampilan Anda diberi ukuran, pengelola tata letak akan mengasumsikan bahwa ukuran tersebut mencakup semua padding tampilan. Anda harus menangani nilai padding ini saat menghitung ukuran tampilan. Berikut ini cuplikan dari PieChart.onSizeChanged() yang menunjukkan cara melakukannya:

Kotlin

    // Account for padding
    var xpad = (paddingLeft + paddingRight).toFloat()
    val ypad = (paddingTop + paddingBottom).toFloat()

    // Account for the label
    if (showText) xpad += textWidth

    val ww = w.toFloat() - xpad
    val hh = h.toFloat() - ypad

    // Figure out how big we can make the pie.
    val diameter = Math.min(ww, hh)
    

Java

    // Account for padding
    float xpad = (float)(getPaddingLeft() + getPaddingRight());
    float ypad = (float)(getPaddingTop() + getPaddingBottom());

    // Account for the label
    if (showText) xpad += textWidth;

    float ww = (float)w - xpad;
    float hh = (float)h - ypad;

    // Figure out how big we can make the pie.
    float diameter = Math.min(ww, hh);
    

Jika memerlukan kontrol yang lebih terperinci atas parameter tata letak tampilan, terapkan onMeasure(). Parameter metode ini adalah nilai View.MeasureSpec yang memberitahukan seberapa besar ukuran tampilan yang dikehendaki oleh induk tampilan Anda, dan apakah ukuran itu merupakan batas maksimal atau sekadar saran. Sebagai upaya pengoptimalan, nilai-nilai ini disimpan sebagai integer yang dikemas, dan gunakan metode statis View.MeasureSpec untuk mengekstrak informasi yang disimpan di setiap integer.

Berikut ini contoh implementasi onMeasure(). Dalam implementasi ini, PieChart berupaya menjadikan areanya cukup besar untuk membuat lingkaran itu sebesar labelnya:

Kotlin

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // Try for a width based on our minimum
        val minw: Int = paddingLeft + paddingRight + suggestedMinimumWidth
        val w: Int = View.resolveSizeAndState(minw, widthMeasureSpec, 1)

        // Whatever the width ends up being, ask for a height that would let the pie
        // get as big as it can
        val minh: Int = View.MeasureSpec.getSize(w) - textWidth.toInt() + paddingBottom + paddingTop
        val h: Int = View.resolveSizeAndState(
                View.MeasureSpec.getSize(w) - textWidth.toInt(),
                heightMeasureSpec,
                0
        )

        setMeasuredDimension(w, h)
    }
    

Java

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       // Try for a width based on our minimum
       int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
       int w = resolveSizeAndState(minw, widthMeasureSpec, 1);

       // Whatever the width ends up being, ask for a height that would let the pie
       // get as big as it can
       int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop();
       int h = resolveSizeAndState(MeasureSpec.getSize(w) - (int)mTextWidth, heightMeasureSpec, 0);

       setMeasuredDimension(w, h);
    }
    

Ada tiga hal penting yang perlu diperhatikan dalam kode ini:

  • Kalkulasi memperhitungkan padding tampilan. Seperti disebutkan di atas, hal ini ditangani oleh tampilan.
  • Metode helper resolveSizeAndState() digunakan untuk membuat nilai lebar dan tinggi akhir. Helper ini menampilkan nilai View.MeasureSpec yang sesuai dengan membandingkan ukuran tampilan yang diinginkan dengan spesifikasi yang diteruskan ke onMeasure().
  • onMeasure() tidak memiliki nilai kembalian. Sebagai gantinya, metode tersebut menyampaikan hasilnya dengan memanggil setMeasuredDimension(). Pemanggilan metode ini bersifat wajib. Jika Anda menghilangkan panggilan ini, class View akan memunculkan pengecualian waktu proses.

Mulai Menggambar

Setelah menentukan pembuatan objek dan kode pengukuran, Anda dapat mengimplementasikan onDraw(). Setiap tampilan mengimplementasikan onDraw() dengan cara berbeda, tetapi beberapa operasi umum berikut selalu dilakukan oleh sebagian besar tampilan:

  • Gambar teks menggunakan drawText(). Tentukan jenis huruf dengan memanggil setTypeface(), dan warna teks dengan memanggil setColor().
  • Gambar bentuk-bentuk dasar menggunakan drawRect(), drawOval(), dan drawArc(). Ubah apakah bentuk akan diisi, diberi garis batas, atau keduanya dengan memanggil setStyle().
  • Gambar bentuk yang lebih kompleks menggunakan class Path. Tentukan bentuk dengan menambahkan garis dan kurva ke objek Path, lalu gambar bentuk menggunakan drawPath(). Sama seperti bentuk-bentuk dasar, path dapat diberi garis batas, diisi, atau keduanya, bergantung pada setStyle().
  • Tentukan isi gradien dengan membuat objek LinearGradient. Panggil setShader() untuk menggunakan LinearGradient Anda pada bentuk yang terisi.
  • Gambar bitmap menggunakan drawBitmap().

Misalnya, berikut adalah kode yang menggambar PieChart. Kode ini menggunakan campuran teks, garis, dan bentuk.

Kotlin

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        canvas.apply {
            // Draw the shadow
            drawOval(shadowBounds, shadowPaint)

            // Draw the label text
            drawText(data[mCurrentItem].mLabel, textX, textY, textPaint)

            // Draw the pie slices
            data.forEach {
                piePaint.shader = it.mShader
                drawArc(bounds,
                        360 - it.endAngle,
                        it.endAngle - it.startAngle,
                        true, piePaint)
            }

            // Draw the pointer
            drawLine(textX, pointerY, pointerX, pointerY, textPaint)
            drawCircle(pointerX, pointerY, pointerSize, mTextPaint)
        }
    }
    

Java

    protected void onDraw(Canvas canvas) {
       super.onDraw(canvas);

       // Draw the shadow
       canvas.drawOval(
               shadowBounds,
               shadowPaint
       );

       // Draw the label text
       canvas.drawText(data.get(currentItem).mLabel, textX, textY, textPaint);

       // Draw the pie slices
       for (int i = 0; i < data.size(); ++i) {
           Item it = data.get(i);
           piePaint.setShader(it.shader);
           canvas.drawArc(bounds,
                   360 - it.endAngle,
                   it.endAngle - it.startAngle,
                   true, piePaint);
       }

       // Draw the pointer
       canvas.drawLine(textX, pointerY, pointerX, pointerY, textPaint);
       canvas.drawCircle(pointerX, pointerY, pointerSize, mTextPaint);
    }