Tworzenie własnego rysunku

Wypróbuj sposób tworzenia wiadomości
Jetpack Compose to zalecany zestaw narzędzi UI na Androida. Dowiedz się, jak korzystać z układów w sekcji Utwórz

Najważniejszym elementem widoku niestandardowego jest jego wygląd. Własny rysunek może być łatwy lub złożony w zależności od potrzeb aplikacji. W tym dokumencie omawiamy niektóre z najczęstszych działań.

Więcej informacji znajdziesz w artykule Omówienie rysunków.

Zastąp onDraw()

Najważniejszym krokiem podczas rysowania widoku niestandardowego jest zastąpienie metody onDraw(). Parametr onDraw() to obiekt Canvas, którego widok może używać do rysowania. Klasa Canvas definiuje metody rysowania tekstu, linii, map bitowych i wielu innych podstawowych elementów graficznych. Możesz użyć tych metod w onDraw(), aby utworzyć własny interfejs użytkownika.

Zacznij od utworzenia obiektu Paint. W następnej sekcji bardziej szczegółowo opisujemy usługę Paint.

Tworzenie obiektów graficznych

Schemat android.graphics dzieli rysunek na 2 obszary:

  • Co ma narysować. Canvas.
  • Jak rysować. Obsługiwane przez: Paint.

Na przykład Canvas umożliwia rysowanie linii, a Paint udostępnia metody określania koloru tej linii. Canvas ma metodę rysowania prostokąta, a Paint określa, czy wypełnić ten prostokąt kolorem, czy pozostawić go pusty. Canvas określa kształty, które można rysować na ekranie, a Paint określa kolor, styl, czcionkę i inne elementy każdego rysowanego kształtu.

Zanim zaczniesz coś narysować, utwórz co najmniej jeden obiekt Paint. Poniższy przykład pokazuje to w metodzie init. Ta metoda jest wywoływana z konstruktora w Javie, ale można ją zainicjować w tekście 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));
   ...
}

Tworzenie obiektów z wyprzedzeniem jest ważną optymalizacją. Widoki są często odświeżane, a wiele obiektów graficznych wymaga kosztownej inicjalizacji. Tworzenie obiektów do rysowania w metodzie onDraw() znacznie zmniejsza wydajność i może powolać interfejs.

Obsługa zdarzeń układu

Aby prawidłowo narysować widok niestandardowy, sprawdź, jaki ma jego rozmiar. W złożonych widokach niestandardowych trzeba często wykonać kilka obliczeń w zależności od rozmiaru i kształtu wyświetlanego obszaru na ekranie. Nigdy nie oceniaj wielkości obszaru na ekranie. Nawet jeśli z widoku korzysta tylko jedna aplikacja, musi ona obsługiwać ekran o różnych rozmiarach ekranu, różne gęstości ekranu i różne współczynniki proporcji obrazu zarówno w orientacji pionowej, jak i poziomej.

Funkcja View ma wiele metod obsługi pomiarów, ale większość z nich nie musi być zastępowana. Jeśli widok nie wymaga specjalnej kontroli nad rozmiarem, zastąp tylko jedną metodę: onSizeChanged().

Funkcja onSizeChanged() jest wywoływana, gdy do widoku zostanie po raz pierwszy przypisany rozmiar, a także ponownie, gdy rozmiar widoku zmieni się z dowolnego powodu. Możesz obliczać w funkcji onSizeChanged() pozycje, wymiary i wszelkie inne wartości związane z rozmiarem widoku zamiast przeliczać je za każdym razem, gdy rysujesz. W poniższym przykładzie funkcja onSizeChanged() służy do obliczania prostokąta ograniczającego wykresu i względnej pozycji etykiety tekstowej i innych elementów wizualnych.

Gdy do widoku jest przypisany rozmiar, menedżer układu zakłada, że uwzględnia on dopełnienie widoku. Przy obliczaniu rozmiaru widoku uwzględniaj 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 wartości View.MeasureSpec, które informują, jak duży ma być widok nadrzędny Twojego widoku oraz czy jest to trudna wartość maksymalna, czy tylko sugestia. W ramach optymalizacji te wartości są przechowywane jako spakowane liczby całkowite, a informacje przechowywane w poszczególnych liczbach całkowitych są wyodrębniane za pomocą statycznych metod obiektu View.MeasureSpec.

Oto przykład implementacji obiektu onMeasure(). W tej implementacji próbuje on powiększyć obszar na tyle duże, że wykres może 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 kodzie należy pamiętać o 3 ważnych kwestiach:

  • Obliczenia uwzględniają dopełnienie widoku. Jak już wspomnieliśmy, za ten obowiązek odpowiada osoba, która ją widziała.
  • Metoda pomocnicza resolveSizeAndState() jest używana do tworzenia końcowych wartości szerokości i wysokości. Zwraca on odpowiednią wartość View.MeasureSpec przez porównanie wymaganego rozmiaru widoku z wartością przekazaną do funkcji onMeasure().
  • onMeasure() nie ma wartości zwróconej. Zamiast tego metoda przekazuje swoje wyniki, wywołując setMeasuredDimension(). Wywoływanie tej metody jest obowiązkowe. Jeśli pominiesz to wywołanie, klasa View zgłosi wyjątek środowiska wykonawczego.

Rysowanie

Po zdefiniowaniu kodu tworzenia obiektu i pomiaru możesz zaimplementować onDraw(). W każdym z widoków danych onDraw() implementuje się w inny sposób, ale większość z nich używa tych samych działań:

  • Narysuj tekst za pomocą funkcji drawText(). Określ krój czcionki, wywołując setTypeface(), oraz kolor tekstu, wywołując setColor().
  • Narysuj kształty podstawowe za pomocą funkcji drawRect(), drawOval() i drawArc(). Określ, czy kształty mają być wypełnione, z konturem, czy oba te elementy, wywołując 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 kształt za pomocą funkcji drawPath(). Tak jak w przypadku kształtów podstawowych, w zależności od funkcji setStyle() ścieżki mogą być rysowane, wypełnione lub oba te elementy.
  • Zdefiniuj wypełnienia gradientowe, tworząc LinearGradient obiektów. Zadzwoń pod numer setShader(), aby używać elementu LinearGradient na wypełnionych kształtach.
  • Rysuj mapy bitowe, korzystając z drawBitmap().

Ten kod tworzy kombinację 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;
}    

Zastosuj efekty graficzne

Android 12 (poziom interfejsu API 31) dodaje do obiektów View i hierarchii renderowania znane efekty graficzne, takie jak rozmycia, filtry kolorów i efekty cieniowania w Androidzie RenderEffect. Możesz łączyć efekty jako efekty łańcuchowe, które składają się z efektu wewnętrznego i zewnętrznego, lub jako efekty mieszane. Obsługa tej funkcji różni się w zależności od mocy procesora urządzenia.

Możesz też zastosować efekty do bazowego elementu RenderNode w funkcji View, wywołując metodę View.setRenderEffect(RenderEffect).

Aby zaimplementować obiekt RenderEffect, wykonaj te czynności:

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

Widok można utworzyć programowo lub powiększyć z szablonu XML i pobrać go za pomocą opcji View powiązanej lub findViewById().