Benutzerdefinierte Zeichnung erstellen

Funktion „Schreiben“ ausprobieren
Jetpack Compose ist das empfohlene UI-Toolkit für Android. Hier erfahren Sie, wie Sie in der Funktion „Compose“ mit Layouts arbeiten.

Der wichtigste Teil einer benutzerdefinierten Ansicht ist ihr Aussehen. Benutzerdefinierte Zeichnungen können je nach den Anforderungen Ihrer Anwendung einfach oder komplex sein. In diesem Dokument werden einige der gängigsten Vorgänge behandelt.

Weitere Informationen findest du unter Drawables – Übersicht.

onDraw() überschreiben

Der wichtigste Schritt beim Erstellen einer benutzerdefinierten Ansicht besteht darin, die Methode onDraw() zu überschreiben. Der Parameter für onDraw() ist ein Canvas-Objekt, das in der Ansicht zum Zeichnen verwendet werden kann. Die Canvas-Klasse definiert Methoden zum Zeichnen von Text, Linien, Bitmaps und vielen anderen Grafikprimitiven. Sie können diese Methoden in onDraw() verwenden, um Ihre benutzerdefinierte Benutzeroberfläche (UI) zu erstellen.

Erstellen Sie zuerst ein Paint-Objekt. Im nächsten Abschnitt wird Paint ausführlicher behandelt.

Zeichnungsobjekte erstellen

Das android.graphics-Framework teilt die Zeichnung in zwei Bereiche auf:

  • Was gezeichnet werden soll, wird von Canvas verwaltet.
  • Eine Anleitung für das Zeichnen von Paint.

Beispielsweise bietet Canvas eine Methode zum Zeichnen einer Linie und Paint Methoden zum Definieren der Linienfarbe. Canvas hat eine Methode zum Zeichnen eines Rechtecks und Paint definiert, ob dieses Rechteck mit einer Farbe gefüllt oder leer bleibt. Canvas definiert Formen, die Sie auf dem Bildschirm zeichnen können, und Paint definiert die Farbe, den Stil, die Schriftart usw. jeder gezeichneten Form.

Bevor Sie etwas zeichnen, erstellen Sie ein oder mehrere Paint-Objekte. Im folgenden Beispiel wird dies in einer Methode namens init durchgeführt. Diese Methode wird vom Konstruktor aus Java aufgerufen, kann aber Inline in Kotlin initialisiert werden.

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

Das vorherige Erstellen von Objekten ist eine wichtige Optimierung. Ansichten werden häufig neu gezeichnet und viele Zeichenobjekte erfordern eine teure Initialisierung. Wenn Sie Zeichenobjekte innerhalb der Methode onDraw() erstellen, reduziert dies die Leistung erheblich und kann Ihre UI verlangsamen.

Layoutereignisse verarbeiten

Um Ihre benutzerdefinierte Ansicht richtig zu zeichnen, müssen Sie ihre Größe ermitteln. Bei komplexen benutzerdefinierten Ansichten müssen häufig mehrere Layoutberechnungen durchgeführt werden, je nach Größe und Form ihres Bildschirmbereichs. Treffen Sie niemals Annahmen über die Größe der Ansicht auf dem Bildschirm. Auch wenn nur eine App Ihre Ansicht verwendet, muss diese App unterschiedliche Bildschirmgrößen, verschiedene Bildschirmdichten und verschiedene Seitenverhältnisse im Hoch- und Querformat verarbeiten können.

Für View gibt es zwar viele Methoden zur Verarbeitung von Messungen, die meisten müssen jedoch nicht überschrieben werden. Wenn für die Ansicht keine besondere Steuerung der Größe erforderlich ist, überschreiben Sie nur eine Methode: onSizeChanged().

onSizeChanged() wird aufgerufen, wenn der Ansicht zum ersten Mal eine Größe zugewiesen wird, und noch einmal, wenn sich die Größe der Ansicht aus irgendeinem Grund ändert. Sie können Positionen, Dimensionen und andere Werte, die sich auf die Größe der Ansicht beziehen, in onSizeChanged() berechnen, anstatt sie jedes Mal neu zu berechnen. Im folgenden Beispiel berechnet die Ansicht das Markierungsrechteck des Diagramms und die relative Position des Textlabels und anderer visueller Elemente in onSizeChanged().

Wenn Ihrer Ansicht eine Größe zugewiesen wird, geht der Layout-Manager davon aus, dass die Größe den Abstand der Ansicht beinhaltet. die Padding-Werte verarbeiten, wenn Sie die Größe Ihrer Ansicht berechnen. Hier ist ein Snippet von onSizeChanged(), das die Vorgehensweise veranschaulicht:

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

Wenn Sie die Layoutparameter der Ansicht genauer steuern möchten, implementieren Sie onMeasure(). Die Parameter dieser Methode sind View.MeasureSpec-Werte, die angeben, wie groß die Ansicht für das übergeordnete Element der Ansicht sein soll und ob diese Größe ein festes Maximum oder nur ein Vorschlag ist. Zur Optimierung werden diese Werte als gepackte Ganzzahlen gespeichert. Sie verwenden die statischen Methoden von View.MeasureSpec, um die in jeder Ganzzahl gespeicherten Informationen zu entpacken.

Hier ist eine Beispielimplementierung von onMeasure(). Bei dieser Implementierung wird versucht, den Bereich groß genug zu machen, um das Diagramm so groß wie sein Label zu machen:

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

Bei diesem Code sind drei wichtige Punkte zu beachten:

  • Bei den Berechnungen wird der Innenrand der Ansicht berücksichtigt. Wie bereits erwähnt, liegt dies in der Verantwortung der Ansicht.
  • Die endgültigen Werte für Breite und Höhe werden mit der Hilfsmethode resolveSizeAndState() erstellt. Dieses Hilfsprogramm gibt einen geeigneten View.MeasureSpec-Wert zurück, indem es die erforderliche Größe der Ansicht mit dem an onMeasure() übergebenen Wert vergleicht.
  • onMeasure() hat keinen Rückgabewert. Stattdessen kommuniziert die Methode die Ergebnisse durch Aufrufen von setMeasuredDimension(). Das Aufrufen dieser Methode ist erforderlich. Wenn Sie diesen Aufruf weglassen, löst die Klasse View eine Laufzeitausnahme aus.

Zeichnen

Nachdem Sie den Code zum Erstellen und Messen von Objekten definiert haben, können Sie onDraw() implementieren. onDraw() wird von jeder Ansicht unterschiedlich implementiert. Es gibt jedoch einige Vorgänge, die von den meisten Ansichten gemeinsam genutzt werden:

  • Zeichnen Sie Text mit drawText(). Geben Sie das Schriftbild durch Aufrufen von setTypeface() und die Textfarbe durch Aufrufen von setColor() an.
  • Sie können einfache Formen mit drawRect(), drawOval() und drawArc() zeichnen. Durch Aufrufen von setStyle() können Sie festlegen, ob die Formen ausgefüllt und/oder umrissen werden sollen.
  • Komplexere Formen können mit der Klasse Path gezeichnet werden. Definieren Sie eine Form, indem Sie Linien und Kurven in ein Path-Objekt einfügen. Zeichnen Sie dann die Form mit drawPath(). Wie bei einfachen Formen können Pfade je nach setStyle() umrissen, ausgefüllt oder beides sein.
  • Definieren Sie Farbverlaufsfüllungen, indem Sie LinearGradient-Objekte erstellen. Rufen Sie setShader() auf, um LinearGradient für ausgefüllte Formen zu verwenden.
  • Zeichnen Sie Bitmaps mit drawBitmap().

Mit dem folgenden Code wird eine Mischung aus Text, Linien und Formen gezeichnet:

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

Grafikeffekte anwenden

Android 12 (API-Ebene 31) fügt die RenderEffect-Klasse hinzu, die gängige Grafikeffekte wie Weichzeichner, Farbfilter, Android-Shader-Effekte und mehr auf View-Objekte und Rendering-Hierarchien anwendet. Sie können Effekte als Ketteneffekte, die aus einem inneren und äußeren Effekt bestehen, oder aus Mischeffekten kombinieren. Die Unterstützung für diese Funktion hängt von der Prozessorleistung des Geräts ab.

Sie können auch Effekte auf das zugrunde liegende RenderNode für eine View anwenden, indem Sie View.setRenderEffect(RenderEffect) aufrufen.

So implementierst du ein RenderEffect-Objekt:

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

Sie können die Ansicht programmatisch erstellen oder aus einem XML-Layout infließen und mit View-Bindung oder findViewById() abrufen.