Özel çizim oluşturma

Oluşturma yöntemini deneyin
Jetpack Compose, Android için önerilen kullanıcı arayüzü araç setidir. Compose'da düzenlerle çalışma hakkında bilgi edinin.

Ö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 olarak kullanılan bazı işlemler ele alınmaktadır.

Daha fazla bilgi için Çekilebilir öğelere genel bakış konusuna bakın.

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

Ö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 nesnedir. Canvas sınıfı metin, çizgiler, bit eşlemler ve diğer birçok temel grafik öğesini çizme yöntemlerini tanımlar. Özel kullanıcı arayüzünüzü (UI) oluşturmak için onDraw() ürününde bu yöntemleri kullanabilirsiniz.

Bir 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çevesinde, çizim iki alana bölünür:

  • Çizilecek Ne, Canvas tarafından işleniyor.
  • Nasıl çizilir? Paint tarafından ele alınıyor.

Örneğin, Canvas bir çizgi çizme yöntemi, Paint ise bu çizginin rengini tanımlamaya yönelik yöntemler sağlar. Canvas bir dikdörtgen çizme yöntemine sahiptir ve Paint bu dikdörtgenin bir renkle doldurulmasını veya boş bırakılmasını tanımlar. Canvas ekranda çizebileceğiniz şekilleri, Paint ise çizdiğiniz her bir şeklin rengini, stilini, yazı tipini ve benzeri unsurları tanımlar.

Bir şey çizmeden önce bir veya daha fazla Paint nesnesi oluşturun. Aşağıdaki örnek, bunu init adlı bir yöntemle yapar. Bu yöntem Java'daki kurucu tarafından çağrılır, ancak Kotlin'de satır içinde 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 optimizasyondur. Görünümler sık sık yeniden çizilir ve birçok çizim nesnesi pahalı başlatma gerektirir. onDraw() yönteminizde çizim nesneleri oluşturmak performansı önemli ölçüde azaltır ve kullanıcı arayüzünüzü yavaşlatabilir.

Düzen etkinliklerini işleme

Özel görünümünüzü doğru şekilde çizmek için boyutunu öğrenin. Karmaşık özel görünümlerin genellikle ekrandaki alanlarının boyutuna ve şekline bağlı olarak birden çok düzen hesaplaması gerekir. Ekrandaki görünümünüzün boyutu hakkında hiçbir zaman varsayımda bulunmayın. Görünümünüzü yalnızca bir uygulama kullansa 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 yapmak için birçok yönteme sahip olsa da çoğunun geçersiz kılınmasına gerek yoktur. Görünümünüz, boyutu üzerinde özel bir kontrole ihtiyaç duymuyorsa yalnızca tek bir yöntemi geçersiz kılın: onSizeChanged().

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

Görünümünüze bir boyut atandığında düzen yöneticisi, boyutun görünümün dolgusunu içerdiğini varsayar. Görünümünüzün boyutunu hesaplarken dolgu değerlerini dikkate alın. Bunun nasıl yapılacağını gösteren onSizeChanged() snippet'ini burada 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ı kontrole ihtiyacınız varsa onMeasure() uygulayın. Bu yöntemin parametreleri, View.MeasureSpec görünümünüzün üst tarafındaki görünümü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ğerlerdir. Optimizasyon olarak, bu değerler paketlenmiş tamsayılar olarak depolanır ve her bir tamsayıda depolanan bilgileri açmak için View.MeasureSpec statik yöntemlerini kullanırsınız.

Aşağıda, onMeasure() işlevinin bir örneği verilmiştir. Bu uygulamada, grafik alanını kendi etiketi kadar büyük hale getirecek kadar büyük hale getirmeye çalışı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:

  • Hesaplamalar, görünümün dolgusunu dikkate alır. Daha önce de belirtildiği gibi bu, görünümün sorumluluğudur.
  • Nihai genişlik ve yükseklik değerlerini oluşturmak için resolveSizeAndState() yardımcı yöntemi kullanılır. Bu yardımcı, görünümün gerekli boyutunu onMeasure() işlevine geçirilen değerle karşılaştırarak uygun bir View.MeasureSpec değeri döndürür.
  • onMeasure(), döndürülen bir değer içermiyor. Bunun yerine, yöntem setMeasuredDimension() yöntemini çağırarak sonuçlarını bildirir. Bu yöntemi çağırmak zorunludur. Bu çağrıyı atlarsanız View sınıfı bir çalışma zamanı istisnası atar.

Çiz

Nesne oluşturma ve ölçüm kodunuzu tanımladıktan sonra onDraw() kodunu uygulayabilirsiniz. onDraw() her görünümde farklı şekilde uygulanır, ancak çoğu görüntülemenin paylaştığı bazı ortak işlemler vardır:

  • drawText() kullanarak metin çizin. Yazı tipini, setTypeface() yöntemini çağırarak ve metin rengini setColor() yöntemini çağırarak belirtin.
  • drawRect(), drawOval() ve drawArc() kullanarak basit şekiller çizin. setStyle() yöntemini çağırarak şekillerin dolu, dış çizgili veya her ikisini de değiştirebilirsiniz.
  • 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, setStyle() değerine bağlı olarak ana hatlarıyla belirtilebilir, doldurulabilir veya her ikisi birden olabilir.
  • LinearGradient nesneleri oluşturarak gradyan dolgularını tanımlayın. Dolgulu şekillerde LinearGradient kullanmak için setShader() numaralı telefonu arayın.
  • drawBitmap() kullanarak bit eşlemler çizin.

Aşağıdaki kod metin, çizgi ve şekillerin bir karışımını çizer:

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 uygulayın

Android 12'de (API düzeyi 31), View nesnelerine ve oluşturma hiyerarşilerine bulanıklaştırma, renk filtreleri ve Android gölgelendirici efektleri gibi yaygın grafik efektlerinin uygulandığı RenderEffect sınıfını ekler. Efektleri, iç ve dış efektlerden ya da karışık efektlerden oluşan zincir efektleri olarak birleştirebilirsiniz. Bu özellik için destek, cihazın işlem gücüne göre değişir.

Ayrıca, View.setRenderEffect(RenderEffect) yöntemini çağırarak View için temel RenderNode efektine de efekt uygulayabilirsiniz.

Bir RenderEffect nesnesini 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 XML düzeninden şişirip Görünüm bağlama ya da findViewById() kullanarak alabilirsiniz.