Benutzerdefinierte Zeichnung erstellen

Schreiben Sie jetzt
Jetpack Compose ist das empfohlene UI-Toolkit für Android. Hier erfahren Sie, wie Sie in „Compose“ mit Layouts arbeiten.
<ph type="x-smartling-placeholder"></ph> Canvas in „Schreiben“ →

Das Wichtigste bei einer benutzerdefinierten Ansicht ist ihre Darstellung. Benutzerdefinierte Zeichnung je nach Anwendungsanforderungen einfach oder komplex sein können. Dieses Dokument deckt einige der gängigsten Operationen ab.

Weitere Informationen finden Sie unter Drawables – Übersicht

onDraw() überschreiben

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

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

Zeichenobjekte erstellen

Die android.graphics teilt das Zeichnen in zwei Bereiche auf:

  • Was gezeichnet werden soll, verarbeitet von Canvas.
  • Wie gezeichnet wird, verarbeitet von Paint.

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

Bevor Sie etwas zeichnen, erstellen Sie ein oder mehrere Paint-Objekte. Die Im folgenden Beispiel erfolgt dies in einer Methode namens init. Diese Methode ist vom Konstruktor aus Java aufgerufen, kann aber inline in 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));
   ...
}

Das Erstellen von Objekten im Voraus ist eine wichtige Optimierung. Aufrufe sind und viele Zeichenobjekte eine aufwendige Initialisierung erfordern. Erhebliche Erstellung von Zeichenobjekten in der onDraw()-Methode reduziert die Leistung und kann dazu führen, dass Ihre Benutzeroberfläche langsam ist.

Layoutereignisse verarbeiten

Um Ihre benutzerdefinierte Ansicht richtig zu zeichnen, müssen Sie ihre Größe ermitteln. Komplex (benutzerdefiniert) Für Ansichten müssen häufig mehrere Layoutberechnungen abhängig von der Größe durchgeführt werden. und die Form ihres Bildschirmbereichs. Treffen Sie niemals Annahmen über die Größe Ihrer auf dem Bildschirm. Auch wenn nur eine App Ihre Ansicht verwendet, muss diese verschiedene Bildschirmgrößen, unterschiedliche Bildschirmdichten und Seitenverhältnisse im Hoch- und im Querformat verwenden.

Obwohl View gibt es viele Messmethoden, von denen die meisten nicht überschrieben. Wenn für Ihre Ansicht keine spezielle Kontrolle über die Größe erforderlich ist, eine Methode überschreiben: onSizeChanged()

onSizeChanged() wird aufgerufen, wenn Ihrer Ansicht zum ersten Mal ein und auch dann, wenn sich die Größe der Ansicht aus irgendeinem Grund ändert. Berechnen und andere Werte in Bezug auf die Größe der Ansicht in onSizeChanged(), anstatt sie bei jedem Zeichnen neu zu berechnen. Im folgenden Beispiel ist die Ansicht onSizeChanged() berechnet das Begrenzungsrahmen des Diagramms und die relative Position des Textlabel und andere visuelle Elemente.

Wenn Ihrer Ansicht eine Größe zugewiesen wird, geht der Layout-Manager davon aus, enthält den Abstand der Ansicht. Padding-Werte verarbeiten, wenn Sie Ihre Ansicht anpassen. Hier ist ein Snippet von onSizeChanged(), das zeigt, Gehen Sie dazu so vor:

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 eine genauere Kontrolle über die Layoutparameter Ihrer Ansicht benötigen, implementieren Sie onMeasure() Die Parameter dieser Methode sind View.MeasureSpec die angeben, wie groß Ihr Aufruf sein soll, ob es sich dabei um ein festes Maximum oder nur um einen Vorschlag handelt. Zur Optimierung Diese Werte werden als gepackte Ganzzahlen gespeichert und Sie verwenden die statischen Methoden der View.MeasureSpec, um die in jeder Ganzzahl gespeicherten Informationen zu entpacken.

Hier siehst du eine Beispielimplementierung von onMeasure(). In dieser wird versucht, den Bereich groß genug zu machen, damit das Diagramm als Label verwenden:

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 Dinge zu beachten:

  • Bei den Berechnungen wird auch der Abstand der Ansicht berücksichtigt. Wie bereits erwähnt ist dies in der Verantwortung der Ansicht.
  • Die Hilfsmethode resolveSizeAndState() wird verwendet, um die endgültigen Werte für Breite und Höhe zu erstellen. Dieses Hilfsprogramm gibt einen geeigneten View.MeasureSpec-Wert, indem Sie die der Ansicht auf den an onMeasure() übergebenen Wert.
  • onMeasure() hat keinen Rückgabewert. Stattdessen wird die Methode kommuniziert seine Ergebnisse mit einem Aufruf setMeasuredDimension() Das Aufrufen dieser Methode ist obligatorisch. Wenn Sie diesen Aufruf weglassen, Die Klasse View gibt eine Laufzeitausnahme aus.

Zeichnen

Nachdem Sie den Code zur Objekterstellung und Messung definiert haben, können Sie onDraw() onDraw() wird in jeder Ansicht unterschiedlich implementiert, Die meisten Ansichten weisen jedoch einige gängige Vorgänge auf:

  • Text zeichnen mit drawText() Geben Sie das Schriftbild an, indem Sie setTypeface() und die Textfarbe durch Aufrufen von setColor().
  • Primitive Formen zeichnen mit drawRect(), drawOval(), und drawArc() Sie können festlegen, ob die Formen gefüllt, umrissen oder beides sind. Rufen Sie dazu setStyle()
  • Zeichnen Sie komplexere Formen mithilfe der Path . Form definieren, indem Sie einem Path Linien und Kurven hinzufügen und zeichnen Sie die Form drawPath(). Wie bei primitiven Formen können Pfade umrissen, ausgefüllt oder beides sein. abhängig von setStyle().
  • Farbverlaufsfüllungen durch Erstellen von LinearGradient definieren Objekte. Anruf setShader() um LinearGradient für ausgefüllte Formen zu verwenden.
  • Bitmaps zeichnen 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-Level 31) fügt die RenderEffect , in der gängige Grafikeffekte wie Weichzeichner, Farbfilter, Android-Shader-Effekte und mehr, View-Objekte und und Rendering von Hierarchien. Du kannst Effekte als Ketteneffekte kombinieren. inneren und äußeren Effekten oder kombinierte Effekte. Unterstützung für diese Funktion variiert je nach Prozessorleistung des Geräts.

Sie können auch Effekte auf die zugrunde liegende RenderNode für View durch Aufrufen von View.setRenderEffect(RenderEffect)

So implementierst du ein RenderEffect-Objekt:

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

Sie können die Ansicht programmatisch erstellen oder über ein XML-Layout erweitern und rufen Sie ihn mit View Binding (Bindung ansehen) ab oder findViewById()