Benutzerdefinierte Zeichnung erstellen

Compose ausprobieren
Jetpack Compose ist das empfohlene UI-Toolkit für Android. Informationen zur Arbeit mit Layouts in Compose

Das Wichtigste an einer benutzerdefinierten Ansicht ist ihr Erscheinungsbild. Benutzerdefiniertes Zeichnen kann je nach den Anforderungen Ihrer Anwendung einfach oder komplex sein. In diesem Dokument werden einige der häufigsten Vorgänge beschrieben.

Weitere Informationen finden Sie unter Drawables.

onDraw() überschreiben

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

Erstellen Sie zuerst ein Paint-Objekt. Im nächsten Abschnitt wird Paint genauer erläutert.

Zeichnungsobjekte erstellen

Das android.graphics-Framework unterteilt das Zeichnen in zwei Bereiche:

  • Was gezeichnet werden soll, wird von Canvas übernommen.
  • Wie zeichnet man, bearbeitet von Paint.

Canvas bietet beispielsweise eine Methode zum Zeichnen einer Linie und Paint Methoden zum Definieren der Farbe dieser Linie. Canvas hat eine Methode zum Zeichnen eines Rechtecks und Paint definiert, ob das Rechteck mit einer Farbe gefüllt oder leer gelassen werden soll. 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, müssen Sie ein oder mehrere Paint-Objekte erstellen. Im folgenden Beispiel wird dies in einer Methode mit dem Namen init veranschaulicht. Diese Methode wird aus dem Konstruktor in Java aufgerufen, kann aber in Kotlin inline 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 Erstellen von Objekten im Voraus ist eine wichtige Optimierung. Ansichten werden häufig neu gezeichnet und viele Zeichenobjekte erfordern eine aufwendige Initialisierung. Wenn Sie Zeichenobjekte in der Methode onDraw() erstellen, wird die Leistung erheblich beeinträchtigt und die Benutzeroberfläche kann träge werden.

Layout-Ereignisse verarbeiten

Damit Ihre benutzerdefinierte Ansicht richtig gezeichnet wird, müssen Sie ihre Größe ermitteln. Bei komplexen benutzerdefinierten Ansichten sind oft mehrere Layoutberechnungen erforderlich, je nach Größe und Form des Bereichs auf dem Bildschirm. Gehen Sie niemals davon aus, wie groß Ihr Bild auf dem Bildschirm ist. Auch wenn nur eine App Ihre Ansicht verwendet, muss diese App mit verschiedenen Bildschirmgrößen, mehreren Bildschirmdichten und verschiedenen Seitenverhältnissen im Hoch- und Querformat umgehen können.

View bietet zwar viele Methoden für die Analyse, die meisten müssen jedoch nicht überschrieben werden. Wenn für Ihre Ansicht keine spezielle Steuerung der Größe erforderlich ist, müssen Sie nur eine Methode überschreiben: onSizeChanged().

onSizeChanged() wird aufgerufen, wenn Ihrer Ansicht zum ersten Mal eine Größe zugewiesen wird, und noch einmal, wenn sich die Größe Ihrer Ansicht aus irgendeinem Grund ändert. Berechnen Sie Positionen, Dimensionen und alle anderen Werte, die sich auf die Größe der Ansicht beziehen, in onSizeChanged(), anstatt sie bei jedem Rendern neu zu berechnen. Im folgenden Beispiel wird mit onSizeChanged() das umschließende Rechteck des Diagramms und die relative Position des Textlabels und anderer visueller Elemente berechnet.

Wenn Ihrer Ansicht eine Größe zugewiesen wird, geht der Layoutmanager davon aus, dass die Größe das Padding der Ansicht enthält. Berücksichtigen Sie die Padding-Werte, wenn Sie die Größe der Ansicht berechnen. Hier ist ein Snippet aus onSizeChanged(), das zeigt, wie das geht:

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 mehr Kontrolle über die Layoutparameter Ihrer Ansicht benötigen, implementieren Sie onMeasure(). Die Parameter dieser Methode sind View.MeasureSpec-Werte, die angeben, wie groß die übergeordnete Ansicht für Ihre Ansicht sein soll und ob diese Größe ein hartes Maximum oder nur ein Vorschlag ist. Zur Optimierung werden diese Werte als gepackte Ganzzahlen gespeichert. Mit den statischen Methoden von View.MeasureSpec können Sie die in jeder Ganzzahl gespeicherten Informationen entpacken.

Hier ist ein Beispiel für die Implementierung von onMeasure(). Bei dieser Implementierung wird versucht, die Fläche so groß zu machen, dass das Diagramm so groß wie das Label ist:

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

In diesem Code gibt es drei wichtige Dinge zu beachten:

  • Bei den Berechnungen wird der Innenabstand der Ansicht berücksichtigt. Wie bereits erwähnt, liegt dies in der Verantwortung der Ansicht.
  • Die Hilfsmethode resolveSizeAndState() wird verwendet, um die endgültigen Werte für Breite und Höhe zu erstellen. Mit diesem Helfer wird ein geeigneter View.MeasureSpec-Wert zurückgegeben, indem die erforderliche Größe der Ansicht mit dem in onMeasure() übergebenen Wert verglichen wird.
  • onMeasure() hat keinen Rückgabewert. Stattdessen werden die Ergebnisse der Methode durch Aufrufen von setMeasuredDimension() übermittelt. Der Aufruf dieser Methode ist obligatorisch. 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 in jeder Ansicht anders implementiert. Es gibt jedoch einige gemeinsame Vorgänge, die in den meisten Ansichten verfügbar sind:

  • Text mit drawText() zeichnen. Geben Sie die Schriftart mit setTypeface() und die Textfarbe mit setColor() an.
  • Zeichnen Sie einfache Formen mit drawRect(), drawOval() und drawArc(). Mit setStyle() können Sie festlegen, ob die Formen gefüllt, umrandet oder beides sein sollen.
  • Mit der Klasse Path können Sie komplexere Formen zeichnen. Definieren Sie eine Form, indem Sie einem Path-Objekt Linien und Kurven hinzufügen, und zeichnen Sie die Form dann mit drawPath(). Wie bei einfachen Formen können Pfade je nach setStyle() umrandet, gefüllt oder beides sein.
  • Definieren Sie Verlaufsfüllungen, indem Sie LinearGradient-Objekte erstellen. Rufen Sie setShader() auf, setShader() um LinearGradient auf gefüllte Formen anzuwenden.
  • Bitmaps mit drawBitmap() zeichnen.

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

In Android 12 (API-Level 31) wird die Klasse RenderEffect eingeführt, mit der gängige Grafikeffekte wie Unschärfe, Farbfilter und Android-Shader-Effekte auf View-Objekte und Rendering-Hierarchien angewendet werden können. Sie können Effekte als Ketteneffekte kombinieren, die aus einem inneren und einem äußeren Effekt bestehen, oder als gemischte Effekte. Die Unterstützung dieser Funktion hängt von der Rechenleistung des Geräts ab.

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

So implementieren Sie ein RenderEffect-Objekt:

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

Sie können die Ansicht programmgesteuert erstellen oder aus einem XML-Layout ableiten und mit Ansichtsbinding oder findViewById() abrufen.