Najważniejszym elementem widoku niestandardowego jest jego wygląd. Własny rysunek może być łatwy lub złożony w zależności od potrzeb aplikacji. W tym dokumencie omawiamy niektóre z najczęstszych działań.
Więcej informacji znajdziesz w artykule Omówienie rysunków.
Zastąp onDraw()
Najważniejszym krokiem podczas rysowania widoku niestandardowego jest zastąpienie metody onDraw()
. Parametr onDraw()
to obiekt Canvas
, którego widok może używać do rysowania. Klasa Canvas
definiuje metody rysowania tekstu, linii, map bitowych i wielu innych podstawowych elementów graficznych. Możesz użyć tych metod w onDraw()
, aby utworzyć własny interfejs użytkownika.
Zacznij od utworzenia obiektu Paint
.
W następnej sekcji bardziej szczegółowo opisujemy usługę Paint
.
Tworzenie obiektów graficznych
Schemat android.graphics
dzieli rysunek na 2 obszary:
- Co ma narysować.
Canvas
. - Jak rysować. Obsługiwane przez:
Paint
.
Na przykład Canvas
umożliwia rysowanie linii, a Paint
udostępnia metody określania koloru tej linii.
Canvas
ma metodę rysowania prostokąta, a Paint
określa, czy wypełnić ten prostokąt kolorem, czy pozostawić go pusty.
Canvas
określa kształty, które można rysować na ekranie, a Paint
określa kolor, styl, czcionkę i inne elementy każdego rysowanego kształtu.
Zanim zaczniesz coś narysować, utwórz co najmniej jeden obiekt Paint
. Poniższy przykład pokazuje to w metodzie init
. Ta metoda jest wywoływana z konstruktora w Javie, ale można ją zainicjować w tekście 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)); ... }
Tworzenie obiektów z wyprzedzeniem jest ważną optymalizacją. Widoki są często odświeżane, a wiele obiektów graficznych wymaga kosztownej inicjalizacji.
Tworzenie obiektów do rysowania w metodzie onDraw()
znacznie zmniejsza wydajność i może powolać interfejs.
Obsługa zdarzeń układu
Aby prawidłowo narysować widok niestandardowy, sprawdź, jaki ma jego rozmiar. W złożonych widokach niestandardowych trzeba często wykonać kilka obliczeń w zależności od rozmiaru i kształtu wyświetlanego obszaru na ekranie. Nigdy nie oceniaj wielkości obszaru na ekranie. Nawet jeśli z widoku korzysta tylko jedna aplikacja, musi ona obsługiwać ekran o różnych rozmiarach ekranu, różne gęstości ekranu i różne współczynniki proporcji obrazu zarówno w orientacji pionowej, jak i poziomej.
Funkcja View
ma wiele metod obsługi pomiarów, ale większość z nich nie musi być zastępowana. Jeśli widok nie wymaga specjalnej kontroli nad rozmiarem, zastąp tylko jedną metodę: onSizeChanged()
.
Funkcja onSizeChanged()
jest wywoływana, gdy do widoku zostanie po raz pierwszy przypisany
rozmiar, a także ponownie, gdy rozmiar widoku zmieni się z dowolnego powodu. Możesz obliczać w funkcji onSizeChanged()
pozycje, wymiary i wszelkie inne wartości związane z rozmiarem widoku zamiast przeliczać je za każdym razem, gdy rysujesz.
W poniższym przykładzie funkcja onSizeChanged()
służy do obliczania prostokąta ograniczającego wykresu i względnej pozycji etykiety tekstowej i innych elementów wizualnych.
Gdy do widoku jest przypisany rozmiar, menedżer układu zakłada, że uwzględnia on dopełnienie widoku. Przy obliczaniu rozmiaru widoku uwzględniaj 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 wartości View.MeasureSpec
, które informują, jak duży ma być widok nadrzędny Twojego widoku oraz czy jest to trudna wartość maksymalna, czy tylko sugestia. W ramach optymalizacji te wartości są przechowywane jako spakowane liczby całkowite, a informacje przechowywane w poszczególnych liczbach całkowitych są wyodrębniane za pomocą statycznych metod obiektu View.MeasureSpec
.
Oto przykład implementacji obiektu onMeasure()
. W tej implementacji próbuje on powiększyć obszar na tyle duże, że wykres może 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 kodzie należy pamiętać o 3 ważnych kwestiach:
- Obliczenia uwzględniają dopełnienie widoku. Jak już wspomnieliśmy, za ten obowiązek odpowiada osoba, która ją widziała.
- Metoda pomocnicza
resolveSizeAndState()
jest używana do tworzenia końcowych wartości szerokości i wysokości. Zwraca on odpowiednią wartośćView.MeasureSpec
przez porównanie wymaganego rozmiaru widoku z wartością przekazaną do funkcjionMeasure()
. onMeasure()
nie ma wartości zwróconej. Zamiast tego metoda przekazuje swoje wyniki, wywołującsetMeasuredDimension()
. Wywoływanie tej metody jest obowiązkowe. Jeśli pominiesz to wywołanie, klasaView
zgłosi wyjątek środowiska wykonawczego.
Rysowanie
Po zdefiniowaniu kodu tworzenia obiektu i pomiaru możesz zaimplementować onDraw()
. W każdym z widoków danych onDraw()
implementuje się w inny sposób, ale większość z nich używa tych samych działań:
- Narysuj tekst za pomocą funkcji
drawText()
. Określ krój czcionki, wywołującsetTypeface()
, oraz kolor tekstu, wywołującsetColor()
. - Narysuj kształty podstawowe za pomocą funkcji
drawRect()
,drawOval()
idrawArc()
. Określ, czy kształty mają być wypełnione, z konturem, czy oba te elementy, wywołującsetStyle()
. - 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 kształt za pomocą funkcjidrawPath()
. Tak jak w przypadku kształtów podstawowych, w zależności od funkcjisetStyle()
ścieżki mogą być rysowane, wypełnione lub oba te elementy. -
Zdefiniuj wypełnienia gradientowe, tworząc
LinearGradient
obiektów. Zadzwoń pod numersetShader()
, aby używać elementuLinearGradient
na wypełnionych kształtach. - Rysuj mapy bitowe, korzystając z
drawBitmap()
.
Ten kod tworzy kombinację 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; }
Zastosuj efekty graficzne
Android 12 (poziom interfejsu API 31) dodaje do obiektów View
i hierarchii renderowania znane efekty graficzne, takie jak rozmycia, filtry kolorów i efekty cieniowania w Androidzie RenderEffect
. Możesz łączyć efekty jako efekty łańcuchowe, które składają się
z efektu wewnętrznego i zewnętrznego, lub jako efekty mieszane. Obsługa tej funkcji różni się w zależności od mocy procesora urządzenia.
Możesz też zastosować efekty do bazowego elementu RenderNode
w funkcji View
, wywołując metodę View.setRenderEffect(RenderEffect)
.
Aby zaimplementować obiekt RenderEffect
, wykonaj te czynności:
view.setRenderEffect(RenderEffect.createBlurEffect(radiusX, radiusY, SHADER_TILE_MODE))
Widok można utworzyć programowo lub powiększyć z szablonu XML i pobrać go za pomocą opcji View powiązanej lub
findViewById()
.