Эта практическая работа входит в курс «Advanced Android in Kotlin». Вы получите максимальную пользу от этого курса, выполняя задания последовательно, но это не обязательно. Все практическая работа курса перечислены на целевой странице практической работы «Advanced Android in Kotlin» .
Введение
В Android доступно несколько методов реализации пользовательской 2D-графики и анимации в представлениях.
Помимо использования объектов drawable , вы можете создавать двухмерные рисунки, используя методы рисования класса Canvas . Canvas — это двухмерная поверхность для рисования, предоставляющая методы для рисования. Это полезно, когда вашему приложению требуется регулярно перерисовываться, поскольку то, что видит пользователь, со временем меняется. В этой лабораторной работе вы научитесь создавать и рисовать на холсте, отображаемом в View .
На холсте можно выполнять следующие типы операций:
- Заполните весь холст цветом.
- Рисуйте фигуры, такие как прямоугольники, дуги и контуры, стилизованные под объект
Paint. ОбъектPaintсодержит информацию о стиле и цвете, необходимую для рисования геометрических фигур (таких как линии, прямоугольники, овалы и контуры), или, например, шрифта текста. - Применяйте преобразования, такие как перемещение, масштабирование или пользовательские преобразования.
- Обрезать, то есть применить форму или контур к холсту, чтобы определить его видимые части.

Как можно представить себе рисование на Android (очень упрощенно!)
Рисование в Android или любой другой современной системе — сложный процесс, включающий множество уровней абстракции и оптимизацию вплоть до аппаратного обеспечения. То, как рисует Android, — увлекательная тема, о которой много написано, и её подробности выходят за рамки этой практической работы.
В контексте этой лабораторной работы и ее приложения, которое рисует на холсте для отображения в полноэкранном режиме, вы можете представить это следующим образом.

- Вам необходимо представление для отображения рисуемого вами объекта. Это может быть одно из представлений, предоставляемых системой Android. Или, в этой лабораторной работе, вы создадите собственное представление, которое будет служить представлением контента для вашего приложения (
MyCanvasView). - Это представление, как и все представления, имеет свой собственный холст (
canvas). - Самый простой способ рисования на холсте представления — переопределить его метод
onDraw()и рисовать на его холсте. - При создании чертежа необходимо кэшировать уже нарисованные объекты. Существует несколько способов кэширования данных: один из них — в растровом изображении (
extraBitmap), а другой — сохранить историю нарисованных объектов в виде координат и инструкций. - Чтобы рисовать на кэшируемом растровом изображении (
extraBitmap) с помощью API рисования на холсте, необходимо создать кэширующий холст (extraCanvas) для кэшируемого растрового изображения. - Затем вы рисуете на кэшированном холсте (
extraCanvas), который рисует на кэшированном растровом изображении (extraBitmap). - Чтобы отобразить все, что нарисовано на экране, вы указываете холсту представления (
canvas) нарисовать кэшированное растровое изображение (extraBitmap).
Что вам уже следует знать
- Как создать приложение с Activity, базовым макетом и запустить его с помощью Android Studio.
- Как связать обработчики событий с представлениями.
- Как создать пользовательское представление.
Чему вы научитесь
- Как создать
Canvasи рисовать на нем в ответ на прикосновения пользователя.
Что ты будешь делать?
- Создайте приложение, которое рисует линии на экране в ответ на прикосновение пользователя к экрану.
- Фиксируйте события движения и в ответ рисуйте линии на холсте, который отображается в полноэкранном пользовательском представлении на экране.
Приложение MiniPaint использует настраиваемый вид для отображения линии в ответ на прикосновения пользователя, как показано на снимке экрана ниже.

Шаг 1. Создайте проект MiniPaint
- Создайте новый проект Kotlin под названием MiniPaint , использующий шаблон Empty Activity .
- Откройте файл
app/res/values/colors.xmlи добавьте следующие два цвета.
<color name="colorBackground">#FFFF5500</color>
<color name="colorPaint">#FFFFEB3B</color>- Открыть
styles.xml - В родительском стиле
AppThemeзаменитеDarkActionBarнаNoActionBar. Это удалит панель действий, чтобы можно было отрисовывать элементы в полноэкранном режиме.
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">Шаг 2. Создайте класс MyCanvasView
На этом этапе вы создаете пользовательское представление MyCanvasView для рисования.
- В пакете
app/java/com.example.android.minipaintсоздайте новый > файл/класс Kotlin с именемMyCanvasView. - Создайте класс
MyCanvasView, расширяющий классView, и передайте емуcontext: Context. Примите предложенные импорты.
import android.content.Context
import android.view.View
class MyCanvasView(context: Context) : View(context) {
}Шаг 3. Установите MyCanvasView в качестве представления содержимого.
Чтобы отобразить то, что вы будете рисовать в MyCanvasView , вам необходимо установить его как представление содержимого MainActivity .
- Откройте
strings.xmlи определите строку, которая будет использоваться для описания содержимого представления.
<string name="canvasContentDescription">Mini Paint is a simple line drawing app.
Drag your fingers to draw. Rotate the phone to clear.</string>- Открыть
MainActivity.kt - В
onCreate()удалитеsetContentView(R.layout.activity_main). - Создайте экземпляр
MyCanvasView.
val myCanvasView = MyCanvasView(this)- Ниже запросите полный экран для макета
myCanvasView. Для этого установите флагSYSTEM_UI_FLAG_FULLSCREENдляmyCanvasView. Таким образом, представление полностью заполнит экран.
myCanvasView.systemUiVisibility = SYSTEM_UI_FLAG_FULLSCREEN- Добавьте описание контента.
myCanvasView.contentDescription = getString(R.string.canvasContentDescription)- Ниже установите для вида содержимого значение
myCanvasView.
setContentView(myCanvasView)- Запустите приложение. Вы увидите совершенно белый экран, поскольку холст не имеет размера, и вы ещё ничего не нарисовали.
Шаг 1. Переопределить onSizeChanged()
Метод onSizeChanged() вызывается системой Android при каждом изменении размера представления. Поскольку представление изначально не имеет размера, его метод onSizeChanged() также вызывается после того, как Activity впервые создаст и расширит его. Таким образом, метод onSizeChanged() идеально подходит для создания и настройки холста представления.
- В
MyCanvasViewна уровне класса определите переменные для холста и растрового изображения. Назовите ихextraCanvasиextraBitmap. Это растровое изображение и холст для кэширования ранее отрисованных объектов.
private lateinit var extraCanvas: Canvas
private lateinit var extraBitmap: Bitmap- Определите переменную уровня класса
backgroundColorдля цвета фона холста и инициализируйте ее значениемcolorBackgroundкоторое вы определили ранее.
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)- В
MyCanvasViewпереопределите методonSizeChanged(). Этот метод обратного вызова вызывается системой Android с изменёнными размерами экрана, то есть с новыми шириной и высотой (к которым нужно перейти) и старыми шириной и высотой (к которым нужно перейти).
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
super.onSizeChanged(width, height, oldWidth, oldHeight)
}- Внутри
onSizeChanged()создайте экземплярBitmapс новыми шириной и высотой, соответствующими размеру экрана, и присвойте егоextraBitmap. Третий аргумент — это цветовая конфигурация растрового изображения . Рекомендуется использоватьARGB_8888который хранит каждый цвет в 4 байтах.
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)- Создайте экземпляр
CanvasизextraBitmapи назначьте егоextraCanvas.
extraCanvas = Canvas(extraBitmap)- Укажите цвет фона, которым будет заполнен
extraCanvas.
extraCanvas.drawColor(backgroundColor)- Если посмотреть на
onSizeChanged(), то можно увидеть, что при каждом выполнении функции создаются новые растровое изображение и холст. Вам нужно новое растровое изображение, поскольку размер изменился. Однако это утечка памяти, и старые растровые изображения остаются. Чтобы исправить это, очиститеextraBitmapперед созданием следующего, добавив этот код сразу после вызоваsuper.
if (::extraBitmap.isInitialized) extraBitmap.recycle()Шаг 2. Переопределить onDraw()
Вся работа по рисованию для MyCanvasView происходит в onDraw() .
Для начала отобразите холст, заполнив экран цветом фона, который вы установили в onSizeChanged() .
- Переопределите
onDraw()и отобразите содержимое кэшированногоextraBitmapна холсте, связанном с представлением. МетодdrawBitmap()Canvasсуществует в нескольких версиях. В этом коде вы указываете растровое изображение, координаты x и y (в пикселях) верхнего левого угла иnullдляPaint, которое вы установите позже.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
}
Обратите внимание, что холст, который передается в onDraw() и используется системой для отображения растрового изображения, отличается от того, который вы создали в методе onSizeChanged() и использовали для рисования на растровом изображении.
- Запустите приложение. Вы увидите весь экран, залитый указанным фоновым цветом.

Для рисования вам нужен объект Paint , который определяет, как будут выглядеть объекты при рисовании, и Path , который определяет, что именно будет отрисовано.
Шаг 1. Инициализируем объект Paint.
- В файле MyCanvasView.kt на верхнем уровне определите константу для ширины обводки.
private const val STROKE_WIDTH = 12f // has to be float- На уровне класса
MyCanvasViewопределите переменнуюdrawColorдля хранения цвета, которым нужно рисовать, и инициализируйте ее с помощью ресурсаcolorPaintкоторый вы определили ранее.
private val drawColor = ResourcesCompat.getColor(resources, R.color.colorPaint, null)- На уровне класса ниже добавьте переменную
paintдля объектаPaintи инициализируйте ее следующим образом.
// Set up the paint with which to draw.
private val paint = Paint().apply {
color = drawColor
// Smooths out edges of what is drawn without affecting shape.
isAntiAlias = true
// Dithering affects how colors with higher-precision than the device are down-sampled.
isDither = true
style = Paint.Style.STROKE // default: FILL
strokeJoin = Paint.Join.ROUND // default: MITER
strokeCap = Paint.Cap.ROUND // default: BUTT
strokeWidth = STROKE_WIDTH // default: Hairline-width (really thin)
}colorpaint— этоdrawColorвы определили ранее.-
isAntiAliasопределяет, следует ли применять сглаживание краёв. УстановкаisAntiAliasв значениеtrueсглаживает края отрисовываемого объекта, не влияя на его форму. - Если
isDitherвtrue, он влияет на то, как происходит понижение разрешения цветов с более высокой точностью, чем у устройства. Например, дизеринг — наиболее распространённый способ сужения цветового диапазона изображений до 256 цветов (или меньше). -
styleопределяет тип рисования: обводка, которая по сути является линией.Paint.Styleопределяет, будет ли рисуемый примитив залит, обведен или и то, и другое (одним цветом). По умолчанию заполняется объект, к которому применяется краска. («Заливка» закрашивает внутреннюю часть фигуры, а «обводка» следует её контуру.) -
strokeJoinобъектаPaint.Joinопределяет, как соединяются линии и сегменты кривых на контуре. Значение по умолчанию —MITER. -
strokeCapзадаёт форму окончания линии.Paint.Capопределяет начало и конец обводимых линий и контуров. Значение по умолчанию —BUTT. -
strokeWidthзадаёт ширину обводки в пикселях. Значение по умолчанию — толщина линии, которая очень тонкая, поэтому оно задаётся константойSTROKE_WIDTHкоторую вы определили ранее.
Шаг 2. Инициализация объекта Path
Path — это путь того, что рисует пользователь.
- В
MyCanvasViewдобавьте переменнуюpathи инициализируйте её объектомPathдля хранения пути, который рисуется при касании экрана пользователем. Импортируйтеandroid.graphics.PathдляPath.
private var path = Path()Шаг 1. Реагируйте на движение на дисплее.
Метод onTouchEvent() представления вызывается всякий раз, когда пользователь прикасается к дисплею.
- В
MyCanvasViewпереопределите методonTouchEvent()для кэширования координатxиyпереданногоevent. Затем используйте выражениеwhenдля обработки событий касания экрана, перемещения по экрану и отпускания касания. Эти события представляют интерес для рисования линии на экране. Для каждого типа события вызовите вспомогательный метод, как показано в коде ниже. Полный список событий касания см. в документации по классуMotionEvent.
override fun onTouchEvent(event: MotionEvent): Boolean {
motionTouchEventX = event.x
motionTouchEventY = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> touchStart()
MotionEvent.ACTION_MOVE -> touchMove()
MotionEvent.ACTION_UP -> touchUp()
}
return true
}- На уровне класса добавьте отсутствующие переменные
motionTouchEventXиmotionTouchEventYдля кэширования координат x и y текущего события касания (координатMotionEvent). Инициализируйте их значением0f.
private var motionTouchEventX = 0f
private var motionTouchEventY = 0f- Создайте заглушки для трех функций
touchStart(),touchMove()иtouchUp().
private fun touchStart() {}
private fun touchMove() {}
private fun touchUp() {}- Ваш код должен скомпилироваться и запуститься, но пока вы не увидите никаких изменений, кроме цветного фона.
Шаг 2. Реализуйте touchStart()
Этот метод вызывается, когда пользователь впервые касается экрана.
- На уровне класса добавьте переменные для кэширования последних значений X и Y. После того, как пользователь прекращает движение и отрывает палец, эти переменные становятся отправной точкой для следующего пути (следующего сегмента линии, который нужно нарисовать).
private var currentX = 0f
private var currentY = 0f- Реализуйте метод
touchStart()следующим образом. Сбросьтеpath, перейдите к координатам xy события касания (motionTouchEventXиmotionTouchEventY) и присвойте этим значениямcurrentXиcurrentY.
private fun touchStart() {
path.reset()
path.moveTo(motionTouchEventX, motionTouchEventY)
currentX = motionTouchEventX
currentY = motionTouchEventY
}Шаг 3. Реализуйте touchMove()
- На уровне класса добавьте переменную
touchToleranceи задайте для нее значениеViewConfiguration.get(context).scaledTouchSlop.
private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlopИспользуя путь, нет необходимости отрисовывать каждый пиксель и каждый раз запрашивать обновление экрана. Вместо этого можно (и нужно) интерполировать путь между точками для значительного повышения производительности.
- Если палец едва пошевелился, рисовать не нужно.
- Если палец переместился на расстояние меньшее, чем
touchTolerance, не рисуйте. -
scaledTouchSlopвозвращает расстояние в пикселях, на которое может сместиться касание, прежде чем система решит, что пользователь выполняет прокрутку.
- Определите метод
touchMove(). Рассчитайте пройденное расстояние (dx,dy), создайте кривую между двумя точками и сохраните её вpath, обновите текущие значенияcurrentXиcurrentYи нарисуйтеpath. Затем вызовитеinvalidate()для принудительной перерисовки экрана с обновлённымpath.
private fun touchMove() {
val dx = Math.abs(motionTouchEventX - currentX)
val dy = Math.abs(motionTouchEventY - currentY)
if (dx >= touchTolerance || dy >= touchTolerance) {
// QuadTo() adds a quadratic bezier from the last point,
// approaching control point (x1,y1), and ending at (x2,y2).
path.quadTo(currentX, currentY, (motionTouchEventX + currentX) / 2, (motionTouchEventY + currentY) / 2)
currentX = motionTouchEventX
currentY = motionTouchEventY
// Draw the path in the extra bitmap to cache it.
extraCanvas.drawPath(path, paint)
}
invalidate()
}Подробнее об этом методе:
- Рассчитайте пройденное расстояние (
dx, dy). - Если перемещение произошло дальше допуска касания, добавьте сегмент к траектории.
- Установите начальную точку следующего сегмента на конечную точку этого сегмента.
- Используя
quadTo()вместоlineTo()создайте плавную линию без углов. См. раздел «Кривые Безье» . - Вызовите
invalidate()для (в конечном итоге вызовитеonDraw()и) перерисовки представления.
Шаг 4: Реализация touchUp()
Когда пользователь отнимает палец, достаточно просто сбросить контур, чтобы он не рисовался снова. Ничего не рисуется, поэтому аннулирование не требуется.
- Реализуйте метод
touchUp().
private fun touchUp() {
// Reset the path so it doesn't get drawn again.
path.reset()
}- Запустите свой код и рисуйте пальцем на экране. Обратите внимание: при повороте устройства экран очищается, поскольку состояние рисования не сохраняется. В этом примере приложения это сделано намеренно, чтобы предоставить пользователю простой способ очистки экрана.

Шаг 5: Нарисуйте рамку вокруг эскиза.
Когда пользователь рисует на экране, ваше приложение строит контур и сохраняет его в растровом изображении extraBitmap . Метод onDraw() отображает дополнительное растровое изображение на холсте представления. В onDraw() можно выполнять дополнительные операции рисования. Например, можно рисовать фигуры после отрисовки растрового изображения.
На этом этапе вы рисуете рамку по краю рисунка.
- В
MyCanvasViewдобавьте переменную с именемframe, которая содержит объектRect.
private lateinit var frame: Rect- В конце
onSizeChanged()определите вставку и добавьте код для созданияRect, который будет использоваться для рамки, с использованием новых размеров и вставки.
// Calculate a rectangular frame around the picture.
val inset = 40
frame = Rect(inset, inset, width - inset, height - inset)- В
onDraw()после рисования растрового изображения нарисуйте прямоугольник.
// Draw a frame around the canvas.
canvas.drawRect(frame, paint)- Запустите приложение. Обратите внимание на рамку.

Задача (необязательно): сохранение данных в пути
В текущем приложении информация о чертежах хранится в растровом изображении. Хотя это хорошее решение, это не единственный возможный способ хранения информации о чертежах. Способ хранения истории рисования зависит от приложения и ваших требований. Например, если вы рисуете фигуры, вы можете сохранить список фигур с их расположением и размерами. В приложении MiniPaint можно сохранить контур как Path . Ниже приведено общее описание того, как это сделать, если хотите попробовать.
- В
MyCanvasViewудалите весь код дляextraCanvasиextraBitmap. - Добавьте переменные для текущего пути и пути, который рисуется в данный момент.
// Path representing the drawing so far
private val drawing = Path()
// Path representing what's currently being drawn
private val curPath = Path()- В
onDraw()вместо рисования растрового изображения рисуются сохраненные и текущие пути.
// Draw the drawing so far
canvas.drawPath(drawing, paint)
// Draw any current squiggle
canvas.drawPath(curPath, paint)
// Draw a frame around the canvas
canvas.drawRect(frame, paint)- В
touchUp()добавьте текущий путь к предыдущему пути и сбросьте текущий путь.
// Add the current path to the drawing so far
drawing.addPath(curPath)
// Rewind the current path for the next touch
curPath.reset()- Запустите свое приложение, и да, не должно быть никакой разницы.
Загрузите код для готовой лабораторной работы.
$ git clone https://github.com/googlecodelabs/android-kotlin-drawing-canvas
Кроме того, вы можете загрузить репозиторий в виде ZIP-файла, распаковать его и открыть в Android Studio.
-
Canvas— это двухмерная поверхность для рисования, предоставляющая методы рисования. -
Canvasможно связать с экземпляромView, который его отображает. - Объект
Paintсодержит информацию о стиле и цвете, необходимую для рисования геометрических фигур (таких как линии, прямоугольники, овалы и контуры) и текста. - Распространенным шаблоном работы с холстом является создание пользовательского представления и переопределение методов
onDraw()иonSizeChanged(). - Переопределите метод
onTouchEvent()чтобы он фиксировал касания пользователя и реагировал на них рисованием. - Вы можете использовать дополнительное растровое изображение для кэширования информации о рисунках, которые со временем меняются. В качестве альтернативы можно хранить фигуры или контуры.
Курс Udacity:
Документация для разработчиков Android:
- Класс
Canvas - Класс
Bitmap -
Viewкласс - Класс
Paint - Конфигурации
Bitmap.config - Класс
Path - Страница Википедии о кривых Безье
- Холст и рисунки
- Серия статей «Графическая архитектура» (расширенная)
- чертежи
- onDraw()
- onSizeChanged()
-
MotionEvent -
ViewConfiguration.get(context).scaledTouchSlop
В этом разделе перечислены возможные домашние задания для студентов, работающих над этой лабораторной работой в рамках курса, проводимого преподавателем. Преподаватель должен выполнить следующие действия:
- При необходимости задавайте домашнее задание.
- Объясните учащимся, как следует сдавать домашние задания.
- Оцените домашние задания.
Преподаватели могут использовать эти предложения так часто или редко, как пожелают, и могут свободно задавать любые другие домашние задания, которые они сочтут подходящими.
Если вы работаете с этой лабораторной работой самостоятельно, можете использовать эти домашние задания для проверки своих знаний.
Ответьте на эти вопросы
Вопрос 1
Какие из следующих компонентов необходимы для работы с Canvas ? Выберите все подходящие варианты.
▢ Bitmap
▢ Paint
▢ Path
▢ View
Вопрос 2
Что делает вызов invalidate() (в общих чертах)?
▢ Отменяет и перезапускает ваше приложение.
▢ Стирает рисунок с растрового изображения.
▢ Указывает, что предыдущий код не следует запускать.
▢ Сообщает системе о необходимости перерисовки экрана.
Вопрос 3
Какова функция объектов Canvas , Bitmap и Paint ?
▢ Поверхность 2D-рисунка, растровое изображение, отображаемое на экране, информация о стиле для рисования.
▢ Поверхность 3D-рисунка, растровое изображение для кэширования пути, информация о стилях для рисования.
▢ Поверхность 2D-чертежа, растровое изображение, отображаемое на экране, стилизация для вида.
▢ Кэш для информации о чертежах, растровое изображение для рисования, информация о стилях для рисования.
Ссылки на другие практические занятия по этому курсу см. на целевой странице практических занятий по курсу Advanced Android in Kotlin.