Membuat gambar kustom

Mencoba 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 bisa mudah atau kompleks sesuai dengan kebutuhan aplikasi Anda. Dokumen ini mencakup beberapa operasi yang paling umum.

Untuk informasi selengkapnya, lihat Ringkasan drawable.

Mengganti onDraw()

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

Mulai dengan membuat Objek Paint. Bagian selanjutnya dalam artikel ini membahas Paint lebih mendalam.

Membuat objek gambar

Tujuan 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 mendefinisikan apakah akan mengisi persegi panjang itu 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 Anda menggambar.

Sebelum menggambar apa pun, buat satu atau beberapa objek Paint. Tujuan contoh berikut melakukannya dalam metode yang disebut init. Metode ini adalah 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. Penayangan adalah sering digambar ulang, dan banyak objek gambar memerlukan inisialisasi yang mahal. Membuat objek gambar dalam metode onDraw() Anda secara signifikan akan menurunkan performa dan dapat membuat UI menjadi lambat.

Menangani peristiwa tata letak

Untuk menggambar tampilan kustom dengan benar, cari tahu ukurannya. Kustom kompleks sering kali perlu melakukan beberapa penghitungan tata letak tergantung pada ukurannya dan bentuk area mereka di layar. Jangan pernah membuat asumsi tentang ukuran pada layar. Meskipun hanya ada satu aplikasi yang menggunakan tampilan Anda, aplikasi tersebut harus menangani berbagai ukuran layar, beberapa kepadatan layar, dan berbagai aspek rasio dalam mode potret dan lanskap.

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

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

Saat tampilan Anda diberi ukuran, pengelola tata letak akan mengasumsikan bahwa ukuran tersebut menyertakan padding tampilan. Menangani nilai padding ketika Anda menghitung ukuran tampilan. Berikut ini cuplikan dari onSizeChanged() yang menunjukkan bagaimana untuk 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 Anda memerlukan kontrol yang lebih baik atas parameter tata letak tampilan, terapkan onMeasure(). Parameter metode ini View.MeasureSpec nilai yang memberi tahu Anda seberapa besar tampilan yang diinginkan orang tua untuk Anda dan apakah ukuran itu merupakan batas maksimum atau sekadar saran. Sebagai pengoptimalan, nilai-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(). Di sini implementasi, model ini mencoba membuat areanya cukup besar untuk membuat diagram sebesar sebagai 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 bantuan resolveSizeAndState() digunakan untuk membuat nilai lebar dan tinggi akhir. Helper ini mengembalikan nilai View.MeasureSpec yang sesuai dengan membandingkan ukuran tampilan yang diperlukan ke nilai yang diteruskan ke onMeasure().
  • onMeasure() tidak memiliki nilai kembalian. Sebagai gantinya, metode mengkomunikasikan hasilnya dengan memanggil setMeasuredDimension(). Pemanggilan metode ini bersifat wajib. Jika Anda menghilangkan panggilan ini, Class View menampilkan pengecualian runtime.

Gambar

Setelah menentukan pembuatan dan pengukuran objek, Anda dapat mengimplementasikan onDraw(). Setiap tampilan mengimplementasikan onDraw() dengan cara yang berbeda, tetapi ada beberapa operasi umum yang sama-sama dimiliki oleh sebagian besar penayangan:

  • Gambar teks menggunakan drawText(). Tentukan jenis huruf dengan memanggil setTypeface() dan warna teks dengan memanggil setColor().
  • Gambar bentuk dasar menggunakan drawRect(), drawOval(), dan drawArc(). Ubah apakah bentuk akan diisi, diberi garis batas, atau keduanya dengan memanggil setStyle().
  • Menggambar bentuk yang lebih kompleks menggunakan Path . Tentukan bentuk dengan menambahkan garis dan kurva ke Path lalu menggambar bentuknya menggunakan drawPath(). Seperti bentuk dasar, jalur dapat digarisbatasi, diisi, atau keduanya, bergantung pada setStyle().
  • Tentukan isi gradien dengan membuat LinearGradient objek terstruktur dalam jumlah besar. Telepon 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 RenderEffect yang menerapkan efek grafis umum seperti buram, filter warna, Efek shader Android, dan lainnya untuk View objek dan hierarki rendering. Anda dapat menggabungkan efek sebagai efek berantai, yang terdiri efek dalam dan luar, atau efek campuran. Dukungan untuk fitur ini bervariasi tergantung pada daya pemrosesan perangkat.

Anda juga dapat menerapkan efek pada RenderNode untuk View dengan memanggil View.setRenderEffect(RenderEffect).

Untuk mengimplementasikan objek RenderEffect, lakukan hal berikut:

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

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