Эта кодовая лаборатория является частью курса Advanced Android in Kotlin. Вы получите максимальную отдачу от этого курса, если будете последовательно работать с лабораториями кода, но это не обязательно. Все кодовые лаборатории курса перечислены на целевой странице Advanced Android in Kotlin codelabs .
Введение
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
предоставляет множество подклассов, называемых виджетами пользовательского интерфейса , которые охватывают многие потребности пользовательского интерфейса типичного приложения 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
текущая скорость вентилятора , которая является одним из значений в перечислении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()
из классаView
, чтобы вычислить размер циферблата пользовательского представления. Импортkotlin
.math.min
по запросу.
Метод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
.color.
if
(
fanSpeed
=
== FanSpeed.
OFF
) Color.
GRAY
else
Color.
GREEN
) кодом ниже.
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 средство чтения с экрана. При включенном TalkBack пользователь может взаимодействовать со своим Android-устройством, не видя экрана, поскольку Android вслух описывает элементы экрана. Пользователи с нарушениями зрения могут полагаться на TalkBack при использовании вашего приложения.
В этой задаче вы включаете TalkBack, чтобы понять, как работают программы чтения с экрана и как перемещаться по приложениям.
- На устройстве Android или в эмуляторе перейдите в « Настройки» > «Универсальный доступ» > «TalkBack» .
- Коснитесь переключателя « Вкл./Выкл .», чтобы включить TalkBack.
- Нажмите OK , чтобы подтвердить разрешения.
- Подтвердите пароль устройства, если потребуется. Если вы впервые запускаете TalkBack, запускается обучающее руководство. (Учебник может быть недоступен на старых устройствах.)
- Может быть полезно перемещаться по учебнику с закрытыми глазами. Чтобы снова открыть учебник в будущем, выберите « Настройки» > «Универсальный доступ» > «TalkBack» > «Настройки» > «Запустить учебник TalkBack» .
- Скомпилируйте и запустите приложение
CustomFanController
или откройте его с помощью кнопки « Обзор » или « Недавние » на своем устройстве. Обратите внимание, что при включенном TalkBack объявляется название приложения, а также текст меткиTextView
(«Управление вентилятором»). Однако, если вы нажмете на само представлениеDialView
, не будет сказано никакой информации ни о состоянии представления (текущая настройка для циферблата), ни о действии, которое будет выполнено, когда вы коснетесь представления, чтобы активировать его.
Шаг 2. Добавьте описания содержимого для ярлыков циферблата
Описания содержимого описывают значение и назначение представлений в вашем приложении. Эти метки позволяют программам чтения с экрана, таким как функция Android TalkBack, точно объяснять функцию каждого элемента. Для статических представлений, таких как 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
и вызовите метод super. Импортируйте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.
Удасити курс:
Документация для разработчиков 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 codelabs.