Özel çizim oluşturma

Compose yöntemini deneyin
Jetpack Compose, Android için önerilen kullanıcı arayüzü araç setidir. Compose'da düzenlerle nasıl çalışacağınızı öğrenin.

Özel görünümün en önemli kısmı görünümüdür. Özel çizim, uygulamanızın ihtiyaçlarına göre kolay veya karmaşık olabilir. Bu belgede, en yaygın işlemlerden bazıları ele alınmaktadır.

Daha fazla bilgi için Çekilebilir öğelere genel bakış başlıklı makaleyi inceleyin.

onDraw() işlevini geçersiz kılma

Özel bir görünüm çizmenin en önemli adımı, onDraw() yöntemini geçersiz kılmaktır. onDraw() parametresi, görünümün kendisini çizmek için kullanabileceği bir Canvas nesnesidir. Canvas sınıfı, metin, çizgi, bit eşlem ve diğer birçok grafik öğesini çizme yöntemlerini tanımlar. onDraw()'da özel kullanıcı arayüzünüzü (UI) oluşturmak için bu yöntemleri kullanabilirsiniz.

Paint nesnesi oluşturarak başlayın. Sonraki bölümde Paint daha ayrıntılı olarak ele alınmaktadır.

Çizim nesneleri oluşturma

android.graphics çerçevesi, çizimi iki alana ayırır:

  • Canvas tarafından işlenen ne çizileceği.
  • Paint tarafından yönetilen How to draw.

Örneğin, Canvas bir çizgi çizme yöntemi sunarken Paint bu çizginin rengini tanımlama yöntemleri sunar. Canvas dikdörtgen çizme yöntemine sahiptir ve Paint dikdörtgenin renkle doldurulup doldurulmayacağını tanımlar. Canvas, ekranda çizebileceğiniz şekilleri tanımlar. Paint ise çizdiğiniz her şeklin rengini, stilini, yazı tipini vb. tanımlar.

Çizim yapmadan önce bir veya daha fazla Paint nesne oluşturun. Aşağıdaki örnekte bu işlem init adlı bir yöntemde yapılıyor. Bu yöntem, Java'daki oluşturucudan çağrılır ancak Kotlin'de satır içi olarak başlatılabilir.

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));
   ...
}

Nesneleri önceden oluşturmak önemli bir optimizasyon yöntemidir. Görünümler sık sık yeniden çizilir ve birçok çizim nesnesi pahalı başlatma işlemleri gerektirir. onDraw() yönteminizde çizim nesneleri oluşturmak performansı önemli ölçüde düşürür ve kullanıcı arayüzünüzün yavaşlamasına neden olabilir.

Düzen etkinliklerini işleme

Özel görünümünüzü düzgün şekilde çizmek için boyutunu öğrenin. Karmaşık özel görünümler, ekrandaki alanlarının boyutuna ve şekline bağlı olarak genellikle birden fazla düzen hesaplaması yapmalıdır. Ekrandaki görünümünüzün boyutuyla ilgili hiçbir zaman varsayımda bulunmayın. Görünümünüzü yalnızca bir uygulama kullanıyor olsa bile bu uygulamanın, hem dikey hem de yatay modda farklı ekran boyutlarını, birden fazla ekran yoğunluğunu ve çeşitli en-boy oranlarını işlemesi gerekir.

View ölçümün işlenmesi için birçok yönteme sahip olsa da bunların çoğu geçersiz kılınmamalıdır. Görünümünüzün boyutu üzerinde özel bir kontrol gerekmiyorsa yalnızca bir yöntemi geçersiz kılın: onSizeChanged().

onSizeChanged(), görünümünüze ilk kez bir boyut atandığında ve görünümünüzün boyutu herhangi bir nedenle değiştiğinde tekrar çağrılır. Çizim yaptığınız her seferde yeniden hesaplamak yerine, onSizeChanged() içinde konumları, boyutları ve görünümünüzün boyutuyla ilgili diğer tüm değerleri hesaplayın. Aşağıdaki örnekte, onSizeChanged() görünümün, grafiğin sınırlayıcı dikdörtgenini ve metin etiketinin diğer görsel öğelere göre konumunu hesapladığı yerdir.

Görünümünüze bir boyut atandığında düzen yöneticisi, boyuta görünümün dolgusunun dahil edildiğini varsayar. Görünümünüzün boyutunu hesaplarken dolgu değerlerini işleyin. Bunu nasıl yapacağınızı gösteren onSizeChanged() adlı makaleden bir snippet'i aşağıda bulabilirsiniz:

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);
}

Görünümünüzün düzen parametreleri üzerinde daha ayrıntılı kontrol sağlamanız gerekiyorsa onMeasure() uygulayın. Bu yöntemin parametreleri şunlardır: View.MeasureSpec Görünümünüzün üst öğesinin, görünümünüzün ne kadar büyük olmasını istediğini ve bu boyutun kesin bir maksimum değer mi yoksa yalnızca bir öneri mi olduğunu belirten değerler. Bu değerler, optimizasyon amacıyla paketlenmiş tam sayılar olarak depolanır. Her tam sayıda depolanan bilgileri paketinden çıkarmak için View.MeasureSpec statik yöntemlerini kullanırsınız.

onMeasure() için örnek bir uygulama aşağıda verilmiştir. Bu uygulamada, alanın grafiği etiketi kadar büyük hale getirmek için yeterince büyük olması amaçlanır:

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);
}

Bu kodda dikkat edilmesi gereken üç önemli nokta vardır:

  • Hesaplamalarda görünümün dolgusu dikkate alınır. Daha önce de belirtildiği gibi, bu görünümün sorumluluğundadır.
  • Son genişlik ve yükseklik değerlerini oluşturmak için yardımcı yöntem resolveSizeAndState() kullanılır. Bu yardımcı, görünümün gereken boyutunu onMeasure() içine aktarılan değerle karşılaştırarak uygun bir View.MeasureSpec değeri döndürür.
  • onMeasure() dönüş değeri içermez. Bunun yerine, yöntem setMeasuredDimension() yöntemini çağırarak sonuçlarını bildirir. Bu yöntemin çağrılması zorunludur. Bu çağrıyı atlarsanız View sınıfı bir çalışma zamanı istisnası oluşturur.

Çiz

Nesne oluşturma ve ölçüm kodunuzu tanımladıktan sonra onDraw()'yı uygulayabilirsiniz. Her görünüm onDraw() farklı şekilde uygular ancak çoğu görünümün paylaştığı bazı ortak işlemler vardır:

  • drawText() kullanarak metin çizin. Yazı tipini setTypeface() ve metin rengini setColor() çağırarak belirtin.
  • drawRect(), drawOval() ve drawArc() kullanarak temel şekiller çizin. setStyle() işlevini çağırarak şekillerin doldurulup doldurulmayacağını, ana hatlarının çizilip çizilmeyeceğini veya her ikisinin de yapılıp yapılmayacağını değiştirin.
  • Path sınıfını kullanarak daha karmaşık şekiller çizin. Path nesnesine çizgiler ve eğriler ekleyerek bir şekil tanımlayın, ardından drawPath() kullanarak şekli çizin. İlkel şekillerde olduğu gibi, yollar da setStyle() bağlı olarak ana hatları çizilebilir, doldurulabilir veya her ikisi de yapılabilir.
  • LinearGradient nesneleri oluşturarak gradyan dolgularını tanımlayın. Dolu şekillerde LinearGradient kullanmak için setShader() tuşuna basın.
  • drawBitmap() kullanarak bit eşlemler çizin.

Aşağıdaki kodda metin, çizgi ve şekil karışımı çizilir:

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;
}    

Grafik efektleri uygulama

Android 12 (API düzeyi 31), bulanıklaştırma, renk filtreleri ve Android gölgelendirici efektleri gibi yaygın grafik efektlerini RenderEffectnesnelerine ve oluşturma hiyerarşilerine uygulayan View sınıfını ekler. Efektleri, iç ve dış efektlerden oluşan zincir efektler veya harmanlanmış efektler olarak birleştirebilirsiniz. Bu özelliğin desteği, cihazın işlem gücüne göre değişir.

Ayrıca, RenderNode için View temelindeki efektleri View.setRenderEffect(RenderEffect) çağırarak da uygulayabilirsiniz.

RenderEffect nesnesi uygulamak için aşağıdakileri yapın:

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

Görünümü programatik olarak oluşturabilir veya bir XML düzeninden genişletebilir ve View binding ya da findViewById() kullanarak alabilirsiniz.