Эта практическая работа входит в курс «Advanced Android in Kotlin». Вы получите максимальную пользу от этого курса, выполняя задания последовательно, но это не обязательно. Все практическая работа курса перечислены на целевой странице практической работы «Advanced Android in Kotlin» .
Введение
Android предлагает большой набор подклассов View , таких как Button , TextView , EditText , ImageView , CheckBox и RadioButton . Вы можете использовать эти подклассы для создания пользовательского интерфейса, обеспечивающего взаимодействие с пользователем и отображение информации в вашем приложении. Если ни один из подклассов View не соответствует вашим потребностям, вы можете создать подкласс View , называемый пользовательским представлением.
Чтобы создать собственное представление, вы можете либо расширить существующий подкласс View (например, Button или EditText ), либо создать свой собственный подкласс View . Расширяя View напрямую, вы можете создать интерактивный элемент пользовательского интерфейса любого размера и формы, переопределив метод onDraw() для отображения этого элемента в View .
После создания пользовательского представления вы можете добавить его в макеты своих действий таким же образом, как вы добавляете TextView или Button .
В этом уроке показано, как создать пользовательское представление с нуля, расширив View .
Что вам уже следует знать
- Как создать приложение с Activity и запустить его с помощью Android Studio.
Чему вы научитесь
- Как расширить
View, чтобы создать пользовательское представление. - Как нарисовать нестандартный вид круглой формы.
- Как использовать прослушиватели для обработки взаимодействия пользователя с пользовательским представлением.
- Как использовать пользовательское представление в макете.
Что ты будешь делать?
- Расширьте
View, чтобы создать пользовательское представление. - Инициализируйте пользовательское представление с помощью значений чертежа и рисования.
- Переопределите
onDraw()для рисования вида. - Используйте прослушиватели для обеспечения поведения пользовательского представления.
- Добавьте пользовательское представление в макет.
Приложение CustomFanController демонстрирует, как создать собственный подкласс представления, расширяя класс View . Новый подкласс называется DialView .
Приложение отображает круглый элемент пользовательского интерфейса, напоминающий физический регулятор вентилятора, с настройками: «Выкл.» (0), «Низкая» (1), «Средняя» (2) и «Высокая» (3). При касании представления индикатор выбора перемещается на следующую позицию: 0-1-2-3, и обратно на 0. Кроме того, если выбрано значение 1 или выше, цвет фона круглой части представления меняется с серого на зелёный (что означает, что вентилятор включён).


Представления — это основные строительные блоки пользовательского интерфейса приложения. Класс View предоставляет множество подклассов, называемых виджетами пользовательского интерфейса (UI widgets) , которые охватывают многие потребности типичного пользовательского интерфейса приложения Android.
Такие элементы пользовательского интерфейса, как Button и TextView являются подклассами, расширяющими класс View . Чтобы сэкономить время и усилия на разработку, вы можете расширить один из этих подклассов View . Пользовательское представление наследует внешний вид и поведение своего родительского элемента, и вы можете переопределить поведение или аспект внешнего вида, который хотите изменить. Например, если вы расширяете EditText для создания пользовательского представления, оно будет работать так же, как представление EditText , но его также можно настроить для отображения, например, кнопки X , которая очищает текст в поле ввода.

Вы можете расширить любой подкласс View , например EditText , чтобы получить пользовательское представление — выберите наиболее близкое к желаемому результату. Затем вы можете использовать пользовательское представление, как и любой другой подкласс View в одном или нескольких макетах как XML-элемент с атрибутами.
Чтобы создать собственное пользовательское представление с нуля, расширьте класс View . Ваш код переопределяет методы View для определения внешнего вида и функциональности представления. Ключевым моментом в создании собственного пользовательского представления является то, что вы отвечаете за отрисовку всего элемента пользовательского интерфейса любого размера и формы на экране. Если вы создаёте подкласс существующего представления, например Button , этот класс будет управлять отрисовкой автоматически. (Подробнее о рисовании вы узнаете далее в этой практической работе.)
Чтобы создать пользовательское представление, выполните следующие общие шаги:
- Создайте пользовательский класс представления, который расширяет
Viewили расширяет подклассView(например,ButtonилиEditText). - Если вы расширяете существующий подкласс
View, переопределите только то поведение или аспекты внешнего вида, которые вы хотите изменить. - Если вы расширяете класс
View, нарисуйте форму пользовательского представления и управляйте его внешним видом, переопределив методыView, такие какonDraw()иonMeasure()в новом классе. - Добавьте код для реагирования на взаимодействие с пользователем и, при необходимости, перерисуйте пользовательское представление.
- Используйте класс пользовательского представления в качестве виджета пользовательского интерфейса в XML-макете вашей активности. Вы также можете определить пользовательские атрибуты для представления, чтобы настроить его в различных макетах.
В этом задании вам предстоит:
- Создайте приложение с
ImageViewв качестве временного заполнителя для пользовательского представления. - Расширьте
View, чтобы создать пользовательское представление. - Инициализируйте пользовательское представление с помощью значений чертежа и рисования.
Шаг 1: Создайте приложение с заполнителем ImageView
- Создайте приложение Kotlin с названием
CustomFanController, используя шаблон Empty Activity. Убедитесь, что имя пакета —com.example.android.customfancontroller. - Откройте
activity_main.xmlна вкладке Текст , чтобы отредактировать XML-код. - Замените существующий
TextViewэтим кодом. Этот текст будет использоваться в качестве метки в активности для пользовательского представления.
<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"/>- Добавьте этот элемент
ImageViewв макет. Это заполнитель для пользовательского представления, которое вы создадите в этой практической работе.
<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"/>- Извлечь строковые и размерные ресурсы из обоих элементов пользовательского интерфейса.
- Перейдите на вкладку «Дизайн» . Макет должен выглядеть так:

Шаг 2. Создайте свой собственный класс представления
- Создайте новый класс Kotlin с именем
DialView. - Измените определение класса, чтобы расширить
View. Импортируйтеandroid.view.Viewпри появлении соответствующего запроса. - Нажмите
View, а затем нажмите на красную лампочку. Выберите «Добавить конструкторы Android View с помощью @JvmOverloads» . Android Studio добавит конструктор из классаView. Аннотация@JvmOverloadsуказывает компилятору Kotlin сгенерировать перегрузки для этой функции, которые заменяют значения параметров по умолчанию.
class DialView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {- Над определением класса
DialView, сразу под импортируемыми элементами, добавьтеenumверхнего уровня для представления доступных скоростей вентилятора. Обратите внимание, что этоenumимеет типInt, поскольку значения представляют собой строковые ресурсы, а не настоящие строки. Android Studio выдаст ошибки из-за отсутствия строковых ресурсов в каждом из этих значений; вы исправите это позже.
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);
}- Добавьте эти константы под
enum. Они понадобятся вам для отрисовки циферблатных индикаторов и меток.
private const val RADIUS_OFFSET_LABEL = 30
private const val RADIUS_OFFSET_INDICATOR = -35- Внутри класса
DialViewопределите несколько переменных, необходимых для отрисовки пользовательского представления. При необходимости импортируйте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текущий радиус окружности. Это значение устанавливается при отображении изображения на экране. - Параметр
fanSpeed— это текущая скорость вращения вентилятора , которая является одним из значений перечисленияFanSpeed. По умолчанию это значение равноOFF. - Наконец,
postPosition— это точка X,Y , которая будет использоваться для отрисовки нескольких элементов представления на экране.
Эти значения создаются и инициализируются здесь, а не при фактической отрисовке вида, чтобы гарантировать максимально быстрое выполнение фактического этапа отрисовки.
- Также внутри определения класса
DialViewинициализируйте объектPaintс несколькими базовыми стилями. Импортируйтеandroid.graphics.Paintиandroid.graphics.Typefaceпо запросу. Как и в случае с переменными, эти стили инициализируются здесь для ускорения процесса рисования.
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)
}- Откройте
res/values/strings.xmlи добавьте строковые ресурсы для скорости вращения вентилятора:
<string name="fan_off">off</string>
<string name="fan_low">1</string>
<string name="fan_medium">2</string>
<string name="fan_high">3</string> После создания пользовательского представления необходимо иметь возможность его отрисовки. При расширении подкласса View , например EditText , этот подкласс определяет внешний вид и атрибуты представления и отображает себя на экране. Следовательно, вам не нужно писать код для отрисовки представления. Вместо этого вы можете переопределить методы родительского класса, чтобы настроить представление.
Если вы создаёте собственное представление с нуля (расширяя View ), вы отвечаете за отрисовку всего представления при каждом обновлении экрана и за переопределение методов View , отвечающих за отрисовку. Для корректной отрисовки пользовательского представления, расширяющего View , необходимо:
- Рассчитайте размер представления при его первом появлении и каждый раз при изменении его размера, переопределив метод
onSizeChanged(). - Переопределите метод
onDraw()для рисования пользовательского представления, используя объектCanvas, стилизованный под объектPaint. - Вызовите метод
invalidate()в ответ на щелчок пользователя, который изменяет способ отрисовки представления, чтобы сделать недействительным все представление, тем самым принудительно вызывая вызовonDraw()для перерисовки представления.
Метод onDraw() вызывается каждый раз при обновлении экрана, что может происходить много раз в секунду. Из соображений производительности и во избежание визуальных сбоев следует выполнять как можно меньше действий в onDraw() . В частности, не следует размещать выделение памяти в onDraw() , поскольку это может привести к сборке мусора, что может привести к визуальному подтормаживанию.
Классы Canvas и Paint предлагают ряд полезных сокращений для рисования:
- Для рисования текста используйте
drawText(). Для указания шрифта используйте функциюsetTypeface(), а для указания цвета текста —setColor(). - Рисуйте примитивные фигуры с помощью
drawRect(),drawOval()иdrawArc(). Чтобы изменить заливку, контур или и то, и другое, вызовитеsetStyle(). - Рисуйте растровые изображения с помощью
drawBitmap().
Подробнее о Canvas и Paint вы узнаете в следующем практическом занятии. Подробнее о том, как Android отрисовывает представления, см. в статье «Как Android отрисовывает представления» .
В этом задании вы отобразите на экране пользовательское представление контроллера вентилятора — сам циферблат, индикатор текущего положения и метки индикатора — с помощью методов onSizeChanged() и onDraw() . Вы также создадите вспомогательный метод computeXYForSpeed(), для вычисления текущего положения метки индикатора на циферблате по осям X и Y.
Шаг 1. Рассчитать позиции и нарисовать вид
- В классе
DialView, под инициализациями, переопределите методonSizeChanged()из классаViewmath.minрасчета размера циферблата пользовательского представления. Импортируйтеkotlinпо запросу.
МетодonSizeChanged()вызывается каждый раз при изменении размера представления, включая первую отрисовку при увеличении макета. ПереопределитеonSizeChanged()для расчета положения, размеров и любых других значений, связанных с размером вашего пользовательского представления, вместо того, чтобы пересчитывать их при каждой отрисовке. В этом случае методonSizeChanged()используется для расчета текущего радиуса круга циферблата.
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
radius = (min(width, height) / 2.0 * 0.8).toFloat()
}- Ниже
onSizeChanged()добавьте этот код, чтобы определить функцию расширенияcomputeXYForSpeed()дляPointFКласс. Импортируйтеkotlin.math.cosиkotlin.math.sinпо запросу. Эта функция расширения классаPointFвычисляет координаты X и Y на экране для текстовой метки и текущего индикатора (0, 1, 2 или 3) с учётом текущего положенияFanSpeedи радиуса циферблата. Это будет использоваться в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
}- Переопределите метод
onDraw()для отрисовки изображения на экране с помощью классовCanvasиPaint. Импортируйтеandroid.graphics.Canvasпо запросу. Вот переопределение скелета:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
}- В
onDraw()добавьте эту строку, чтобы установить цвет краски: серый (Color.GRAY) или зелёный (Color.GREEN) в зависимости от того,OFFли скорость вентилятора или установлена на другое значение. Импортируйтеandroid.graphics.Colorпо запросу.
// Set dial background color to green if selection not off.
paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN- Добавьте этот код, чтобы нарисовать круг для циферблата, используя метод
drawCircle(). Этот метод использует текущую ширину и высоту представления для определения центра круга, его радиуса и текущего цвета краски. Свойстваwidthиheightявляются членами суперклассаViewи указывают текущие размеры представления.
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)- Добавьте следующий код, чтобы нарисовать меньший круг для метки индикатора скорости вращения вентилятора, также с помощью метода
drawCircle()В этой части используется метод расширенияPointF.computeXYforSpeed()для вычисления координат X, Y для центра индикатора на основе текущей скорости вращения вентилятора.
// 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)- Наконец, нарисуйте метки скорости вентилятора (0, 1, 2, 3) в соответствующих местах вокруг циферблата. Эта часть метода снова вызывает
PointF.computeXYForSpeed()для определения положения каждой метки и каждый раз повторно использует объектpointPosition, чтобы избежать выделения памяти. Для рисования меток используйте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)
}Завершенный метод onDraw() выглядит так:
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)
}
}Шаг 2. Добавьте представление в макет.
Чтобы добавить пользовательское представление в пользовательский интерфейс приложения, укажите его как элемент в XML-макете активности. Управляйте его внешним видом и поведением с помощью атрибутов XML-элемента, как и для любого другого элемента пользовательского интерфейса.
- В
activity_main.xmlизмените тегImageViewдляdialViewнаcom.example.android.customfancontroller.DialViewи удалите атрибутandroid:background. ИDialView, и исходныйImageViewнаследуют стандартные атрибуты классаView, поэтому нет необходимости изменять какие-либо другие атрибуты. Новый элементDialViewвыглядит следующим образом:
<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-2-3» и обратно в положение «выкл.». Кроме того, если выбрано значение «1» или выше, фон должен измениться с серого на зелёный, указывая на то, что вентилятор включён.
Чтобы сделать ваше пользовательское представление кликабельным, вам необходимо:
- Установите свойство
isClickableпредставления вtrue. Это позволит вашему пользовательскому представлению реагировать на клики. - Реализуйте метод
performClick()классаViewдля выполнения операций при щелчке по представлению. - Вызовите метод
invalidate(). Это даст указание системе Android вызвать методonDraw()для перерисовки представления.
Обычно в стандартном представлении Android вы реализуете OnClickListener() для выполнения действия при щелчке пользователя по этому представлению. Для пользовательского представления вы реализуете метод performClick () класса View и вызываете super . performClick(). Метод performClick() по умолчанию также вызывает onClickListener() , поэтому вы можете добавить свои действия в performClick() и оставить onClickListener() доступным для дальнейшей настройки вами или другими разработчиками, которые могут использовать ваше пользовательское представление.
- В
DialView.kt, внутри перечисленияFanSpeed, добавьте функцию расширенияnext(), которая изменяет текущую скорость вентилятора на следующую скорость в списке (сOFFнаLOW,MEDIUMиHIGH, а затем обратно наOFF). Полный список теперь выглядит следующим образом:
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
}
}- Внутри класса
DialView, непосредственно перед методомonSizeChanged(), добавьте блокinit(). Установка свойстваisClickableпредставления в значение true позволяет этому представлению принимать пользовательский ввод.
init {
isClickable = true
}- Ниже
init(),переопределите методperformClick()с помощью приведенного ниже кода.
override fun performClick(): Boolean {
if (super.performClick()) return true
fanSpeed = fanSpeed.next()
contentDescription = resources.getString(fanSpeed.label)
invalidate()
return true
}Сначала должен произойти вызов super . performClick() , который включает события доступности, а также вызывает onClickListener() .
Следующие две строки увеличивают скорость вентилятора с помощью метода next() и устанавливают описание содержимого представления для строкового ресурса, представляющего текущую скорость (выкл., 1, 2 или 3).
Наконец, метод invalidate() делает всё представление недействительным, вызывая вызов onDraw() для его перерисовки. Если что-то в вашем пользовательском представлении изменилось по какой-либо причине, включая взаимодействие с пользователем, и это изменение необходимо отобразить, вызовите invalidate().
- Запустите приложение. Коснитесь элемента
DialView, чтобы переместить индикатор из положения «выкл.» в положение «1». Индикатор должен стать зелёным. С каждым нажатием индикатор должен переходить на следующую позицию. Когда индикатор возвращается в положение «выкл.», индикатор должен снова стать серым.


В этом примере показана базовая механика использования настраиваемых атрибутов в вашем пользовательском представлении. Вы определяете настраиваемые атрибуты для класса DialView , используя разные цвета для каждой позиции шкалы.
- Создайте и откройте
res/values/attrs.xml. - Внутри
<resources>добавьте элемент ресурса<declare-styleable>. - Внутри элемента ресурса
<declare-styleable>добавьте три элементаattr, по одному для каждого атрибута, сnameиformat.formatпохож на тип, и в данном случае это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>- Откройте файл макета
activity_main.xml. - В
DialViewдобавьте атрибутыfanColor1,fanColor2иfanColor3и задайте для них значения цветов, показанных ниже. Используйтеapp:в качестве префикса для настраиваемого атрибута (напримерapp:fanColor1) вместоandroid:поскольку ваши настраиваемые атрибуты принадлежат пространству имёнschemas.android.com/apk/res/your_app_package_name, а не пространству имёнandroid.
app:fanColor1="#FFEB3B"
app:fanColor2="#CDDC39"
app:fanColor3="#009688"Чтобы использовать атрибуты в классе DialView , их необходимо извлечь. Они хранятся в AttributeSet , который передается вашему классу при создании, если он существует. Атрибуты извлекаются в init , а их значения присваиваются локальным переменным для кэширования.
- Откройте файл класса
DialView.kt. - Внутри
DialViewобъявите переменные для кэширования значений атрибутов.
private var fanSpeedLowColor = 0
private var fanSpeedMediumColor = 0
private var fanSeedMaxColor = 0- В блоке
initдобавьте следующий код, используя функцию расширенияwithStyledAttributes. Вы указываете атрибуты и представление, а также задаёте локальные переменные. ИмпортwithStyledAttributesтакже импортирует нужную функцию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)
}- Используйте локальные переменные в
onDraw()для установки цвета циферблата в зависимости от текущей скорости вращения вентилятора. Заменитеcolor.где задаётся цвет краски (paint=if(fanSpeed== FanSpeed.OFF) Color.elseColor.),GRAYGREENкод.
paint.color = when (fanSpeed) {
FanSpeed.OFF -> Color.GRAY
FanSpeed.LOW -> fanSpeedLowColor
FanSpeed.MEDIUM -> fanSpeedMediumColor
FanSpeed.HIGH -> fanSeedMaxColor
} as Int- Запустите приложение, нажмите на циферблат, и настройки цвета должны быть разными для каждого положения, как показано ниже.
|
|
|
|
Дополнительную информацию о пользовательских атрибутах представления см. в разделе Создание класса представления .
Доступность — это набор методов проектирования, реализации и тестирования, которые позволяют сделать ваше приложение доступным для всех, включая людей с ограниченными возможностями.
К распространённым нарушениям, которые могут повлиять на использование устройства Android, относятся слепота, слабое зрение, дальтонизм, глухота или потеря слуха, а также ограниченные двигательные навыки. Разрабатывая приложения с учётом доступности, вы улучшаете пользовательский опыт не только для пользователей с этими нарушениями, но и для всех остальных пользователей.
Android предоставляет несколько функций доступности по умолчанию в стандартных представлениях пользовательского интерфейса, таких как TextView и Button . Однако при создании пользовательского представления необходимо учитывать, как оно будет предоставлять доступные функции, такие как голосовые описания содержимого на экране.
В этом задании вы узнаете о TalkBack, программе чтения с экрана Android, и измените свое приложение, включив в него голосовые подсказки и описания для пользовательского представления DialView .
Шаг 1. Изучите TalkBack
TalkBack — это встроенная программа экранного доступа Android. С её помощью пользователь может взаимодействовать со своим устройством Android, не видя экрана, поскольку Android описывает его элементы вслух. Пользователи с нарушениями зрения могут использовать TalkBack для работы с вашим приложением.
В этом задании вы включите TalkBack, чтобы понять, как работают программы чтения с экрана и как осуществлять навигацию по приложениям.
- На устройстве Android или эмуляторе перейдите в раздел Настройки > Специальные возможности > TalkBack .
- Нажмите кнопку включения/выключения, чтобы включить TalkBack.
- Нажмите «ОК» , чтобы подтвердить разрешения.
- Подтвердите пароль устройства, если потребуется. Если вы запускаете TalkBack впервые, запустится обучающее руководство. (Обучение может быть недоступно на старых устройствах.)
- Возможно, будет полезно пройти обучение с закрытыми глазами. Чтобы снова открыть обучение, перейдите в раздел «Настройки» > «Универсальный доступ» > «TalkBack» > «Настройки» > «Запустить обучение TalkBack» .
- Скомпилируйте и запустите приложение
CustomFanControllerили откройте его кнопкой «Обзор» или «Недавние» на устройстве. При включённом TalkBack обратите внимание, что озвучивается название приложения, а также текст меткиTextView(«Управление вентилятором»). Однако при нажатии на само представлениеDialViewне озвучивается информация ни о его состоянии (текущих настройках циферблата), ни о действии, которое будет выполнено при нажатии на представление для его активации.
Шаг 2. Добавьте описания содержимого для меток циферблата.
Описания содержимого описывают значение и назначение представлений в вашем приложении. Эти метки позволяют программам чтения с экрана, таким как TalkBack в Android, точно объяснять функцию каждого элемента. Для статических представлений, таких как ImageView , вы можете добавить описание содержимого в представление в файле макета с помощью атрибута contentDescription . Текстовые представления ( TextView и EditText ) автоматически используют текст из представления в качестве описания содержимого.
Для пользовательского представления управления вентилятором вам необходимо динамически обновлять описание содержимого каждый раз при щелчке по представлению, чтобы указывать текущие настройки вентилятора.
- В нижней части класса
DialViewобъявите функциюupdateContentDescription()без аргументов и возвращаемого типа.
fun updateContentDescription() {
}- В методе
updateContentDescription()измените свойствоcontentDescriptionдля пользовательского представления на строковый ресурс, связанный с текущей скоростью вентилятора (выкл., 1, 2 или 3). Это те же метки, которые используются вonDraw()при отображении циферблата на экране.
fun updateContentDescription() {
contentDescription = resources.getString(fanSpeed.label)
}- Прокрутите страницу до блока
init()и в конце этого блока добавьте вызовupdateContentDescription(). Это инициализирует описание контента при инициализации представления.
init {
isClickable = true
// ...
updateContentDescription()
}- Добавьте еще один вызов
updateContentDescription()в методperformClick(), непосредственно передinvalidate().
override fun performClick(): Boolean {
if (super.performClick()) return true
fanSpeed = fanSpeed.next()
updateContentDescription()
invalidate()
return true
}- Скомпилируйте и запустите приложение, убедившись, что TalkBack включён. Нажмите, чтобы изменить настройки отображения циферблата, и обратите внимание, что TalkBack теперь объявляет текущую метку (выкл., 1, 2, 3), а также фразу «Дважды коснитесь для активации».
Шаг 3. Добавьте дополнительную информацию о действии по клику
На этом можно остановиться, и ваше представление можно будет использовать в TalkBack. Но было бы полезно, если бы оно не только указывало на возможность активации («Двойное касание для активации»), но и объясняло, что произойдёт при активации («Двойное касание для изменения» или «Двойное касание для сброса»).
Для этого вы добавляете информацию о действии представления (в данном случае о щелчке или касании) в информационный объект узла доступности с помощью делегата доступности. Делегат доступности позволяет настраивать функции доступности вашего приложения посредством композиции (а не наследования).
Для этой задачи вы будете использовать классы доступности в библиотеках Android Jetpack ( androidx.* ) для обеспечения обратной совместимости.
- В
DialView.kt, в блокеinit, установите делегат доступности для представления как новый объектAccessibilityDelegateCompat. Импортируйтеandroidx.core.view.ViewCompatиandroidx.core.view.AccessibilityDelegateCompatпо запросу. Эта стратегия обеспечивает максимальную обратную совместимость в вашем приложении.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
})- Внутри объекта
AccessibilityDelegateCompatпереопределите функциюonInitializeAccessibilityNodeInfo()объектомAccessibilityNodeInfoCompatи вызовите метод суперкласса. Импортируйтеandroidx.core.view.accessibility.AccessibilityNodeInfoCompatпри появлении соответствующего запроса.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View,
info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
}
})Каждое представление имеет дерево узлов доступности, которые могут соответствовать или не соответствовать фактическим компонентам макета представления. Службы доступности Android перемещаются по этим узлам, чтобы получить информацию о представлении (например, описания озвучиваемого содержимого или возможные действия, которые можно выполнить с этим представлением). При создании пользовательского представления может также потребоваться переопределить информацию об узле, чтобы предоставить пользовательскую информацию для обеспечения доступности. В этом случае вы переопределяете информацию об узле, чтобы указать, что для действия представления имеется пользовательская информация.
- Внутри
onInitializeAccessibilityNodeInfo()создайте новый объектAccessibilityNodeInfoCompat.AccessibilityActionCompatи присвойте его переменнойcustomClick. Передайте в конструктор константуAccessibilityNodeInfo.ACTION_CLICKи строку-заполнитель. Импортируйте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"
)
}
})Класс AccessibilityActionCompat представляет действие над представлением для обеспечения доступности. Типичным действием является щелчок или касание, как в данном случае, но могут также использоваться другие действия, включая получение или потерю фокуса, операции с буфером обмена (вырезать/копировать/вставить) или прокрутку внутри представления. Конструктор этого класса требует константу действия (в данном случае AccessibilityNodeInfo.ACTION_CLICK ) и строку, которая используется TalkBack для указания действия.
- Замените строку
"placeholder"вызовомcontext.getString()для получения строкового ресурса. Для конкретного ресурса проверьте текущую скорость вентилятора. Если текущая скорость равнаFanSpeed.HIGH, строка будет"Reset". Если скорость вентилятора другая, строка будет"Change."Эти строковые ресурсы будут созданы позже.
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)
)
}
})- После закрывающих скобок для определения
customClickиспользуйте методaddAction(), чтобы добавить новое действие доступности к объекту информации об узле.
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)
}
})- В
res/values/strings.xmlдобавьте строковые ресурсы для «Изменение» и «Сброс».
<string name="change">Change</string>
<string name="reset">Reset</string>- Скомпилируйте и запустите приложение, убедившись, что TalkBack включён. Обратите внимание, что фраза «Дважды коснитесь для активации» теперь отображается как «Дважды коснитесь для изменения» (если скорость вентилятора ниже высокой или 3) или «Дважды коснитесь для сброса» (если скорость вентилятора уже высокая или 3). Обратите внимание, что подсказка «Дважды коснитесь для...» предоставляется самой службой TalkBack.
Загрузите код для готовой лабораторной работы.
$ git clone https://github.com/googlecodelabs/android-kotlin-drawing-custom-views
Кроме того, вы можете загрузить репозиторий в виде ZIP-файла, распаковать его и открыть в Android Studio.
- Чтобы создать пользовательское представление, которое наследует внешний вид и поведение подкласса
View, напримерEditText, добавьте новый класс, расширяющий этот подкласс, и внесите изменения, переопределив некоторые методы подкласса. - Чтобы создать пользовательское представление любого размера и формы, добавьте новый класс, расширяющий
View. - Переопределите методы
View, такие какonDraw(), чтобы определить форму и базовый внешний вид представления. - Используйте
invalidate()для принудительной отрисовки или перерисовки представления. - Чтобы оптимизировать производительность, выделите переменные и назначьте все необходимые значения для рисования и заливки перед их использованием в
onDraw(), например, при инициализации переменных-членов. - Переопределите
performClick()вместоOnClickListener() в пользовательском представлении, чтобы обеспечить интерактивное поведение представления. Это позволит вам и другим разработчикам Android, которые могут использовать ваш класс пользовательского представления, использоватьonClickListener()для реализации дополнительного поведения. - Добавьте пользовательское представление в XML-файл макета с атрибутами, определяющими его внешний вид, как это делается с другими элементами пользовательского интерфейса.
- Создайте файл
attrs.xmlв папкеvalues, чтобы определить настраиваемые атрибуты. Затем вы сможете использовать эти атрибуты для настраиваемого представления в XML-файле макета.
Курс Udacity:
Документация для разработчиков Android:
- Создание пользовательских представлений
-
@JvmOverloads - Пользовательские компоненты
- Как Android рисует представления
-
onMeasure() -
onSizeChanged() -
onDraw() -
Canvas -
Paint -
drawText() -
setTypeface() -
setColor() -
drawRect() -
drawOval() -
drawArc() -
drawBitmap() -
setStyle() -
invalidate() - Вид
- Входные события
- Краска
- Библиотека расширения Kotlin android-ktx
-
withStyledAttributes - Документация Android KTX
- Оригинальный блог анонса Android KTX
- Сделайте пользовательские представления более доступными
-
AccessibilityDelegateCompat -
AccessibilityNodeInfoCompat -
AccessibilityNodeInfoCompat.AccessibilityActionCompat
Видео:
В этом разделе перечислены возможные домашние задания для студентов, работающих над этой лабораторной работой в рамках курса, проводимого преподавателем. Преподаватель должен выполнить следующие действия:
- При необходимости задавайте домашнее задание.
- Объясните учащимся, как следует сдавать домашние задания.
- Оцените домашние задания.
Преподаватели могут использовать эти предложения так часто или редко, как пожелают, и могут свободно задавать любые другие домашние задания, которые они сочтут подходящими.
Если вы работаете с этой лабораторной работой самостоятельно, можете использовать эти домашние задания для проверки своих знаний.
Вопрос 1
Какой метод следует переопределить, чтобы рассчитать позиции, размеры и любые другие значения, когда пользовательскому представлению впервые назначается размер?
▢ onMeasure()
▢ onSizeChanged()
▢ invalidate()
▢ onDraw()
Вопрос 2
Какой метод следует вызвать из потока пользовательского интерфейса, чтобы указать, что вы хотите перерисовать свое представление с помощью onDraw() , после изменения значения атрибута?
▢ onMeasure()
▢ onSizeChanged()
▢ invalidate()
▢ getVisibility()
Вопрос 3
Какой метод View следует переопределить, чтобы добавить интерактивности в ваше пользовательское представление?
▢ setOnClickListener()
▢ onSizeChanged()
▢ isClickable()
▢ performClick()
Ссылки на другие практические занятия по этому курсу см. на целевой странице практических занятий по курсу Advanced Android in Kotlin.



