Najważniejszy element widoku niestandardowego to jego wygląd. Rysowanie niestandardowe może być proste lub złożone w zależności od potrzeb aplikacji. W tym dokumencie opisujemy niektóre z najczęstszych operacji.
Więcej informacji znajdziesz w omówieniu elementów rysowalnych.
Zastąpienie metody onDraw()
Najważniejszym krokiem w rysowaniu widoku niestandardowego jest zastąpienie metody onDraw(). Parametr przekazywany do funkcji onDraw() to Canvas obiekt, którego widok może używać do rysowania. Klasa Canvas definiuje metody rysowania tekstu, linii, bitmap i wielu innych elementów graficznych. Za pomocą tych metod możesz w onDraw() utworzyć niestandardowy interfejs użytkownika.
Zacznij od utworzenia obiektu Paint.
Więcej informacji o Paint znajdziesz w następnej sekcji.
Tworzenie obiektów rysunkowych
Platforma android.graphics dzieli rysowanie na 2 obszary:
- Co narysować, obsługiwane przez
Canvas. - Jak rysować – obsługiwane przez
Paint.
Na przykład Canvas udostępnia metodę rysowania linii, a Paint – metody określania koloru tej linii.
Canvas ma metodę rysowania prostokąta, a Paint
określa, czy prostokąt ma być wypełniony kolorem, czy pusty.
Canvas określa kształty, które możesz narysować na ekranie, a Paint określa kolor, styl, czcionkę itp. każdego rysowanego kształtu.
Zanim zaczniesz rysować, utwórz co najmniej 1 obiekt Paint. W tym przykładzie pokazujemy, jak to zrobić w metodzie o nazwie init. Ta metoda jest wywoływana z konstruktora w Javie, ale można ją zainicjować w kodzie w języku 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)); ... }
Wcześniejsze tworzenie obiektów to ważny element optymalizacji. Widoki są często odświeżane, a wiele obiektów rysowania wymaga kosztownej inicjalizacji.
Tworzenie obiektów rysunkowych w metodzie onDraw() znacznie obniża wydajność i może spowolnić interfejs.
Obsługa zdarzeń układu
Aby prawidłowo narysować widok niestandardowy, sprawdź jego rozmiar. Złożone widoki niestandardowe często wymagają wykonania wielu obliczeń układu w zależności od rozmiaru i kształtu obszaru na ekranie. Nigdy nie zakładaj, że widok na ekranie ma określony rozmiar. Nawet jeśli z Twojego widoku korzysta tylko jedna aplikacja, musi ona obsługiwać różne rozmiary ekranu, wiele gęstości ekranu i różne proporcje obrazu w trybie pionowym i poziomym.
Chociaż Viewma wiele metod obsługi pomiarów, większość z nich nie wymaga zastąpienia. Jeśli widok nie wymaga specjalnej kontroli nad rozmiarem, wystarczy zastąpić tylko jedną metodę:onSizeChanged().
onSizeChanged() jest wywoływana, gdy widokowi po raz pierwszy przypisywany jest rozmiar, a także gdy rozmiar widoku zmienia się z jakiegokolwiek powodu. Obliczaj pozycje, wymiary i inne wartości związane z rozmiarem widoku w onSizeChanged(), zamiast obliczać je ponownie przy każdym rysowaniu.
W poniższym przykładzie symbol onSizeChanged() oznacza miejsce, w którym widok oblicza prostokąt ograniczający wykresu oraz względne położenie etykiety tekstowej i innych elementów wizualnych.
Gdy widokowi zostanie przypisany rozmiar, menedżer układu zakłada, że rozmiar zawiera dopełnienie widoku. Podczas obliczania rozmiaru widoku uwzględnij wartości dopełnienia. Oto fragment kodu z onSizeChanged(), który pokazuje, jak to zrobić:
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 potrzebujesz większej kontroli nad parametrami układu widoku, zaimplementuj onMeasure().
Parametry tej metody to View.MeasureSpecwartości, które informują, jak duży ma być widok podrzędny w stosunku do widoku nadrzędnego, oraz czy ten rozmiar jest maksymalny, czy tylko sugerowany. W celu optymalizacji te wartości są przechowywane jako spakowane liczby całkowite, a do rozpakowywania informacji przechowywanych w każdej liczbie całkowitej używasz metod statycznych klasy View.MeasureSpec.
Oto przykładowa implementacja właściwości onMeasure(). W tej implementacji obszar jest powiększany tak, aby wykres był tak duży jak etykieta:
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 warto zwrócić uwagę na 3 ważne kwestie:
- Obliczenia uwzględniają dopełnienie widoku. Jak wspomnieliśmy wcześniej, jest to odpowiedzialność widoku.
- Metoda pomocnicza
resolveSizeAndState()służy do tworzenia końcowych wartości szerokości i wysokości. Ten pomocnik zwraca odpowiednią wartośćView.MeasureSpec, porównując potrzebny rozmiar widoku z wartością przekazaną doonMeasure(). onMeasure()nie zwraca żadnej wartości. Zamiast tego metoda przekazuje wyniki, wywołując funkcjęsetMeasuredDimension(). Wywołanie tej metody jest obowiązkowe. Jeśli pominiesz to wywołanie, klasaViewzgłosi wyjątek w czasie działania programu.
Narysuj
Po zdefiniowaniu kodu tworzenia i pomiaru obiektu możesz wdrożyć onDraw(). Każdy widok implementuje onDraw() inaczej, ale większość widoków ma pewne wspólne operacje:
- Rysowanie tekstu za pomocą
drawText(). Określ krój pisma, wywołującsetTypeface(), a kolor tekstu, wywołującsetColor(). - Rysuj kształty podstawowe za pomocą funkcji
drawRect(),drawOval()idrawArc(). Zmień, czy kształty mają być wypełnione, obrysowane czy jedno i drugie, wywołując funkcjęsetStyle(). - Rysuj bardziej złożone kształty za pomocą klasy
Path. Zdefiniuj kształt, dodając linie i krzywe do obiektuPath, a następnie narysuj go za pomocą narzędziadrawPath(). Podobnie jak w przypadku kształtów podstawowych ścieżki można obrysowywać, wypełniać lub robić jedno i drugie, w zależności od wartości parametrusetStyle(). -
Zdefiniuj wypełnienia gradientowe, tworząc obiekty
LinearGradient. KliknijsetShader()LinearGradient, aby użyćLinearGradientna wypełnionych kształtach. - Rysuj mapy bitowe za pomocą funkcji
drawBitmap().
Poniższy kod rysuje mieszankę tekstu, linii i kształtów:
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; }
Stosowanie efektów graficznych
Android 12 (poziom API 31) wprowadza klasę
RenderEffect
, która stosuje popularne efekty graficzne, takie jak rozmycie, filtry kolorów, efekty cieniowania Androida i inne, do obiektów
View i hierarchii renderowania. Możesz łączyć efekty w efekty łańcuchowe, które składają się z efektu wewnętrznego i zewnętrznego, lub efekty mieszane. Obsługa tej funkcji zależy od mocy obliczeniowej urządzenia.
Możesz też zastosować efekty do bazowego RenderNode dla View, wywołują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ć programowo lub rozwinąć z układu XML i pobrać za pomocą powiązania widoku lub
findViewById().