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 geeigneterView.MeasureSpec-Wert zurückgegeben, indem die erforderliche Größe der Ansicht mit dem inonMeasure()übergebenen Wert verglichen wird. onMeasure()hat keinen Rückgabewert. Stattdessen werden die Ergebnisse der Methode durch Aufrufen vonsetMeasuredDimension()übermittelt. Der Aufruf dieser Methode ist obligatorisch. Wenn Sie diesen Aufruf weglassen, löst die KlasseVieweine 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 mitsetTypeface()und die Textfarbe mitsetColor()an. - Zeichnen Sie einfache Formen mit
drawRect(),drawOval()unddrawArc(). MitsetStyle()können Sie festlegen, ob die Formen gefüllt, umrandet oder beides sein sollen. - Mit der Klasse
Pathkönnen Sie komplexere Formen zeichnen. Definieren Sie eine Form, indem Sie einemPath-Objekt Linien und Kurven hinzufügen, und zeichnen Sie die Form dann mitdrawPath(). Wie bei einfachen Formen können Pfade je nachsetStyle()umrandet, gefüllt oder beides sein. -
Definieren Sie Verlaufsfüllungen, indem Sie
LinearGradient-Objekte erstellen. Rufen SiesetShader()auf,setShader()umLinearGradientauf 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.