Te warsztaty są częścią kursu Zaawansowany Android w Kotlinie. Najwięcej korzyści z tego kursu uzyskasz, jeśli przejdziesz wszystkie ćwiczenia w kolejności, ale nie jest to obowiązkowe. Wszystkie ćwiczenia z tego kursu znajdziesz na stronie docelowej ćwiczeń z zaawansowanego Androida w Kotlinie.
Wprowadzenie
Android oferuje duży zestaw podklas View
, takich jak Button
, TextView
, EditText
, ImageView
, CheckBox
i RadioButton
. Możesz używać tych podklas do tworzenia interfejsu, który umożliwia interakcję użytkownika i wyświetla informacje w aplikacji. Jeśli żadna z podklas View
nie spełnia Twoich potrzeb, możesz utworzyć podklasę View
znaną jako widok niestandardowy.
Aby utworzyć widok niestandardowy, możesz rozszerzyć istniejącą podklasę View
(np. Button
lub EditText
) albo utworzyć własną podklasę View
. Bezpośrednie rozszerzenie klasy View
umożliwia utworzenie interaktywnego elementu interfejsu o dowolnym rozmiarze i kształcie przez zastąpienie metody onDraw()
w klasie View
, aby go narysować.
Po utworzeniu widoku niestandardowego możesz go dodać do układów aktywności w taki sam sposób, jak dodajesz element TextView
lub Button
.
Z tej lekcji dowiesz się, jak utworzyć niestandardowy widok od zera, rozszerzając klasę View
.
Co warto wiedzieć
- Jak utworzyć aplikację z aktywnością i uruchomić ją w Android Studio.
Czego się nauczysz
- Jak rozszerzyć
View
, aby utworzyć widok niestandardowy. - Jak narysować widok niestandardowy w kształcie koła.
- Jak używać detektorów do obsługi interakcji użytkownika z widokiem niestandardowym.
- Jak używać widoku niestandardowego w układzie.
Jakie zadania wykonasz
Aplikacja CustomFanController pokazuje, jak utworzyć podklasę widoku niestandardowego przez rozszerzenie klasy View
. Nowa podklasa nosi nazwę DialView
.
Aplikacja wyświetla okrągły element interfejsu, który przypomina fizyczny panel sterowania wentylatorem, z ustawieniami wyłączony (0), niski (1), średni (2) i wysoki (3). Gdy użytkownik kliknie widok, wskaźnik wyboru przesunie się do następnej pozycji: 0–1–2–3 i z powrotem do 0. Jeśli wybrana wartość wynosi co najmniej 1, kolor tła okrągłej części widoku zmieni się z szarego na zielony (co oznacza, że wentylator jest włączony).
Widoki to podstawowe elementy interfejsu aplikacji. Klasa View
udostępnia wiele podklas, zwanych widżetami interfejsu, które zaspokajają wiele potrzeb typowego interfejsu aplikacji na Androida.
Elementy interfejsu, takie jak Button
i TextView
, to podklasy rozszerzające klasę View
. Aby zaoszczędzić czas i wysiłek związany z programowaniem, możesz rozszerzyć jedną z tych podklas View
. Widok niestandardowy dziedziczy wygląd i zachowanie elementu nadrzędnego. Możesz zastąpić zachowanie lub aspekt wyglądu, który chcesz zmienić. Jeśli np. rozszerzysz element EditText
, aby utworzyć widok niestandardowy, będzie on działać tak samo jak widok EditText
, ale możesz go też dostosować, aby wyświetlał np. przycisk X, który czyści tekst z pola wprowadzania tekstu.
Możesz rozszerzyć dowolną podklasę View
, np. EditText
, aby uzyskać widok niestandardowy. Wybierz tę, która jest najbardziej zbliżona do Twoich potrzeb. Następnie możesz używać widoku niestandardowego tak jak każdej innej podklasy View
w co najmniej jednym układzie jako elementu XML z atrybutami.
Aby utworzyć własny widok niestandardowy od podstaw, rozszerz klasę View
. Twój kod zastępuje metody View
, aby zdefiniować wygląd i funkcjonalność widoku. Kluczem do utworzenia własnego widoku niestandardowego jest to, że odpowiadasz za narysowanie na ekranie całego elementu interfejsu o dowolnym rozmiarze i kształcie. Jeśli utworzysz podklasę istniejącego widoku, np. Button
, ta klasa zajmie się rysowaniem. (Więcej informacji o rysowaniu znajdziesz w dalszej części tego ćwiczenia z programowania).
Aby utworzyć widok niestandardowy, wykonaj te ogólne czynności:
- Utwórz klasę widoku niestandardowego, która rozszerza klasę
View
lub podklasęView
(np.Button
lubEditText
). - Jeśli rozszerzasz istniejącą klasę podrzędną
View
, zastąp tylko te zachowania lub aspekty wyglądu, które chcesz zmienić. - Jeśli rozszerzysz klasę
View
, narysuj kształt widoku niestandardowego i kontroluj jego wygląd, zastępując metodyView
, takie jakonDraw()
ionMeasure()
, w nowej klasie. - Dodaj kod, który będzie reagować na interakcje użytkownika, a w razie potrzeby ponownie narysuj widok niestandardowy.
- Użyj klasy widoku niestandardowego jako widżetu interfejsu w układzie XML aktywności. Możesz też zdefiniować atrybuty niestandardowe widoku, aby dostosować go w różnych układach.
W tym zadaniu:
- Utwórz aplikację z symbolem
ImageView
jako tymczasowym symbolem zastępczym widoku niestandardowego. - Rozwiń
View
, aby utworzyć widok niestandardowy. - Zainicjuj widok niestandardowy za pomocą wartości rysowania i malowania.
Krok 1. Utwórz aplikację z elementem zastępczym ImageView
- Utwórz aplikację w Kotlinie o nazwie
CustomFanController
, korzystając z szablonu Empty Activity. Sprawdź, czy nazwa pakietu tocom.example.android.customfancontroller
. - Otwórz
activity_main.xml
na karcie Tekst, aby edytować kod XML. - Zastąp istniejący kod
TextView
tym kodem. Ten tekst pełni rolę etykiety w aktywności widoku niestandardowego.
<TextView
android:id="@+id/customViewLabel"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Display3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:textColor="@android:color/black"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="24dp"
android:text="Fan Control"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
- Dodaj ten element
ImageView
do układu. To jest symbol zastępczy widoku niestandardowego, który utworzysz w tym laboratorium.
<ImageView
android:id="@+id/dialView"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@android:color/darker_gray"
app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp"/>
- Wyodrębnij ciągi znaków i zasoby wymiarów w obu elementach interfejsu.
- Kliknij kartę Projekt. Układ powinien wyglądać tak:
Krok 2. Utwórz klasę widoku niestandardowego
- Utwórz nową klasę Kotlin o nazwie
DialView
. - Zmodyfikuj definicję klasy, aby rozszerzyć
View
. Gdy pojawi się prośba, zaimportujandroid.view.View
. - Kliknij
View
, a następnie czerwoną żarówkę. Wybierz Add Android View constructors using '@JvmOverloads' (Dodaj konstruktory widoku Androida za pomocą adnotacji „@JvmOverloads”). Android Studio dodaje konstruktor z klasyView
. Adnotacja@JvmOverloads
informuje kompilator języka Kotlin, aby wygenerował przeciążenia tej funkcji, które zastępują domyślne wartości parametrów.
class DialView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
- Nad definicją klasy
DialView
, tuż pod instrukcjami importu, dodaj element najwyższego poziomuenum
, który będzie reprezentować dostępne prędkości wentylatora. Pamiętaj, że tenenum
jest typuInt
, ponieważ wartości są zasobami ciągów tekstowych, a nie rzeczywistymi ciągami tekstowymi. Android Studio wyświetli błędy dotyczące brakujących zasobów ciągów tekstowych w każdej z tych wartości. Naprawisz je w późniejszym kroku.
private enum class FanSpeed(val label: Int) {
OFF(R.string.fan_off),
LOW(R.string.fan_low),
MEDIUM(R.string.fan_medium),
HIGH(R.string.fan_high);
}
- Pod symbolem
enum
dodaj te stałe. Użyjesz ich do rysowania wskaźników i etykiet.
private const val RADIUS_OFFSET_LABEL = 30
private const val RADIUS_OFFSET_INDICATOR = -35
- W klasie
DialView
zdefiniuj kilka zmiennych potrzebnych do narysowania widoku niestandardowego. W razie potrzeby zaimportujandroid.graphics.PointF
.
private var radius = 0.0f // Radius of the circle.
private var fanSpeed = FanSpeed.OFF // The active selection.
// position variable which will be used to draw label and indicator circle position
private val pointPosition: PointF = PointF(0.0f, 0.0f)
radius
to aktualny promień okręgu. Ta wartość jest ustawiana, gdy widok jest rysowany na ekranie.fanSpeed
to aktualna prędkość wentylatora, która jest jedną z wartości w wyliczeniuFanSpeed
. Domyślna wartość toOFF
.- Finally
postPosition
to punkt X,Y, który będzie używany do rysowania na ekranie kilku elementów widoku.
Te wartości są tworzone i inicjowane tutaj, a nie w momencie rysowania widoku, aby zapewnić jak najszybsze wykonanie rzeczywistego kroku rysowania.
- W definicji klasy
DialView
zainicjuj obiektPaint
za pomocą kilku podstawowych stylów. Gdy pojawi się prośba, zaimportujandroid.graphics.Paint
iandroid.graphics.Typeface
. Podobnie jak w przypadku zmiennych, te style są tu inicjowane, aby przyspieszyć proces rysowania.
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
textAlign = Paint.Align.CENTER
textSize = 55.0f
typeface = Typeface.create( "", Typeface.BOLD)
}
- Otwórz
res/values/strings.xml
i dodaj zasoby ciągów znaków dla szybkości wentylatora:
<string name="fan_off">off</string>
<string name="fan_low">1</string>
<string name="fan_medium">2</string>
<string name="fan_high">3</string>
Po utworzeniu widoku niestandardowego musisz umieć go narysować. Gdy rozszerzasz podklasę View
, np. EditText
, definiuje ona wygląd i atrybuty widoku oraz rysuje się na ekranie. Dzięki temu nie musisz pisać kodu, aby narysować widok. Zamiast tego możesz zastąpić metody elementu nadrzędnego, aby dostosować widok.
Jeśli tworzysz własny widok od zera (rozszerzając klasę View
), musisz rysować cały widok za każdym razem, gdy ekran się odświeża, i zastępować metody View
, które obsługują rysowanie. Aby prawidłowo narysować widok niestandardowy, który rozciąga się na View
, musisz:
- Oblicz rozmiar widoku, gdy pojawi się po raz pierwszy, a potem za każdym razem, gdy się zmieni, zastępując metodę
onSizeChanged()
. - Zastąp metodę
onDraw()
, aby narysować widok niestandardowy, używając obiektuCanvas
, którego styl jest określony przez obiektPaint
. - Wywołaj metodę
invalidate()
w odpowiedzi na kliknięcie użytkownika, które zmienia sposób rysowania widoku, aby unieważnić cały widok, wymuszając w ten sposób wywołanie metodyonDraw()
w celu ponownego narysowania widoku.
Metoda onDraw()
jest wywoływana przy każdym odświeżeniu ekranu, co może się zdarzać wiele razy na sekundę. Ze względu na wydajność i aby uniknąć błędów wizualnych, w funkcji onDraw()
należy wykonywać jak najmniej pracy. W szczególności nie umieszczaj alokacji w onDraw()
, ponieważ mogą one prowadzić do odśmiecania, które może powodować zacinanie się obrazu.
Klasy Canvas
i Paint
oferują wiele przydatnych skrótów rysowania:
- Narysuj tekst 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 mapy bitowe za pomocą
drawBitmap()
.
Więcej informacji o Canvas
i Paint
znajdziesz w dalszej części ćwiczeń z programowania. Więcej informacji o tym, jak Android rysuje widoki, znajdziesz w artykule How Android Draws Views (Jak Android rysuje widoki).
W tym zadaniu narysujesz na ekranie widok niestandardowy kontrolera wentylatora – samą tarczę, wskaźnik bieżącego położenia i etykiety wskaźników – za pomocą metod onSizeChanged()
i onDraw()
. Utworzysz też metodę pomocniczą computeXYForSpeed(),
, która oblicza bieżące położenie etykiety wskaźnika na tarczy w osiach X i Y.
Krok 1. Obliczanie pozycji i rysowanie widoku
- W klasie
DialView
, poniżej inicjalizacji, zastąp metodęonSizeChanged()
z klasyView
, aby obliczyć rozmiar tarczy widoku niestandardowego. Importujkotlin
.math.min
, gdy o to poproszą.
MetodaonSizeChanged()
jest wywoływana za każdym razem, gdy zmienia się rozmiar widoku, w tym po raz pierwszy, gdy jest on rysowany po rozwinięciu układu. Zastąp metodęonSizeChanged()
, aby obliczać pozycje, wymiary i inne wartości związane z rozmiarem niestandardowego widoku, zamiast obliczać je ponownie za każdym razem, gdy rysujesz. W tym przypadku używasz funkcjionSizeChanged()
do obliczenia bieżącego promienia elementu okręgu tarczy.
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
radius = (min(width, height) / 2.0 * 0.8).toFloat()
}
- Pod kodem
onSizeChanged()
dodaj ten kod, aby zdefiniować funkcję rozszerzeniacomputeXYForSpeed()
dla klasyPointF
. Gdy pojawi się prośba, zaimportujkotlin.math.cos
ikotlin.math.sin
. Ta funkcja rozszerzająca w klasiePointF
oblicza współrzędne X i Y na ekranie dla etykiety tekstowej i bieżącego wskaźnika (0, 1, 2 lub 3) na podstawie bieżącej pozycjiFanSpeed
i promienia tarczy. Będziesz go używać wonDraw().
private fun PointF.computeXYForSpeed(pos: FanSpeed, radius: Float) {
// Angles are in radians.
val startAngle = Math.PI * (9 / 8.0)
val angle = startAngle + pos.ordinal * (Math.PI / 4)
x = (radius * cos(angle)).toFloat() + width / 2
y = (radius * sin(angle)).toFloat() + height / 2
}
- Zastąp metodę
onDraw()
, aby renderować widok na ekranie za pomocą klasCanvas
iPaint
. W razie potrzeby zaimportujandroid.graphics.Canvas
. Jest to zastąpienie szkieletu:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
}
- W sekcji
onDraw()
dodaj ten wiersz, aby ustawić kolor farby na szary (Color.GRAY
) lub zielony (Color.GREEN
) w zależności od tego, czy prędkość wentylatora wynosiOFF
, czy inną wartość. W razie potrzeby zaimportujandroid.graphics.Color
.
// Set dial background color to green if selection not off.
paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
- Dodaj ten kod, aby narysować okrąg tarczy za pomocą metody
drawCircle()
. Ta metoda wykorzystuje bieżącą szerokość i wysokość widoku do określenia środka okręgu, jego promienia i bieżącego koloru rysowania. Właściwościwidth
iheight
należą do superklasyView
i określają bieżące wymiary widoku.
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
- Dodaj ten kod, aby narysować mniejsze kółko dla wskaźnika prędkości wentylatora, również za pomocą metody
drawCircle()
. Ta część korzysta z metodyPointF
.computeXYforSpeed()
metoda rozszerzenia do obliczania współrzędnych X i Y środka wskaźnika na podstawie aktualnej prędkości wentylatora.
// Draw the indicator circle.
val markerRadius = radius + RADIUS_OFFSET_INDICATOR
pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
paint.color = Color.BLACK
canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
- Na koniec narysuj etykiety prędkości wentylatora (0, 1, 2, 3) w odpowiednich miejscach wokół pokrętła. Ta część metody ponownie wywołuje funkcję
PointF.computeXYForSpeed()
, aby uzyskać pozycję każdej etykiety, i za każdym razem ponownie używa obiektupointPosition
, aby uniknąć przydzielania pamięci. Aby narysować etykiety, użyjdrawText()
.
// Draw the text labels.
val labelRadius = radius + RADIUS_OFFSET_LABEL
for (i in FanSpeed.values()) {
pointPosition.computeXYForSpeed(i, labelRadius)
val label = resources.getString(i.label)
canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
}
Ukończona metoda onDraw()
wygląda tak:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Set dial background color to green if selection not off.
paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
// Draw the indicator circle.
val markerRadius = radius + RADIUS_OFFSET_INDICATOR
pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
paint.color = Color.BLACK
canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
// Draw the text labels.
val labelRadius = radius + RADIUS_OFFSET_LABEL
for (i in FanSpeed.values()) {
pointPosition.computeXYForSpeed(i, labelRadius)
val label = resources.getString(i.label)
canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
}
}
Krok 2. Dodawanie widoku do układu
Aby dodać widok niestandardowy do interfejsu aplikacji, określ go jako element w układzie XML aktywności. Możesz kontrolować jego wygląd i działanie za pomocą atrybutów elementów XML, tak jak w przypadku każdego innego elementu interfejsu.
- W
activity_main.xml
zmień tagImageView
dladialView
nacom.example.android.customfancontroller.DialView
i usuń atrybutandroid:background
. ZarównoDialView
, jak i oryginalnyImageView
dziedziczą standardowe atrybuty z klasyView
, więc nie musisz zmieniać żadnych innych atrybutów. Nowy elementDialView
wygląda tak:
<com.example.android.customfancontroller.DialView
android:id="@+id/dialView"
android:layout_width="@dimen/fan_dimen"
android:layout_height="@dimen/fan_dimen"
app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="@dimen/default_margin"
android:layout_marginRight="@dimen/default_margin"
android:layout_marginTop="@dimen/default_margin" />
- Uruchom aplikację. Widok sterowania wentylatorem pojawi się w aktywności.
Ostatnim zadaniem jest umożliwienie niestandardowemu widokowi wykonania działania, gdy użytkownik go kliknie. Każde kliknięcie powinno przesuwać wskaźnik wyboru do następnej pozycji: wyłączony-1-2-3 i z powrotem do wyłączonego. Jeśli wybór to 1 lub więcej, zmień tło z szarego na zielone, aby wskazać, że wentylator jest włączony.
Aby włączyć możliwość klikania widoku niestandardowego:
- Ustaw właściwość
isClickable
widoku natrue
. Dzięki temu niestandardowy widok może reagować na kliknięcia. - Zaimplementuj klasę
View
z metodąperformClick
()
, aby wykonywać operacje po kliknięciu widoku. - Wywołaj metodę
invalidate()
. Informuje to system Android, że ma wywołać metodęonDraw()
, aby ponownie narysować widok.
Zwykle w przypadku standardowego widoku Androida implementujesz OnClickListener()
, aby wykonać działanie, gdy użytkownik kliknie ten widok. W przypadku widoku niestandardowego zamiast tego implementujesz metodę performClick
()
klasy View
i wywołujesz super
.performClick().
Domyślna metoda performClick()
wywołuje też onClickListener()
, więc możesz dodać swoje działania do performClick()
i pozostawić onClickListener()
do dalszego dostosowywania przez Ciebie lub innych deweloperów, którzy mogą używać Twojego widoku niestandardowego.
- W
DialView.kt
w wyliczeniuFanSpeed
dodaj funkcję rozszerzenianext()
, która zmienia bieżącą prędkość wentylatora na następną prędkość na liście (zOFF
naLOW
,MEDIUM
iHIGH
, a potem z powrotem naOFF
). Pełne wyliczenie wygląda teraz tak:
private enum class FanSpeed(val label: Int) {
OFF(R.string.fan_off),
LOW(R.string.fan_low),
MEDIUM(R.string.fan_medium),
HIGH(R.string.fan_high);
fun next() = when (this) {
OFF -> LOW
LOW -> MEDIUM
MEDIUM -> HIGH
HIGH -> OFF
}
}
- W klasie
DialView
, tuż przed metodąonSizeChanged()
, dodaj blokinit()
. Ustawienie wartości właściwościisClickable
widoku na „true” umożliwia temu widokowi akceptowanie danych wejściowych użytkownika.
init {
isClickable = true
}
- Poniżej
init(),
zastąp metodęperformClick()
poniższym kodem.
override fun performClick(): Boolean {
if (super.performClick()) return true
fanSpeed = fanSpeed.next()
contentDescription = resources.getString(fanSpeed.label)
invalidate()
return true
}
Połączenie z numerem super
.performClick()
musi nastąpić jako pierwsze, co umożliwia zdarzenia związane z ułatwieniami dostępu, a także połączenia onClickListener()
.
Kolejne 2 wiersze zwiększają szybkość wentylatora za pomocą metody next()
i ustawiają opis treści widoku na zasób ciągu znaków reprezentujący bieżącą szybkość (wyłączony, 1, 2 lub 3).
Metoda invalidate()
unieważnia cały widok, co wymusza wywołanie funkcji onDraw()
w celu ponownego narysowania widoku. Jeśli w widoku niestandardowym z jakiegokolwiek powodu nastąpi zmiana, w tym w wyniku interakcji użytkownika, i trzeba ją wyświetlić, wywołaj funkcję invalidate().
.
- Uruchom aplikację. Kliknij element
DialView
, aby przesunąć wskaźnik z pozycji wyłączonej do pozycji 1. Pokrętło powinno zmienić kolor na zielony. Po każdym kliknięciu wskaźnik powinien przesuwać się na następną pozycję. Gdy wskaźnik powróci do pozycji wyłączonej, pokrętło znów powinno być szare.
W tym przykładzie pokazano podstawowe mechanizmy używania atrybutów niestandardowych z widokiem niestandardowym. Atrybuty niestandardowe dla klasy DialView
możesz zdefiniować, przypisując inny kolor do każdej pozycji pokrętła.
- Utwórz i otwórz
res/values/attrs.xml
. - Wewnątrz elementu
<resources>
dodaj element zasobu<declare-styleable>
. - W elemencie zasobu
<declare-styleable>
dodaj 3 elementyattr
– po jednym dla każdego atrybutu – z atrybutaminame
iformat
.format
jest podobny do typu, a w tym przypadku jest tocolor
.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="DialView">
<attr name="fanColor1" format="color" />
<attr name="fanColor2" format="color" />
<attr name="fanColor3" format="color" />
</declare-styleable>
</resources>
- Otwórz plik układu
activity_main.xml
. - W sekcji
DialView
dodaj atrybutyfanColor1
,fanColor2
ifanColor3
oraz ustaw ich wartości na kolory podane poniżej. Użyj znakuapp:
jako przedrostka atrybutu niestandardowego (np.app:fanColor1
), a nie znakuandroid:
, ponieważ atrybuty niestandardowe należą do przestrzeni nazwschemas.android.com/apk/res/
your_app_package_name
, a nie do przestrzeni nazwandroid
.
app:fanColor1="#FFEB3B"
app:fanColor2="#CDDC39"
app:fanColor3="#009688"
Aby użyć atrybutów w klasie DialView
, musisz je pobrać. Są one przechowywane w AttributeSet
, który jest przekazywany Twojej klasie po utworzeniu, jeśli istnieje. Atrybuty są pobierane w sekcji init
, a wartości atrybutów są przypisywane do zmiennych lokalnych w celu buforowania.
- Otwórz plik klasy
DialView.kt
. - W sekcji
DialView
zadeklaruj zmienne, aby zapisać w pamięci podręcznej wartości atrybutów.
private var fanSpeedLowColor = 0
private var fanSpeedMediumColor = 0
private var fanSeedMaxColor = 0
- W bloku
init
dodaj ten kod za pomocą funkcji rozszerzeniawithStyledAttributes
. Podajesz atrybuty i widok oraz ustawiasz zmienne lokalne. ZaimportowaniewithStyledAttributes
spowoduje też zaimportowanie funkcjigetColor()
.
context.withStyledAttributes(attrs, R.styleable.DialView) {
fanSpeedLowColor = getColor(R.styleable.DialView_fanColor1, 0)
fanSpeedMediumColor = getColor(R.styleable.DialView_fanColor2, 0)
fanSeedMaxColor = getColor(R.styleable.DialView_fanColor3, 0)
}
- Użyj zmiennych lokalnych w
onDraw()
, aby ustawić kolor tarczy na podstawie bieżącej szybkości wentylatora. Zastąp wiersz, w którym ustawiony jest kolor farby (paint
.
color
=
if
(
fanSpeed
== FanSpeed.
OFF
) Color.
GRAY
else
Color.
GREEN
), kodem podanym poniżej.
paint.color = when (fanSpeed) {
FanSpeed.OFF -> Color.GRAY
FanSpeed.LOW -> fanSpeedLowColor
FanSpeed.MEDIUM -> fanSpeedMediumColor
FanSpeed.HIGH -> fanSeedMaxColor
} as Int
- Uruchom aplikację, kliknij pokrętło, a ustawienie koloru powinno być inne dla każdej pozycji, jak pokazano poniżej.
Więcej informacji o atrybutach widoku niestandardowego znajdziesz w artykule Tworzenie klasy widoku.
Ułatwienia dostępu to zestaw technik projektowania, wdrażania i testowania, które umożliwiają korzystanie z aplikacji wszystkim użytkownikom, w tym osobom z niepełnosprawnościami.
Do typowych niepełnosprawności, które mogą utrudniać korzystanie z urządzenia z Androidem, należą ślepota, niedowidzenie, daltonizm, głuchota lub niedosłuch oraz ograniczone zdolności motoryczne. Tworząc aplikacje z myślą o ułatwieniach dostępu, poprawiasz komfort korzystania z nich nie tylko użytkownikom z niepełnosprawnościami, ale też wszystkim innym.
Android domyślnie udostępnia kilka funkcji ułatwień dostępu w standardowych widokach interfejsu, takich jak TextView
i Button
. Gdy tworzysz widok niestandardowy, musisz jednak zastanowić się, jak zapewni on funkcje ułatwień dostępu, takie jak odczytywanie na głos treści wyświetlanych na ekranie.
W tym ćwiczeniu dowiesz się więcej o czytniku ekranu TalkBack na Androidzie i zmodyfikujesz aplikację, aby zawierała wskazówki i opisy do odczytania dla DialView
widoku niestandardowego.
Krok 1. Poznaj TalkBack
TalkBack to wbudowany czytnik ekranu Androida. Gdy TalkBack jest włączony, użytkownik może korzystać z urządzenia z Androidem bez patrzenia na ekran, ponieważ Android odczytuje na głos elementy ekranu. Użytkownicy z wadami wzroku mogą korzystać z Twojej aplikacji za pomocą TalkBack.
W tym zadaniu włączysz TalkBack, aby dowiedzieć się, jak działają czytniki ekranu i jak poruszać się po aplikacjach.
- Na urządzeniu z Androidem lub emulatorze otwórz Ustawienia > Ułatwienia dostępu > TalkBack.
- Kliknij przełącznik Wł./Wył., aby włączyć TalkBack.
- Aby potwierdzić uprawnienia, kliknij OK.
- W razie potrzeby potwierdź hasło do urządzenia. Jeśli uruchamiasz TalkBack po raz pierwszy, otworzy się samouczek. (Samouczek może być niedostępny na starszych urządzeniach).
- Warto przejść samouczek z zamkniętymi oczami. Aby ponownie otworzyć samouczek w przyszłości, wybierz Ustawienia > Ułatwienia dostępu > TalkBack > Ustawienia > Uruchom samouczek TalkBack.
- Skompiluj i uruchom
CustomFanController
aplikację lub otwórz ją za pomocą przycisku Przegląd lub Ostatnie na urządzeniu. Gdy TalkBack jest włączony, nazwa aplikacji jest odczytywana na głos, podobnie jak tekst etykietyTextView
(„Sterowanie wentylatorem”). Jeśli jednak klikniesz sam widokDialView
, nie usłyszysz informacji o jego stanie (bieżącym ustawieniu pokrętła) ani o działaniu, które zostanie wykonane po kliknięciu widoku w celu jego aktywowania.
Krok 2. Dodawanie opisów treści do etykiet pokrętła
Opisy treści wyjaśniają znaczenie i przeznaczenie widoków w aplikacji. Te etykiety umożliwiają czytnikom ekranu, takim jak funkcja TalkBack na Androidzie, dokładne wyjaśnienie funkcji każdego elementu. W przypadku widoków statycznych, takich jak ImageView
, możesz dodać opis treści do widoku w pliku układu za pomocą atrybutu contentDescription
. Widoki tekstu (TextView
i EditText
) automatycznie używają tekstu w widoku jako opisu treści.
W przypadku widoku niestandardowego sterowania wentylatorem musisz dynamicznie aktualizować opis treści za każdym razem, gdy użytkownik kliknie widok, aby wskazać bieżące ustawienie wentylatora.
- U dołu
DialView
klasy zadeklaruj funkcjęupdateContentDescription()
bez argumentów ani typu zwracanego.
fun updateContentDescription() {
}
- W
updateContentDescription()
zmień właściwośćcontentDescription
w przypadku widoku niestandardowego na zasób tekstowy powiązany z bieżącą szybkością wentylatora (wyłączony, 1, 2 lub 3). Są to te same etykiety, które są używane wonDraw()
, gdy tarcza jest rysowana na ekranie.
fun updateContentDescription() {
contentDescription = resources.getString(fanSpeed.label)
}
- Przewiń w górę do bloku
init()
i na jego końcu dodaj wywołanie funkcjiupdateContentDescription()
. Inicjuje opis treści po zainicjowaniu widoku.
init {
isClickable = true
// ...
updateContentDescription()
}
- Dodaj kolejne wywołanie do
updateContentDescription()
w metodzieperformClick()
, tuż przedinvalidate()
.
override fun performClick(): Boolean {
if (super.performClick()) return true
fanSpeed = fanSpeed.next()
updateContentDescription()
invalidate()
return true
}
- Skompiluj i uruchom aplikację, a potem włącz TalkBack. Kliknij, aby zmienić ustawienie widoku pokrętła. Zwróć uwagę, że TalkBack odczytuje teraz bieżącą etykietę (wyłączone, 1, 2, 3) oraz frazę „Kliknij dwukrotnie, aby aktywować”.
Krok 3. Dodawanie dodatkowych informacji o działaniu kliknięcia
Możesz na tym poprzestać, a widok będzie działać w TalkBack. Warto jednak, aby widok informował nie tylko o tym, że może zostać aktywowany („Kliknij dwukrotnie, aby aktywować”), ale też wyjaśniał, co się stanie po jego aktywacji („Kliknij dwukrotnie, aby zmienić” lub „Kliknij dwukrotnie, aby zresetować”).
Aby to zrobić, dodaj informacje o działaniu widoku (w tym przypadku kliknięciu lub dotknięciu) do obiektu informacji o węźle ułatwień dostępu za pomocą delegata ułatwień dostępu. Delegat ułatwień dostępu umożliwia dostosowywanie funkcji aplikacji związanych z ułatwieniami dostępu za pomocą kompozycji (a nie dziedziczenia).
Aby wykonać to zadanie, użyj klas ułatwień dostępu w bibliotekach Androida Jetpack (androidx.*
), aby zapewnić zgodność wsteczną.
- W
DialView.kt
w blokuinit
ustaw delegata ułatwień dostępu w widoku jako nowy obiektAccessibilityDelegateCompat
. Gdy pojawi się prośba, zaimportujandroidx.core.view.ViewCompat
iandroidx.core.view.AccessibilityDelegateCompat
. Ta strategia zapewnia największą zgodność wsteczną w aplikacji.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
})
- W obiekcie
AccessibilityDelegateCompat
zastąp funkcjęonInitializeAccessibilityNodeInfo()
obiektemAccessibilityNodeInfoCompat
i wywołaj metodę klasy nadrzędnej. Gdy pojawi się prośba, zaimportujandroidx.core.view.accessibility.AccessibilityNodeInfoCompat
.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View,
info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
}
})
Każdy widok ma drzewo węzłów ułatwień dostępu, które może, ale nie musi odpowiadać rzeczywistym komponentom układu widoku. Usługi ułatwień dostępu na Androidzie poruszają się po tych węzłach, aby uzyskać informacje o widoku (np. opisy treści, które można odczytać, lub możliwe działania, które można wykonać w tym widoku). Podczas tworzenia widoku niestandardowego może być też konieczne zastąpienie informacji o węźle, aby podać niestandardowe informacje na potrzeby ułatwień dostępu. W takim przypadku zastąpisz informacje o węźle, aby wskazać, że działanie widoku zawiera informacje niestandardowe.
- W obiekcie
onInitializeAccessibilityNodeInfo()
utwórz nowy obiektAccessibilityNodeInfoCompat.AccessibilityActionCompat
i przypisz go do zmiennejcustomClick
. Przekaż do konstruktora stałąAccessibilityNodeInfo.ACTION_CLICK
i ciąg znaków zastępczych. W razie potrzeby zaimportujAccessibilityNodeInfo
.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View,
info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
AccessibilityNodeInfo.ACTION_CLICK,
"placeholder"
)
}
})
Klasa AccessibilityActionCompat
reprezentuje działanie na widoku na potrzeby ułatwień dostępu. Typowe działanie to kliknięcie lub dotknięcie, jak w tym przypadku, ale inne działania mogą obejmować uzyskanie lub utratę fokusu, operację schowka (wytnij/kopiuj/wklej) lub przewijanie w widoku. Konstruktor tej klasy wymaga stałej działania (w tym przypadku AccessibilityNodeInfo.ACTION_CLICK
) i ciągu znaków, który jest używany przez TalkBack do wskazywania, jakie jest działanie.
- Zastąp ciąg znaków
"placeholder"
wywołaniem funkcjicontext.getString()
, aby pobrać zasób w postaci ciągu znaków. W przypadku konkretnego urządzenia sprawdź aktualną szybkość obrotów wentylatora. Jeśli prędkość wynosi obecnieFanSpeed.HIGH
, ciąg znaków to"Reset"
. Jeśli prędkość wentylatora jest inna, ciąg znaków to"Change."
. Te zasoby ciągów znaków utworzysz w późniejszym kroku.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View,
info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
AccessibilityNodeInfo.ACTION_CLICK,
context.getString(if (fanSpeed != FanSpeed.HIGH) R.string.change else R.string.reset)
)
}
})
- Po nawiasie zamykającym definicję
customClick
użyj metodyaddAction()
, aby dodać nowe działanie ułatwień dostępu do obiektu informacji o węźle.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View,
info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
AccessibilityNodeInfo.ACTION_CLICK,
context.getString(if (fanSpeed != FanSpeed.HIGH)
R.string.change else R.string.reset)
)
info.addAction(customClick)
}
})
- W pliku
res/values/strings.xml
dodaj zasoby ciągów znaków dla opcji „Zmień” i „Zresetuj”.
<string name="change">Change</string>
<string name="reset">Reset</string>
- Skompiluj i uruchom aplikację, a potem upewnij się, że funkcja TalkBack jest włączona. Zauważ, że fraza „Kliknij dwukrotnie, aby włączyć” zmieniła się na „Kliknij dwukrotnie, aby zmienić” (jeśli szybkość wentylatora jest mniejsza niż wysoka lub 3) lub „Kliknij dwukrotnie, aby zresetować” (jeśli szybkość wentylatora jest już wysoka lub 3). Pamiętaj, że komunikat „Kliknij dwukrotnie, aby…” jest dostarczany przez samą usługę TalkBack.
Pobierz kod ukończonego ćwiczenia.
$ git clone https://github.com/googlecodelabs/android-kotlin-drawing-custom-views
Możesz też pobrać repozytorium jako plik ZIP, rozpakować go i otworzyć w Android Studio.
- Aby utworzyć widok niestandardowy, który dziedziczy wygląd i działanie podklasy
View
, np.EditText
, dodaj nową klasę, która rozszerza tę podklasę, i wprowadź zmiany, zastępując niektóre metody podklasy. - Aby utworzyć widok niestandardowy o dowolnym rozmiarze i kształcie, dodaj nową klasę, która rozszerza
View
. - Zastąp metody
View
, takie jakonDraw()
, aby zdefiniować kształt i podstawowy wygląd widoku. - Użyj
invalidate()
, aby wymusić narysowanie lub ponowne narysowanie widoku. - Aby zoptymalizować wydajność, przydziel zmienne i przypisz wszelkie wymagane wartości do rysowania i malowania przed użyciem ich w
onDraw()
, np. podczas inicjowania zmiennych składowych. - Zastąp
performClick()
zamiastOnClickListener
() w widoku niestandardowym, aby zapewnić interaktywne działanie widoku. Umożliwia to Tobie i innym programistom na Androida, którzy mogą używać Twojej niestandardowej klasy widoku, używanieonClickListener()
do zapewnienia dodatkowych funkcji. - Dodaj widok niestandardowy do pliku układu XML z atrybutami określającymi jego wygląd, tak jak w przypadku innych elementów interfejsu.
- Utwórz plik
attrs.xml
w folderzevalues
, aby zdefiniować atrybuty niestandardowe. Następnie możesz użyć atrybutów niestandardowych w niestandardowym widoku w pliku układu XML.
Kurs Udacity:
Dokumentacja dla deweloperów aplikacji na Androida:
- Tworzenie widoków niestandardowych
@JvmOverloads
- Komponenty niestandardowe
- Jak Android rysuje widoki
onMeasure()
onSizeChanged()
onDraw()
Canvas
Paint
drawText()
setTypeface()
setColor()
drawRect()
drawOval()
drawArc()
drawBitmap()
setStyle()
invalidate()
- Wyświetl
- Zdarzenia wejściowe
- Malowanie
- Biblioteka rozszerzeń Kotlin android-ktx
withStyledAttributes
- Dokumentacja Android KTX
- Blog z oryginalnym ogłoszeniem dotyczącym Androida KTX
- Zwiększanie dostępności widoków niestandardowych
AccessibilityDelegateCompat
AccessibilityNodeInfoCompat
AccessibilityNodeInfoCompat.AccessibilityActionCompat
Materiały wideo:
W tej sekcji znajdziesz listę możliwych zadań domowych dla uczniów, którzy wykonują ten moduł w ramach kursu prowadzonego przez instruktora. Nauczyciel musi:
- W razie potrzeby przypisz pracę domową.
- Poinformuj uczniów, jak przesyłać projekty.
- Oceń zadania domowe.
Instruktorzy mogą korzystać z tych sugestii w dowolnym zakresie i mogą zadawać inne zadania domowe, które uznają za odpowiednie.
Jeśli wykonujesz ten kurs samodzielnie, możesz użyć tych zadań domowych, aby sprawdzić swoją wiedzę.
Pytanie 1
Którą metodę zastąpisz, aby obliczyć pozycje, wymiary i inne wartości, gdy widok niestandardowy po raz pierwszy otrzyma rozmiar?
▢ onMeasure()
▢ onSizeChanged()
▢ invalidate()
▢ onDraw()
Pytanie 2
Aby wskazać, że chcesz ponownie narysować widok za pomocą metody onDraw()
, którą metodę wywołasz z wątku interfejsu po zmianie wartości atrybutu?
▢ onMeasure()
▢ onSizeChanged()
▢ invalidate()
▢ getVisibility()
Pytanie 3
Którą metodę View
należy zastąpić, aby dodać interaktywność do widoku niestandardowego?
▢ setOnClickListener()
▢ onSizeChanged()
▢ isClickable()
▢ performClick()
Linki do innych ćwiczeń z tego kursu znajdziesz na stronie docelowej ćwiczeń z zaawansowanego Androida w Kotlinie.