Membuat gambar kustom

Coba cara Compose
Jetpack Compose adalah toolkit UI yang direkomendasikan untuk Android. Pelajari cara menggunakan tata letak di Compose.

Bagian terpenting dari tampilan kustom adalah penampilannya. Gambar kustom dapat mudah atau rumit sesuai kebutuhan aplikasi Anda. Dokumen ini mencakup beberapa operasi yang paling umum.

Untuk mengetahui informasi selengkapnya, lihat Ringkasan drawable.

Mengganti onDraw()

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

Mulailah dengan 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 akan digambar, ditangani oleh Canvas.
  • Bagaimana menggambarnya, ditangani oleh Paint.

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

Sebelum menggambar apa pun, buat satu atau beberapa objek Paint. Contoh berikut melakukannya dalam metode yang disebut init. Metode ini dipanggil dari konstruktor dari Java, tetapi dapat diinisialisasi secara inline di Kotlin.

Kotlin

@ColorInt
private var textColor    // Obtained from style attributes.

@Dimension
private var textHeight   // Obtained from style attributes.

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 Paint textPaint;
private Paint piePaint;
private Paint shadowPaint;

@ColorInt
private int textColor;       // Obtained from style attributes.

@Dimension
private float textHeight;    // Obtained from style attributes.

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 sering digambar ulang, dan banyak objek gambar memerlukan inisialisasi yang mahal. Membuat objek gambar dalam metode onDraw() akan mengurangi performa secara signifikan dan dapat membuat UI Anda menjadi lambat.

Menangani peristiwa tata letak

Untuk menggambar tampilan kustom dengan benar, cari tahu ukurannya. Tampilan kustom yang kompleks sering kali perlu melakukan beberapa penghitungan tata letak bergantung pada ukuran dan bentuk areanya di layar. Jangan pernah berasumsi tentang ukuran tampilan Anda di layar. Meskipun hanya satu aplikasi yang menggunakan tampilan Anda, aplikasi tersebut perlu menangani berbagai ukuran layar, beberapa kepadatan layar, dan berbagai rasio aspek 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, hanya ganti 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(), bukan menghitungnya ulang setiap kali Anda menggambar. Dalam contoh berikut, onSizeChanged() adalah tempat tampilan menghitung kotak pembatas diagram dan posisi relatif label teks dan elemen visual lainnya.

Saat tampilan Anda diberi ukuran, pengelola tata letak mengasumsikan bahwa ukuran menyertakan padding tampilan. Tangani nilai padding saat Anda menghitung ukuran tampilan. Berikut adalah cuplikan dari onSizeChanged() yang menunjukkan cara melakukannya:

Kotlin

private val showText    // Obtained from styled attributes.
private val textWidth   // Obtained from styled attributes.

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    // Account for padding.
    var xpad = (paddingLeft + paddingRight).toFloat()
    val ypad = (paddingTop + paddingBottom).toFloat()

    // Account for the label.
    if (showText) xpad += textWidth.toFloat()
    val ww = w.toFloat() - xpad
    val hh = h.toFloat() - ypad

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

Java

private Boolean showText;    // Obtained from styled attributes.
private int textWidth;       // Obtained from styled attributes.

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    // 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 you can make the pie.
    float diameter = Math.min(ww, hh);
}

Jika memerlukan kontrol yang lebih baik atas parameter tata letak tampilan, terapkan onMeasure(). Parameter metode ini adalah nilai View.MeasureSpec yang memberi tahu seberapa besar ukuran tampilan yang dikehendaki oleh induk tampilan Anda dan apakah ukuran tersebut merupakan batas maksimal atau sekadar saran. Sebagai pengoptimalan, nilai ini disimpan sebagai bilangan bulat yang dikemas, dan Anda menggunakan metode statis View.MeasureSpec untuk mengekstrak informasi yang disimpan di setiap bilangan bulat.

Berikut ini contoh implementasi onMeasure(). Dalam implementasi ini, kode ini mencoba membuat areanya cukup besar untuk membuat diagram sebesar labelnya:

Kotlin

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

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

    setMeasuredDimension(w, h)
}

Java

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

   // Whatever the width is, ask for a height that lets the pie get as big as it
   // can.
   int minh = MeasureSpec.getSize(w) - (int)textWidth + getPaddingBottom() + getPaddingTop();
   int h = resolveSizeAndState(minh, heightMeasureSpec, 0);

   setMeasuredDimension(w, h);
}

Ada tiga hal penting yang perlu diperhatikan dalam kode ini:

  • Kalkulasi memperhitungkan padding tampilan. Seperti yang disebutkan sebelumnya, ini adalah tanggung jawab 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 diperlukan dengan nilai yang diteruskan ke onMeasure().
  • onMeasure() tidak memiliki nilai kembalian. Sebagai gantinya, metode ini mengomunikasikan hasilnya dengan memanggil setMeasuredDimension(). Memanggil metode ini bersifat wajib. Jika Anda menghilangkan panggilan ini, class View akan menampilkan pengecualian runtime.

Gambar

Setelah menentukan pembuatan objek dan kode pengukuran, Anda dapat mengimplementasikan onDraw(). Setiap tampilan mengimplementasikan onDraw() dengan cara berbeda, tetapi ada beberapa operasi umum yang dibagikan 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 diisi, diberi garis batas, atau keduanya dengan memanggil setStyle().
  • Gambar bentuk yang lebih kompleks menggunakan class Path. Tentukan bentuk dengan menambahkan garis dan kurva pada objek Path, lalu gambar bentuk menggunakan drawPath(). Seperti bentuk-bentuk dasar, jalur dapat diberi garis batas, diisi, atau keduanya, bergantung pada setStyle().
  • Tentukan isi gradien dengan membuat objek LinearGradient. Panggil setShader() untuk menggunakan LinearGradient pada bentuk yang diisi.
  • Gambar bitmap menggunakan drawBitmap().

Kode berikut menggambar campuran teks, garis, dan bentuk:

Kotlin

private val data = mutableListOf<Item>() // A list of items that are displayed.

private var shadowBounds = RectF()       // Calculated in onSizeChanged.
private var pointerRadius: Float = 2f    // Obtained from styled attributes.
private var pointerX: Float = 0f         // Calculated in onSizeChanged.
private var pointerY: Float = 0f         // Calculated in onSizeChanged.
private var textX: Float = 0f            // Calculated in onSizeChanged.
private var textY: Float = 0f            // Calculated in onSizeChanged.
private var bounds = RectF()             // Calculated in onSizeChanged.
private var currentItem: Int = 0         // The index of the currently selected item.

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

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

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

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

        // Draw the pointer.
        drawLine(textX, pointerY, pointerX, pointerY, textPaint)
        drawCircle(pointerX, pointerY, pointerRadius, textPaint)
    }
}

// Maintains the state for a data item.
private data class Item(
    var label: String,      
    var value: Float = 0f,

    @ColorInt
    var color: Int = 0,

    // Computed values.
    var startAngle: Float = 0f,
    var endAngle: Float = 0f,

    var shader: Shader
)

Java

private List<Item> data = new ArrayList<Item>();  // A list of items that are displayed.

private RectF shadowBounds;                       // Calculated in onSizeChanged.
private float pointerRadius;                      // Obtained from styled attributes.
private float pointerX;                           // Calculated in onSizeChanged.
private float pointerY;                           // Calculated in onSizeChanged.
private float textX;                              // Calculated in onSizeChanged.
private float textY;                              // Calculated in onSizeChanged.
private RectF bounds;                             // Calculated in onSizeChanged.
private int currentItem = 0;                      // The index of the currently selected item.

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

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

    // Draw the label text.
    canvas.drawText(data.get(currentItem).label, 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, pointerRadius, textPaint);
}

// Maintains the state for a data item.
private class Item {
    public String label;
    public float value;
    @ColorInt
    public int color;

    // Computed values.
    public int startAngle;
    public int endAngle;

    public Shader shader;
}    

Menerapkan efek grafis

Android 12 (API level 31) menambahkan class RenderEffect, yang menerapkan efek grafis umum seperti pemburaman, filter warna, efek shader Android, dan lainnya ke objek View dan hierarki rendering. Anda dapat menggabungkan efek sebagai efek berantai, yang terdiri dari efek dalam dan luar, atau efek campuran. Dukungan untuk fitur ini bervariasi tergantung daya pemrosesan perangkat.

Anda juga dapat menerapkan efek ke RenderNode yang mendasarinya untuk View dengan memanggil View.setRenderEffect(RenderEffect).

Untuk mengimplementasikan objek RenderEffect, lakukan hal berikut:

view.setRenderEffect(RenderEffect.createBlurEffect(radiusX, radiusY, SHADER_TILE_MODE))

Anda dapat membuat tampilan secara terprogram atau meng-inflate-nya dari tata letak XML, lalu mengambilnya menggunakan View binding atau findViewById().