Tworzenie własnego rysunku

Wypróbuj metodę Compose
Jetpack Compose to zalecany zestaw narzędzi interfejsu na Androida. Dowiedz się, jak pracować z układami w Compose.

Najważniejszy element widoku niestandardowego to jego wygląd. Rysowanie niestandardowe może być proste lub złożone w zależności od potrzeb aplikacji. W tym dokumencie opisujemy niektóre z najczęstszych operacji.

Więcej informacji znajdziesz w omówieniu elementów rysowalnych.

Zastąpienie metody onDraw()

Najważniejszym krokiem w rysowaniu widoku niestandardowego jest zastąpienie metody onDraw(). Parametr przekazywany do funkcji onDraw() to Canvas obiekt, którego widok może używać do rysowania. Klasa Canvas definiuje metody rysowania tekstu, linii, bitmap i wielu innych elementów graficznych. Za pomocą tych metod możesz w onDraw() utworzyć niestandardowy interfejs użytkownika.

Zacznij od utworzenia obiektu Paint. Więcej informacji o Paint znajdziesz w następnej sekcji.

Tworzenie obiektów rysunkowych

Platforma android.graphics dzieli rysowanie na 2 obszary:

  • Co narysować, obsługiwane przez Canvas.
  • Jak rysować – obsługiwane przez Paint.

Na przykład Canvas udostępnia metodę rysowania linii, a Paint – metody określania koloru tej linii. Canvas ma metodę rysowania prostokąta, a Paint określa, czy prostokąt ma być wypełniony kolorem, czy pusty. Canvas określa kształty, które możesz narysować na ekranie, a Paint określa kolor, styl, czcionkę itp. każdego rysowanego kształtu.

Zanim zaczniesz rysować, utwórz co najmniej 1 obiekt Paint. W tym przykładzie pokazujemy, jak to zrobić w metodzie o nazwie init. Ta metoda jest wywoływana z konstruktora w Javie, ale można ją zainicjować w kodzie w języku 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));
   ...
}

Wcześniejsze tworzenie obiektów to ważny element optymalizacji. Widoki są często odświeżane, a wiele obiektów rysowania wymaga kosztownej inicjalizacji. Tworzenie obiektów rysunkowych w metodzie onDraw() znacznie obniża wydajność i może spowolnić interfejs.

Obsługa zdarzeń układu

Aby prawidłowo narysować widok niestandardowy, sprawdź jego rozmiar. Złożone widoki niestandardowe często wymagają wykonania wielu obliczeń układu w zależności od rozmiaru i kształtu obszaru na ekranie. Nigdy nie zakładaj, że widok na ekranie ma określony rozmiar. Nawet jeśli z Twojego widoku korzysta tylko jedna aplikacja, musi ona obsługiwać różne rozmiary ekranu, wiele gęstości ekranu i różne proporcje obrazu w trybie pionowym i poziomym.

Chociaż Viewma wiele metod obsługi pomiarów, większość z nich nie wymaga zastąpienia. Jeśli widok nie wymaga specjalnej kontroli nad rozmiarem, wystarczy zastąpić tylko jedną metodę:onSizeChanged().

onSizeChanged() jest wywoływana, gdy widokowi po raz pierwszy przypisywany jest rozmiar, a także gdy rozmiar widoku zmienia się z jakiegokolwiek powodu. Obliczaj pozycje, wymiary i inne wartości związane z rozmiarem widoku w onSizeChanged(), zamiast obliczać je ponownie przy każdym rysowaniu. W poniższym przykładzie symbol onSizeChanged() oznacza miejsce, w którym widok oblicza prostokąt ograniczający wykresu oraz względne położenie etykiety tekstowej i innych elementów wizualnych.

Gdy widokowi zostanie przypisany rozmiar, menedżer układu zakłada, że rozmiar zawiera dopełnienie widoku. Podczas obliczania rozmiaru widoku uwzględnij wartości dopełnienia. Oto fragment kodu z onSizeChanged(), który pokazuje, jak to zrobić:

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

Jeśli potrzebujesz większej kontroli nad parametrami układu widoku, zaimplementuj onMeasure(). Parametry tej metody to View.MeasureSpecwartości, które informują, jak duży ma być widok podrzędny w stosunku do widoku nadrzędnego, oraz czy ten rozmiar jest maksymalny, czy tylko sugerowany. W celu optymalizacji te wartości są przechowywane jako spakowane liczby całkowite, a do rozpakowywania informacji przechowywanych w każdej liczbie całkowitej używasz metod statycznych klasy View.MeasureSpec.

Oto przykładowa implementacja właściwości onMeasure(). W tej implementacji obszar jest powiększany tak, aby wykres był tak duży jak etykieta:

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

W tym kodzie warto zwrócić uwagę na 3 ważne kwestie:

  • Obliczenia uwzględniają dopełnienie widoku. Jak wspomnieliśmy wcześniej, jest to odpowiedzialność widoku.
  • Metoda pomocnicza resolveSizeAndState() służy do tworzenia końcowych wartości szerokości i wysokości. Ten pomocnik zwraca odpowiednią wartość View.MeasureSpec, porównując potrzebny rozmiar widoku z wartością przekazaną do onMeasure().
  • onMeasure() nie zwraca żadnej wartości. Zamiast tego metoda przekazuje wyniki, wywołując funkcję setMeasuredDimension(). Wywołanie tej metody jest obowiązkowe. Jeśli pominiesz to wywołanie, klasa View zgłosi wyjątek w czasie działania programu.

Narysuj

Po zdefiniowaniu kodu tworzenia i pomiaru obiektu możesz wdrożyć onDraw(). Każdy widok implementuje onDraw() inaczej, ale większość widoków ma pewne wspólne operacje:

  • Rysowanie tekstu za pomocą drawText(). Określ krój pisma, wywołując setTypeface() , a kolor tekstu, wywołując setColor().
  • Rysuj kształty podstawowe za pomocą funkcji drawRect(), drawOval()drawArc(). Zmień, czy kształty mają być wypełnione, obrysowane czy jedno i drugie, wywołując funkcję setStyle().
  • Rysuj bardziej złożone kształty za pomocą klasy Path. Zdefiniuj kształt, dodając linie i krzywe do obiektu Path, a następnie narysuj go za pomocą narzędzia drawPath(). Podobnie jak w przypadku kształtów podstawowych ścieżki można obrysowywać, wypełniać lub robić jedno i drugie, w zależności od wartości parametru setStyle().
  • Zdefiniuj wypełnienia gradientowe, tworząc obiekty LinearGradient. Kliknij setShader()LinearGradient, aby użyć LinearGradient na wypełnionych kształtach.
  • Rysuj mapy bitowe za pomocą funkcji drawBitmap().

Poniższy kod rysuje mieszankę tekstu, linii i kształtów:

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

Stosowanie efektów graficznych

Android 12 (poziom API 31) wprowadza klasę RenderEffect , która stosuje popularne efekty graficzne, takie jak rozmycie, filtry kolorów, efekty cieniowania Androida i inne, do obiektów View i hierarchii renderowania. Możesz łączyć efekty w efekty łańcuchowe, które składają się z efektu wewnętrznego i zewnętrznego, lub efekty mieszane. Obsługa tej funkcji zależy od mocy obliczeniowej urządzenia.

Możesz też zastosować efekty do bazowego RenderNode dla View, wywołując View.setRenderEffect(RenderEffect).

Aby wdrożyć obiekt RenderEffect, wykonaj te czynności:

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

Widok możesz utworzyć programowo lub rozwinąć z układu XML i pobrać za pomocą powiązania widoku lub findViewById().