Создание пользовательских представлений

Эта кодовая лаборатория является частью курса 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

  1. Создайте приложение Kotlin с названием CustomFanController используя шаблон Empty Activity. Убедитесь, что имя пакета com.example.android.customfancontroller .
  2. Откройте файл activity_main.xml на вкладке « Текст », чтобы отредактировать код XML.
  3. Замените существующий 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"/>
  1. Добавьте этот элемент 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"/>
  1. Извлеките строковые и размерные ресурсы в обоих элементах пользовательского интерфейса.
  2. Перейдите на вкладку « Дизайн ». Макет должен выглядеть следующим образом:

Шаг 2. Создайте свой собственный класс представления

  1. Создайте новый класс Kotlin с именем DialView .
  2. Измените определение класса, чтобы расширить View . Импортируйте android.view.View при появлении запроса.
  3. Нажмите « 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) {
  1. Над определением класса 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);
}
  1. Ниже enum добавьте эти константы. Вы будете использовать их как часть рисования циферблатных индикаторов и меток.
private const val RADIUS_OFFSET_LABEL = 30      
private const val RADIUS_OFFSET_INDICATOR = -35
  1. Внутри класса 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 , которая будет использоваться для рисования нескольких элементов представления на экране.

Эти значения создаются и инициализируются здесь, а не при фактическом отрисовке вида, чтобы обеспечить максимально быстрое выполнение фактического шага рисования.

  1. Также внутри определения класса 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)
}
  1. Откройте 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. Рассчитать позиции и нарисовать вид

  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()
}
  1. Ниже 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
}
  1. Переопределите метод onDraw() для рендеринга представления на экране с помощью классов Canvas и Paint . Импортируйте android.graphics.Canvas по запросу. Это переопределение скелета:
override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   
}
  1. Внутри 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
  1. Добавьте этот код, чтобы нарисовать круг для циферблата с помощью drawCircle() . Этот метод использует текущую ширину и высоту вида, чтобы найти центр круга, радиус круга и текущий цвет краски. Свойства width и height являются членами суперкласса View и указывают текущие размеры представления.
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
  1. Добавьте следующий код, чтобы нарисовать меньший круг для метки индикатора скорости вращения вентилятора, а также с помощью 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)
  1. Наконец, нарисуйте метки скорости вращения вентилятора (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, как и для любого другого элемента пользовательского интерфейса.

  1. В файле 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. Запустите приложение. Представление управления вентилятором отображается в действии.

Последняя задача — разрешить вашему пользовательскому представлению выполнять действие, когда пользователь нажимает на представление. Каждое нажатие должно перемещать индикатор выбора в следующую позицию: выкл-1-2-3 и обратно в выкл. Кроме того, если выбрано значение 1 или выше, измените фон с серого на зеленый, указывая на то, что питание вентилятора включено.

Чтобы ваш пользовательский вид был кликабельным, вы:

  • Задайте для свойства isClickable представления значение true . Это позволяет вашему пользовательскому представлению реагировать на клики.
  • performClick () класса View для выполнения операций при нажатии на представление.
  • Вызовите метод invalidate() . Это говорит системе Android вызвать метод onDraw() для перерисовки представления.

Обычно в стандартном представлении Android вы реализуете OnClickListener() для выполнения действия, когда пользователь щелкает это представление. Для пользовательского представления вместо этого вы реализуете метод performClick () класса View и вызываете super . performClick(). Метод performClick() по умолчанию также вызывает onClickListener() , поэтому вы можете добавить свои действия в performClick() и оставить onClickListener() доступным для дальнейшей настройки вами или другими разработчиками, которые могут использовать ваше пользовательское представление.

  1. В 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
   }
}
  1. Внутри класса DialView , непосредственно перед onSizeChanged() , добавьте блок init() . Установка для свойства isClickable представления значения true позволяет этому представлению принимать вводимые пользователем данные.
init {
   isClickable = true
}
  1. Ниже 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().

  1. Запустите приложение. Коснитесь элемента DialView , чтобы переместить индикатор с выключенного на 1. Циферблат должен стать зеленым. При каждом нажатии индикатор должен переходить на следующую позицию. Когда индикатор вернется в выключенное состояние, циферблат снова станет серым.

В этом примере показана основная механика использования настраиваемых атрибутов в пользовательском представлении. Вы определяете настраиваемые атрибуты для класса DialView с другим цветом для каждой позиции веерного циферблата.

  1. Создайте и откройте res/values/attrs.xml .
  2. Внутри <resources> добавьте ресурсный элемент <declare-styleable> .
  3. Внутри элемента ресурса <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>
  1. Откройте файл макета activity_main.xml .
  2. В 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 и присваиваете значения атрибутов локальным переменным для кэширования.

  1. Откройте файл класса DialView.kt .
  2. Внутри DialView объявите переменные для кэширования значений атрибутов.
private var fanSpeedLowColor = 0
private var fanSpeedMediumColor = 0
private var fanSeedMaxColor = 0
  1. В блоке 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)
}
  1. Используйте локальные переменные в 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
  1. Запустите приложение, нажмите на циферблат, и настройки цвета должны быть разными для каждой позиции, как показано ниже.

Дополнительные сведения о настраиваемых атрибутах представления см. в разделе Создание класса представления .

Доступность — это набор методов проектирования, реализации и тестирования, которые позволяют использовать ваше приложение для всех, включая людей с ограниченными возможностями.

Общие нарушения, которые могут повлиять на использование человеком устройства Android, включают слепоту, слабое зрение, цветовую слепоту, глухоту или потерю слуха, а также ограниченные двигательные навыки. Когда вы разрабатываете свои приложения с учетом специальных возможностей, вы улучшаете взаимодействие с пользователем не только для пользователей с такими ограниченными возможностями, но и для всех других пользователей.

Android по умолчанию предоставляет несколько специальных возможностей в стандартных представлениях пользовательского интерфейса, таких как TextView и Button . Однако при создании пользовательского представления необходимо учитывать, как это пользовательское представление будет предоставлять доступные функции, такие как голосовые описания содержимого на экране.

В этой задаче вы узнаете о TalkBack, средстве чтения с экрана Android, и измените свое приложение, чтобы включить голосовые подсказки и описания для пользовательского представления DialView .

Шаг 1. Изучите TalkBack

TalkBack — это встроенное в Android средство чтения с экрана. При включенном TalkBack пользователь может взаимодействовать со своим Android-устройством, не видя экрана, поскольку Android вслух описывает элементы экрана. Пользователи с нарушениями зрения могут полагаться на TalkBack при использовании вашего приложения.

В этой задаче вы включаете TalkBack, чтобы понять, как работают программы чтения с экрана и как перемещаться по приложениям.

  1. На устройстве Android или в эмуляторе перейдите в « Настройки» > «Универсальный доступ» > «TalkBack» .
  2. Коснитесь переключателя « Вкл./Выкл .», чтобы включить TalkBack.
  3. Нажмите OK , чтобы подтвердить разрешения.
  4. Подтвердите пароль устройства, если потребуется. Если вы впервые запускаете TalkBack, запускается обучающее руководство. (Учебник может быть недоступен на старых устройствах.)
  5. Может быть полезно перемещаться по учебнику с закрытыми глазами. Чтобы снова открыть учебник в будущем, выберите « Настройки» > «Универсальный доступ» > «TalkBack» > «Настройки» > «Запустить учебник TalkBack» .
  6. Скомпилируйте и запустите приложение CustomFanController или откройте его с помощью кнопки « Обзор » или « Недавние » на своем устройстве. Обратите внимание, что при включенном TalkBack объявляется название приложения, а также текст метки TextView («Управление вентилятором»). Однако, если вы нажмете на само представление DialView , не будет сказано никакой информации ни о состоянии представления (текущая настройка для циферблата), ни о действии, которое будет выполнено, когда вы коснетесь представления, чтобы активировать его.

Шаг 2. Добавьте описания содержимого для ярлыков циферблата

Описания содержимого описывают значение и назначение представлений в вашем приложении. Эти метки позволяют программам чтения с экрана, таким как функция Android TalkBack, точно объяснять функцию каждого элемента. Для статических представлений, таких как ImageView , вы можете добавить описание содержимого к представлению в файле макета с помощью атрибута contentDescription . Текстовые представления ( TextView и EditText ) автоматически используют текст в представлении в качестве описания содержимого.

Для пользовательского представления управления вентилятором вам необходимо динамически обновлять описание содержимого при каждом щелчке представления, чтобы указать текущую настройку вентилятора.

  1. В нижней части класса DialView объявите функцию updateContentDescription() без аргументов или типа возвращаемого значения.
fun updateContentDescription() {
}
  1. Внутри updateContentDescription() измените свойство contentDescription для пользовательского представления на строковый ресурс, связанный с текущей скоростью вращения вентилятора (выкл., 1, 2 или 3). Это те же метки, которые используются в onDraw() , когда циферблат рисуется на экране.
fun updateContentDescription() {
   contentDescription = resources.getString(fanSpeed.label)
}
  1. Прокрутите вверх до блока init() и в конце этого блока добавьте вызов updateContentDescription() . Это инициализирует описание содержимого при инициализации представления.
init {
   isClickable = true
   // ...

   updateContentDescription()
}
  1. Добавьте еще один вызов updateContentDescription() в performClick() непосредственно перед invalidate() .
override fun performClick(): Boolean {
   if (super.performClick()) return true
   fanSpeed = fanSpeed.next()
   updateContentDescription()
   invalidate()
   return true
}
  1. Скомпилируйте и запустите приложение, а также убедитесь, что TalkBack включен. Коснитесь, чтобы изменить настройку представления набора номера, и обратите внимание, что теперь TalkBack объявляет текущую метку (выкл., 1, 2, 3), а также фразу «Дважды коснитесь для активации».

Шаг 3. Добавьте дополнительную информацию для действия клика

Вы можете остановиться на этом, и ваше представление можно будет использовать в TalkBack. Но было бы полезно, если бы ваш вид мог указывать не только на то, что его можно активировать («Двойное касание для активации»), но также объяснять, что произойдет , когда представление активируется («Дважды коснитесь, чтобы изменить» или «Дважды нажмите, чтобы изменить». -нажмите, чтобы сбросить.")

Для этого вы добавляете информацию о действии представления (здесь — действие щелчка или касания) в информационный объект узла доступности посредством делегата доступности. Делегат специальных возможностей позволяет настраивать функции вашего приложения, связанные со специальными возможностями, посредством композиции (а не наследования).

Для этой задачи вы будете использовать классы специальных возможностей в библиотеках Android Jetpack ( androidx.* ), чтобы обеспечить обратную совместимость.

  1. В DialView.kt в блоке init установите делегат доступности в представлении как новый объект AccessibilityDelegateCompat . Импортируйте androidx.core.view.ViewCompat и androidx.core.view.AccessibilityDelegateCompat по запросу. Эта стратегия обеспечивает наибольшую обратную совместимость в вашем приложении.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   
})
  1. Внутри объекта 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 перемещаются по этим узлам, чтобы получить информацию о представлении (например, описания озвучиваемого содержимого или возможные действия, которые можно выполнять в этом представлении). чтобы предоставить пользовательскую информацию для доступности. В этом случае вы будете переопределять информацию об узле, чтобы указать, что есть пользовательская информация для действия представления.

  1. Внутри 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 для указания действия.

  1. Замените строку "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)
      )
   }  
})
  1. После закрытия скобок для определения 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)
   }
})
  1. В res/values/strings.xml добавьте строковые ресурсы для «Изменить» и «Сбросить».
<string name="change">Change</string>
<string name="reset">Reset</string>
  1. Скомпилируйте и запустите приложение и убедитесь, что TalkBack включен. Обратите внимание, что фраза «Двойное нажатие для активации» теперь звучит как «Двойное нажатие для изменения» (если скорость вентилятора ниже высокой или равной 3) или «Двойное нажатие для сброса» (если скорость вентилятора уже равна высокий или 3). Обратите внимание, что подсказка «Дважды нажмите, чтобы…» предоставляется самой службой TalkBack.

Скачайте код для готовой кодлабы..

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-custom-views


Кроме того, вы можете загрузить репозиторий в виде Zip-файла, разархивировать его и открыть в Android Studio.

Скачать ZIP

  • Чтобы создать пользовательское представление, наследующее внешний вид и поведение подкласса View , такого как EditText , добавьте новый класс, расширяющий этот подкласс, и внесите изменения, переопределив некоторые методы подкласса.
  • Чтобы создать собственное представление любого размера и формы, добавьте новый класс, расширяющий View .
  • Переопределите методы View , такие как onDraw() , чтобы определить форму представления и основной внешний вид.
  • Используйте invalidate() , чтобы принудительно отрисовать или перерисовать представление.
  • Чтобы оптимизировать производительность, выделите переменные и назначьте все необходимые значения для рисования и рисования перед их использованием в onDraw() , например, при инициализации переменных-членов.
  • Переопределите performClick() , а не OnClickListener () для пользовательского представления, чтобы обеспечить интерактивное поведение представления. Это позволяет вашим или другим разработчикам Android, которые могут использовать ваш собственный класс представления, использовать onClickListener() для обеспечения дальнейшего поведения.
  • Добавьте пользовательское представление в файл макета XML с атрибутами, чтобы определить его внешний вид, как и в случае с другими элементами пользовательского интерфейса.
  • Создайте файл attrs.xml в папке values , чтобы определить настраиваемые атрибуты. Затем вы можете использовать настраиваемые атрибуты для пользовательского представления в файле макета XML.

Удасити курс:

Документация для разработчиков Android:

Видео:

В этом разделе перечислены возможные домашние задания для студентов, которые работают с этой кодовой лабораторией в рамках курса, проводимого инструктором. Инструктор должен сделать следующее:

  • При необходимости задайте домашнее задание.
  • Объясните учащимся, как сдавать домашние задания.
  • Оценивайте домашние задания.

Преподаватели могут использовать эти предложения так мало или так часто, как они хотят, и должны свободно давать любые другие домашние задания, которые они считают подходящими.

Если вы работаете с этой кодовой лабораторией самостоятельно, не стесняйтесь использовать эти домашние задания, чтобы проверить свои знания.

Вопрос 1

Чтобы вычислить позиции, размеры и любые другие значения, когда пользовательскому представлению впервые назначается размер, какой метод вы переопределяете?

onMeasure()

onSizeChanged()

invalidate()

▢ при onDraw()

вопрос 2

Чтобы указать, что вы хотите, чтобы ваше представление было перерисовано с помощью onDraw() , какой метод вы вызываете из потока пользовательского интерфейса после изменения значения атрибута?

▢ onMeasure()

▢ onSizeChanged()

▢ invalidate()

▢ getVisibility()

Вопрос 3

Какой метод View следует переопределить, чтобы добавить интерактивности в пользовательское представление?

▢ setOnClickListener()

▢ onSizeChanged()

▢ isClickable()

▢ performClick()

Ссылки на другие лаборатории кода в этом курсе см. на целевой странице Advanced Android in Kotlin codelabs.