Tworzenie widoków niestandardowych

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, CheckBoxRadioButton. 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

  • Rozwiń View, aby utworzyć widok niestandardowy.
  • Zainicjuj widok niestandardowy za pomocą wartości rysowania i malowania.
  • Zastąp onDraw(), aby narysować widok.
  • Używaj odbiorców, aby określać zachowanie widoku niestandardowego.
  • Dodaj widok niestandardowy do układu.

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 ButtonTextView, 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 lub EditText).
  • 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 metody View, takie jak onDraw()onMeasure(), 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

  1. Utwórz aplikację w Kotlinie o nazwie CustomFanController, korzystając z szablonu Empty Activity. Sprawdź, czy nazwa pakietu to com.example.android.customfancontroller.
  2. Otwórz activity_main.xml na karcie Tekst, aby edytować kod XML.
  3. 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"/>
  1. 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"/>
  1. Wyodrębnij ciągi znaków i zasoby wymiarów w obu elementach interfejsu.
  2. Kliknij kartę Projekt. Układ powinien wyglądać tak:

Krok 2. Utwórz klasę widoku niestandardowego

  1. Utwórz nową klasę Kotlin o nazwie DialView.
  2. Zmodyfikuj definicję klasy, aby rozszerzyć View. Gdy pojawi się prośba, zaimportuj android.view.View.
  3. 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 klasy View. 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) {
  1. Nad definicją klasy DialView, tuż pod instrukcjami importu, dodaj element najwyższego poziomu enum, który będzie reprezentować dostępne prędkości wentylatora. Pamiętaj, że ten enum jest typu Int, 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);
}
  1. 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
  1. W klasie DialView zdefiniuj kilka zmiennych potrzebnych do narysowania widoku niestandardowego. W razie potrzeby zaimportuj android.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 wyliczeniu FanSpeed. Domyślna wartość to OFF.
  • FinallypostPosition 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.

  1. W definicji klasy DialView zainicjuj obiekt Paint za pomocą kilku podstawowych stylów. Gdy pojawi się prośba, zaimportuj android.graphics.Paintandroid.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)
}
  1. 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 obiektu Canvas, którego styl jest określony przez obiekt Paint.
  • 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 metody onDraw() 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 CanvasPaint oferują wiele przydatnych skrótów rysowania:

Więcej informacji o CanvasPaint 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()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

  1. W klasie DialView, poniżej inicjalizacji, zastąp metodę onSizeChanged() z klasy View, aby obliczyć rozmiar tarczy widoku niestandardowego. Importuj kotlin.math.min, gdy o to poproszą.

    Metoda onSizeChanged() 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 funkcji onSizeChanged() 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()
}
  1. Pod kodem onSizeChanged() dodaj ten kod, aby zdefiniować funkcję rozszerzenia computeXYForSpeed() dla klasy PointF . Gdy pojawi się prośba, zaimportuj kotlin.math.coskotlin.math.sin. Ta funkcja rozszerzająca w klasie PointF 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 pozycji FanSpeed i promienia tarczy. Będziesz go używać w onDraw().
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
}
  1. Zastąp metodę onDraw(), aby renderować widok na ekranie za pomocą klas CanvasPaint. W razie potrzeby zaimportuj android.graphics.Canvas. Jest to zastąpienie szkieletu:
override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   
}
  1. 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 wynosi OFF, czy inną wartość. W razie potrzeby zaimportuj android.graphics.Color.
// Set dial background color to green if selection not off.
paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
  1. 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ści widthheight należą do superklasy View i określają bieżące wymiary widoku.
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
  1. 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 metody PointF.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)
  1. 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 obiektu pointPosition, aby uniknąć przydzielania pamięci. Aby narysować etykiety, użyj drawText().
// 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.

  1. activity_main.xml zmień tag ImageView dla dialView na com.example.android.customfancontroller.DialView i usuń atrybut android:background. Zarówno DialView, jak i oryginalny ImageView dziedziczą standardowe atrybuty z klasy View, więc nie musisz zmieniać żadnych innych atrybutów. Nowy element DialView 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" />
  1. 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 na true. 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.

  1. DialView.kt w wyliczeniu FanSpeed dodaj funkcję rozszerzenia next(), która zmienia bieżącą prędkość wentylatora na następną prędkość na liście (z OFF na LOW, MEDIUMHIGH, a potem z powrotem na OFF). 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
   }
}
  1. W klasie DialView, tuż przed metodą onSizeChanged(), dodaj blok init(). Ustawienie wartości właściwości isClickable widoku na „true” umożliwia temu widokowi akceptowanie danych wejściowych użytkownika.
init {
   isClickable = true
}
  1. 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()..

  1. 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.

  1. Utwórz i otwórz res/values/attrs.xml.
  2. Wewnątrz elementu <resources> dodaj element zasobu <declare-styleable>.
  3. W elemencie zasobu <declare-styleable> dodaj 3 elementy attr – po jednym dla każdego atrybutu – z atrybutami nameformat. format jest podobny do typu, a w tym przypadku jest to color.
<?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>
  1. Otwórz plik układu activity_main.xml.
  2. W sekcji DialView dodaj atrybuty fanColor1, fanColor2 i fanColor3 oraz ustaw ich wartości na kolory podane poniżej. Użyj znaku app: jako przedrostka atrybutu niestandardowego (np. app:fanColor1), a nie znaku android:, ponieważ atrybuty niestandardowe należą do przestrzeni nazw schemas.android.com/apk/res/your_app_package_name, a nie do przestrzeni nazw android.
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.

  1. Otwórz plik klasy DialView.kt.
  2. 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
  1. W bloku init dodaj ten kod za pomocą funkcji rozszerzenia withStyledAttributes. Podajesz atrybuty i widok oraz ustawiasz zmienne lokalne. Zaimportowanie withStyledAttributes spowoduje też zaimportowanie funkcji getColor().
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)
}
  1. 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
  1. 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 TextViewButton. 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.

  1. Na urządzeniu z Androidem lub emulatorze otwórz Ustawienia > Ułatwienia dostępu > TalkBack.
  2. Kliknij przełącznik Wł./Wył., aby włączyć TalkBack.
  3. Aby potwierdzić uprawnienia, kliknij OK.
  4. 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).
  5. 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.
  6. 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 etykiety TextView („Sterowanie wentylatorem”). Jeśli jednak klikniesz sam widok DialView, 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 (TextViewEditText) 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.

  1. U dołu DialView klasy zadeklaruj funkcję updateContentDescription() bez argumentów ani typu zwracanego.
fun updateContentDescription() {
}
  1. 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 w onDraw(), gdy tarcza jest rysowana na ekranie.
fun updateContentDescription() {
   contentDescription = resources.getString(fanSpeed.label)
}
  1. Przewiń w górę do bloku init() i na jego końcu dodaj wywołanie funkcji updateContentDescription(). Inicjuje opis treści po zainicjowaniu widoku.
init {
   isClickable = true
   // ...

   updateContentDescription()
}
  1. Dodaj kolejne wywołanie do updateContentDescription() w metodzie performClick(), tuż przed invalidate().
override fun performClick(): Boolean {
   if (super.performClick()) return true
   fanSpeed = fanSpeed.next()
   updateContentDescription()
   invalidate()
   return true
}
  1. 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ą.

  1. DialView.kt w bloku init ustaw delegata ułatwień dostępu w widoku jako nowy obiekt AccessibilityDelegateCompat. Gdy pojawi się prośba, zaimportuj androidx.core.view.ViewCompatandroidx.core.view.AccessibilityDelegateCompat. Ta strategia zapewnia największą zgodność wsteczną w aplikacji.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   
})
  1. W obiekcie AccessibilityDelegateCompat zastąp funkcję onInitializeAccessibilityNodeInfo() obiektem AccessibilityNodeInfoCompat i wywołaj metodę klasy nadrzędnej. Gdy pojawi się prośba, zaimportuj androidx.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.

  1. W obiekcie onInitializeAccessibilityNodeInfo() utwórz nowy obiekt AccessibilityNodeInfoCompat.AccessibilityActionCompat i przypisz go do zmiennej customClick. Przekaż do konstruktora stałą AccessibilityNodeInfo.ACTION_CLICK i ciąg znaków zastępczych. W razie potrzeby zaimportuj AccessibilityNodeInfo.
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.

  1. Zastąp ciąg znaków "placeholder" wywołaniem funkcji context.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 obecnie FanSpeed.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)
      )
   }  
})
  1. Po nawiasie zamykającym definicję customClick użyj metody addAction(), 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)
   }
})
  1. 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>
  1. 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.

Pobierz plik ZIP

  • 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 jak onDraw(), 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() zamiast OnClickListener() 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żywanie onClickListener() 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 folderze values, 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:

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.