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 funkcji Utwórz

Najważniejszym elementem widoku niestandardowego jest jego wygląd. Rysunek niestandardowy może być łatwe lub złożone w zależności od potrzeb aplikacji. Ten dokument omawiają najczęściej używane operacje.

Więcej informacji: Omówienie elementów rysowalnych

Zastąp onDraw()

Najważniejszym krokiem podczas rysowania widoku niestandardowego jest zastąpienie onDraw() . Parametr onDraw() to Canvas obiektu, którego widok może używać do rysowania. Zajęcia: Canvas Określa metody rysowania tekstu, linii, map bitowych i wielu innych elementów graficznych elementów podstawowych. Możesz użyć tych metod w aplikacji onDraw(), aby utworzyć niestandardowy interfejs użytkownika (UI).

Zacznij od utworzenia Paint obiekt. W następnej sekcji znajdziesz więcej informacji o Paint.

Tworzenie obiektów rysunkowych

android.graphics rama dzieli rysowanie na 2 obszary:

  • Co narysować, obsługiwane przez: Canvas.
  • Instrukcje rysowania. Obsługa: Paint.

Metoda Canvas pozwala np. narysować linię, a Paint udostępnia metody definiowania koloru linii. Canvas udostępnia metodę rysowania prostokąta, a Paint określa, czy prostokąt ma być wypełniony kolorem, czy pusty. Canvas definiuje kształty, które możesz rysować na ekranie. Paint określa kolor, styl, czcionkę itp. co rysujesz.

Zanim cokolwiek narysowasz, utwórz co najmniej 1 obiekt Paint. Z tego przykładu można to zrobić w metodzie o nazwie init. Ta metoda jest jest wywoływana z konstruktora w Javie, ale może zostać zainicjowana w tekście 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 to ważna optymalizacja. Widoki są jest często odświeżany, a wiele obiektów rysunkowych wymaga kosztownego inicjowania. znaczne tworzenie obiektów rysunkowych w ramach metody onDraw(). zmniejsza wydajność i może spowalniać interfejs użytkownika.

Obsługuj zdarzenia układu

Aby prawidłowo narysować widok niestandardowy, sprawdź jego rozmiar. Złożony niestandardowy często konieczne jest wykonywanie wielu obliczeń układu w zależności od rozmiaru i kształt ich obszaru na ekranie. Nigdy nie należy przyjmować żadnych założeń dotyczących rozmiaru widoku na ekranie. Nawet jeśli tylko jedna aplikacja korzysta z Twojego widoku, musi ona obsługuje różne rozmiary i gęstości ekranu oraz różne aspekty w trybie pionowym i poziomym.

Chociaż View ma wiele metod prowadzenia pomiarów, ale większość z nich zastąpiono. Jeśli widok nie wymaga specjalnej kontroli nad rozmiarem, zastąpić jedną metodę: onSizeChanged()

Funkcja onSizeChanged() jest wywoływana, gdy do widoku zostanie po raz pierwszy przypisany oraz jeszcze raz, jeśli rozmiar widoku zmieni się z jakiegokolwiek powodu. Obliczanie pozycji, wymiarów i innych wartości związanych z rozmiarem widoku w onSizeChanged(), zamiast przeliczać je za każdym razem. W poniższym przykładzie onSizeChanged() jest miejscem, w którym widok oblicza prostokątny ograniczający wykres i względną pozycję etykiety tekstowej i innych elementów wizualnych.

Gdy przypisuje się rozmiarowi, menedżer układu zakłada, że dany rozmiar obejmuje dopełnienie widoku. Obsługuj wartości dopełnienia przy obliczaniu rozmiaru widoku. Oto fragment kodu onSizeChanged(), który pokazuje, jak w tym celu:

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 chcesz mieć większą kontrolę nad parametrami układu widoku, zaimplementuj onMeasure() Parametry tej metody są View.MeasureSpec wartości określające, jak duży ma być element nadrzędny Twojego widoku, czy to sztywna wartość maksymalna, czy tylko sugestia. W ramach optymalizacji są one przechowywane jako spakowane liczby całkowite. Korzystasz z metod statycznych View.MeasureSpec, aby rozpakować informacje przechowywane w poszczególnych liczbach całkowitych.

Oto przykładowa implementacja onMeasure(). W tym stara się powiększyć obszar mapy, aby wykres był już tak duży :

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 należy pamiętać o 3 ważnych kwestiach:

  • W obliczeniach uwzględniane jest dopełnienie widoku. Jak wspomniano za ten widok.
  • Metoda pomocnicza resolveSizeAndState() służy do utworzenia ostatecznej wartości szerokości i wysokości. Ten pomocnik zwraca odpowiednią wartość View.MeasureSpec, porównując potrzebnego rozmiaru widoku do wartości przekazywanej do onMeasure().
  • Parametr onMeasure() nie ma wartości zwróconej. Zamiast tego metoda komunikuje swoje wyniki za pomocą funkcji setMeasuredDimension() Wywołanie tej metody jest obowiązkowe. Jeśli pominiesz to wywołanie, Klasa View zgłasza wyjątek środowiska wykonawczego.

Rysowanie

Po zdefiniowaniu kodu tworzenia obiektu i pomiaru możesz zaimplementować onDraw() W każdym widoku implementacja onDraw() jest implementowana w inny sposób, ale istnieją pewne typowe działania, które są wspólne w większości widoków:

  • Narysuj tekst za pomocą drawText() Określ krój czcionki, wywołując setTypeface() i kolor tekstu po wywołaniu setColor()
  • Narysuj kształty podstawowe za pomocą drawRect(), drawOval(), oraz drawArc() Aby zmienić, czy kształty mają być wypełnione, konturowe czy oba, wywołaj setStyle()
  • Narysuj bardziej złożone kształty za pomocą Path zajęcia. Zdefiniuj kształt, dodając linie i krzywe do Path obiekt, a potem narysuj go za pomocą drawPath() Podobnie jak w przypadku kształtów podstawowych, ścieżki można zaznaczać, wypełniać lub wykonywać jedno i drugie. w zależności od setStyle().
  • Zdefiniuj wypełnienia gradientowe, tworząc LinearGradient obiektów. Zadzwoń do nas setShader() aby używać elementu LinearGradient na wypełnionych kształtach.
  • Rysuj mapy bitowe za pomocą drawBitmap()

Ten kod rysuje tekst, linie i kształty:

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

W Androidzie 12 (poziom API 31) dodano RenderEffect , która stosuje typowe efekty graficzne, takie jak rozmycie, filtry kolorów, Efekty cieniowania Androida i inne funkcje View obiektów i hierarchie renderowania. Efekty można łączyć w efekty łańcuchowe, które składają się wewnętrzny i zewnętrzny efekt czy efekty mieszane. Obsługa tej funkcji różni się w zależności od mocy obliczeniowej urządzenia.

Możesz też zastosować efekty do tła RenderNodeView, dzwonią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ć automatycznie lub zwiększać jego zasięg za pomocą układu XML, pobrać je za pomocą Wyświetl powiązanie lub findViewById()