Рисование на холсте

Эта практическая работа входит в курс «Advanced Android in Kotlin». Вы получите максимальную пользу от этого курса, выполняя задания последовательно, но это не обязательно. Все практическая работа курса перечислены на целевой странице практической работы «Advanced Android in Kotlin» .

Введение

В Android доступно несколько методов реализации пользовательской 2D-графики и анимации в представлениях.

Помимо использования объектов drawable , вы можете создавать двухмерные рисунки, используя методы рисования класса Canvas . Canvas — это двухмерная поверхность для рисования, предоставляющая методы для рисования. Это полезно, когда вашему приложению требуется регулярно перерисовываться, поскольку то, что видит пользователь, со временем меняется. В этой лабораторной работе вы научитесь создавать и рисовать на холсте, отображаемом в View .

На холсте можно выполнять следующие типы операций:

  • Заполните весь холст цветом.
  • Рисуйте фигуры, такие как прямоугольники, дуги и контуры, стилизованные под объект Paint . Объект Paint содержит информацию о стиле и цвете, необходимую для рисования геометрических фигур (таких как линии, прямоугольники, овалы и контуры), или, например, шрифта текста.
  • Применяйте преобразования, такие как перемещение, масштабирование или пользовательские преобразования.
  • Обрезать, то есть применить форму или контур к холсту, чтобы определить его видимые части.

Как можно представить себе рисование на Android (очень упрощенно!)

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

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

  1. Вам необходимо представление для отображения рисуемого вами объекта. Это может быть одно из представлений, предоставляемых системой Android. Или, в этой лабораторной работе, вы создадите собственное представление, которое будет служить представлением контента для вашего приложения ( MyCanvasView ).
  2. Это представление, как и все представления, имеет свой собственный холст ( canvas ).
  3. Самый простой способ рисования на холсте представления — переопределить его метод onDraw() и рисовать на его холсте.
  4. При создании чертежа необходимо кэшировать уже нарисованные объекты. Существует несколько способов кэширования данных: один из них — в растровом изображении ( extraBitmap ), а другой — сохранить историю нарисованных объектов в виде координат и инструкций.
  5. Чтобы рисовать на кэшируемом растровом изображении ( extraBitmap ) с помощью API рисования на холсте, необходимо создать кэширующий холст ( extraCanvas ) для кэшируемого растрового изображения.
  6. Затем вы рисуете на кэшированном холсте ( extraCanvas ), который рисует на кэшированном растровом изображении ( extraBitmap ).
  7. Чтобы отобразить все, что нарисовано на экране, вы указываете холсту представления ( canvas ) нарисовать кэшированное растровое изображение ( extraBitmap ).

Что вам уже следует знать

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

Чему вы научитесь

  • Как создать Canvas и рисовать на нем в ответ на прикосновения пользователя.

Что ты будешь делать?

  • Создайте приложение, которое рисует линии на экране в ответ на прикосновение пользователя к экрану.
  • Фиксируйте события движения и в ответ рисуйте линии на холсте, который отображается в полноэкранном пользовательском представлении на экране.

Приложение MiniPaint использует настраиваемый вид для отображения линии в ответ на прикосновения пользователя, как показано на снимке экрана ниже.

Шаг 1. Создайте проект MiniPaint

  1. Создайте новый проект Kotlin под названием MiniPaint , использующий шаблон Empty Activity .
  2. Откройте файл app/res/values/colors.xml и добавьте следующие два цвета.
<color name="colorBackground">#FFFF5500</color>
<color name="colorPaint">#FFFFEB3B</color>
  1. Открыть styles.xml
  2. В родительском стиле AppTheme замените DarkActionBar на NoActionBar . Это удалит панель действий, чтобы можно было отрисовывать элементы в полноэкранном режиме.
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">

Шаг 2. Создайте класс MyCanvasView

На этом этапе вы создаете пользовательское представление MyCanvasView для рисования.

  1. В пакете app/java/com.example.android.minipaint создайте новый > файл/класс Kotlin с именем MyCanvasView .
  2. Создайте класс MyCanvasView , расширяющий класс View , и передайте ему context: Context . Примите предложенные импорты.
import android.content.Context
import android.view.View

class MyCanvasView(context: Context) : View(context) {
}

Шаг 3. Установите MyCanvasView в качестве представления содержимого.

Чтобы отобразить то, что вы будете рисовать в MyCanvasView , вам необходимо установить его как представление содержимого MainActivity .

  1. Откройте strings.xml и определите строку, которая будет использоваться для описания содержимого представления.
<string name="canvasContentDescription">Mini Paint is a simple line drawing app.
   Drag your fingers to draw. Rotate the phone to clear.</string>
  1. Открыть MainActivity.kt
  2. В onCreate() удалите setContentView(R.layout.activity_main) .
  3. Создайте экземпляр MyCanvasView .
val myCanvasView = MyCanvasView(this)
  1. Ниже запросите полный экран для макета myCanvasView . Для этого установите флаг SYSTEM_UI_FLAG_FULLSCREEN для myCanvasView . Таким образом, представление полностью заполнит экран.
myCanvasView.systemUiVisibility = SYSTEM_UI_FLAG_FULLSCREEN
  1. Добавьте описание контента.
myCanvasView.contentDescription = getString(R.string.canvasContentDescription)
  1. Ниже установите для вида содержимого значение myCanvasView .
setContentView(myCanvasView)
  1. Запустите приложение. Вы увидите совершенно белый экран, поскольку холст не имеет размера, и вы ещё ничего не нарисовали.

Шаг 1. Переопределить onSizeChanged()

Метод onSizeChanged() вызывается системой Android при каждом изменении размера представления. Поскольку представление изначально не имеет размера, его метод onSizeChanged() также вызывается после того, как Activity впервые создаст и расширит его. Таким образом, метод onSizeChanged() идеально подходит для создания и настройки холста представления.

  1. В MyCanvasView на уровне класса определите переменные для холста и растрового изображения. Назовите их extraCanvas и extraBitmap . Это растровое изображение и холст для кэширования ранее отрисованных объектов.
private lateinit var extraCanvas: Canvas
private lateinit var extraBitmap: Bitmap
  1. Определите переменную уровня класса backgroundColor для цвета фона холста и инициализируйте ее значением colorBackground которое вы определили ранее.
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)
  1. В MyCanvasView переопределите метод onSizeChanged() . Этот метод обратного вызова вызывается системой Android с изменёнными размерами экрана, то есть с новыми шириной и высотой (к которым нужно перейти) и старыми шириной и высотой (к которым нужно перейти).
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
   super.onSizeChanged(width, height, oldWidth, oldHeight)
}
  1. Внутри onSizeChanged() создайте экземпляр Bitmap с новыми шириной и высотой, соответствующими размеру экрана, и присвойте его extraBitmap . Третий аргумент — это цветовая конфигурация растрового изображения . Рекомендуется использовать ARGB_8888 который хранит каждый цвет в 4 байтах.
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
  1. Создайте экземпляр Canvas из extraBitmap и назначьте его extraCanvas .
 extraCanvas = Canvas(extraBitmap)
  1. Укажите цвет фона, которым будет заполнен extraCanvas .
extraCanvas.drawColor(backgroundColor)
  1. Если посмотреть на onSizeChanged() , то можно увидеть, что при каждом выполнении функции создаются новые растровое изображение и холст. Вам нужно новое растровое изображение, поскольку размер изменился. Однако это утечка памяти, и старые растровые изображения остаются. Чтобы исправить это, очистите extraBitmap перед созданием следующего, добавив этот код сразу после вызова super .
if (::extraBitmap.isInitialized) extraBitmap.recycle()

Шаг 2. Переопределить onDraw()

Вся работа по рисованию для MyCanvasView происходит в onDraw() .

Для начала отобразите холст, заполнив экран цветом фона, который вы установили в onSizeChanged() .

  1. Переопределите onDraw() и отобразите содержимое кэшированного extraBitmap на холсте, связанном с представлением. Метод drawBitmap() Canvas существует в нескольких версиях. В этом коде вы указываете растровое изображение, координаты x и y (в пикселях) верхнего левого угла и null для Paint , которое вы установите позже.
override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
}


Обратите внимание, что холст, который передается в onDraw() и используется системой для отображения растрового изображения, отличается от того, который вы создали в методе onSizeChanged() и использовали для рисования на растровом изображении.

  1. Запустите приложение. Вы увидите весь экран, залитый указанным фоновым цветом.

Для рисования вам нужен объект Paint , который определяет, как будут выглядеть объекты при рисовании, и Path , который определяет, что именно будет отрисовано.

Шаг 1. Инициализируем объект Paint.

  1. В файле MyCanvasView.kt на верхнем уровне определите константу для ширины обводки.
private const val STROKE_WIDTH = 12f // has to be float
  1. На уровне класса MyCanvasView определите переменную drawColor для хранения цвета, которым нужно рисовать, и инициализируйте ее с помощью ресурса colorPaint который вы определили ранее.
private val drawColor = ResourcesCompat.getColor(resources, R.color.colorPaint, null)
  1. На уровне класса ниже добавьте переменную 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)
}
  • color paint — это drawColor вы определили ранее.
  • isAntiAlias определяет, следует ли применять сглаживание краёв. Установка isAntiAlias в значение true сглаживает края отрисовываемого объекта, не влияя на его форму.
  • Если isDither в true , он влияет на то, как происходит понижение разрешения цветов с более высокой точностью, чем у устройства. Например, дизеринг — наиболее распространённый способ сужения цветового диапазона изображений до 256 цветов (или меньше).
  • style определяет тип рисования: обводка, которая по сути является линией. Paint.Style определяет, будет ли рисуемый примитив залит, обведен или и то, и другое (одним цветом). По умолчанию заполняется объект, к которому применяется краска. («Заливка» закрашивает внутреннюю часть фигуры, а «обводка» следует её контуру.)
  • strokeJoin объекта Paint.Join определяет, как соединяются линии и сегменты кривых на контуре. Значение по умолчанию — MITER .
  • strokeCap задаёт форму окончания линии. Paint.Cap определяет начало и конец обводимых линий и контуров. Значение по умолчанию — BUTT .
  • strokeWidth задаёт ширину обводки в пикселях. Значение по умолчанию — толщина линии, которая очень тонкая, поэтому оно задаётся константой STROKE_WIDTH которую вы определили ранее.

Шаг 2. Инициализация объекта Path

Path — это путь того, что рисует пользователь.

  1. В MyCanvasView добавьте переменную path и инициализируйте её объектом Path для хранения пути, который рисуется при касании экрана пользователем. Импортируйте android.graphics.Path для Path .
private var path = Path()

Шаг 1. Реагируйте на движение на дисплее.

Метод onTouchEvent() представления вызывается всякий раз, когда пользователь прикасается к дисплею.

  1. В 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
}
  1. На уровне класса добавьте отсутствующие переменные motionTouchEventX и motionTouchEventY для кэширования координат x и y текущего события касания (координат MotionEvent ). Инициализируйте их значением 0f .
private var motionTouchEventX = 0f
private var motionTouchEventY = 0f
  1. Создайте заглушки для трех функций touchStart() , touchMove() и touchUp() .
private fun touchStart() {}

private fun touchMove() {}

private fun touchUp() {}
  1. Ваш код должен скомпилироваться и запуститься, но пока вы не увидите никаких изменений, кроме цветного фона.

Шаг 2. Реализуйте touchStart()

Этот метод вызывается, когда пользователь впервые касается экрана.

  1. На уровне класса добавьте переменные для кэширования последних значений X и Y. После того, как пользователь прекращает движение и отрывает палец, эти переменные становятся отправной точкой для следующего пути (следующего сегмента линии, который нужно нарисовать).
private var currentX = 0f
private var currentY = 0f
  1. Реализуйте метод touchStart() следующим образом. Сбросьте path , перейдите к координатам xy события касания ( motionTouchEventX и motionTouchEventY ) и присвойте этим значениям currentX и currentY .
private fun touchStart() {
   path.reset()
   path.moveTo(motionTouchEventX, motionTouchEventY)
   currentX = motionTouchEventX
   currentY = motionTouchEventY
}

Шаг 3. Реализуйте touchMove()

  1. На уровне класса добавьте переменную touchTolerance и задайте для нее значение ViewConfiguration.get(context).scaledTouchSlop .
private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlop

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

  • Если палец едва пошевелился, рисовать не нужно.
  • Если палец переместился на расстояние меньшее, чем touchTolerance , не рисуйте.
  • scaledTouchSlop возвращает расстояние в пикселях, на которое может сместиться касание, прежде чем система решит, что пользователь выполняет прокрутку.
  1. Определите метод 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()
}

Подробнее об этом методе:

  1. Рассчитайте пройденное расстояние ( dx, dy ).
  2. Если перемещение произошло дальше допуска касания, добавьте сегмент к траектории.
  3. Установите начальную точку следующего сегмента на конечную точку этого сегмента.
  4. Используя quadTo() вместо lineTo() создайте плавную линию без углов. См. раздел «Кривые Безье» .
  5. Вызовите invalidate() для (в конечном итоге вызовите onDraw() и) перерисовки представления.

Шаг 4: Реализация touchUp()

Когда пользователь отнимает палец, достаточно просто сбросить контур, чтобы он не рисовался снова. Ничего не рисуется, поэтому аннулирование не требуется.

  1. Реализуйте метод touchUp() .
private fun touchUp() {
   // Reset the path so it doesn't get drawn again.
   path.reset()
}
  1. Запустите свой код и рисуйте пальцем на экране. Обратите внимание: при повороте устройства экран очищается, поскольку состояние рисования не сохраняется. В этом примере приложения это сделано намеренно, чтобы предоставить пользователю простой способ очистки экрана.

Шаг 5: Нарисуйте рамку вокруг эскиза.

Когда пользователь рисует на экране, ваше приложение строит контур и сохраняет его в растровом изображении extraBitmap . Метод onDraw() отображает дополнительное растровое изображение на холсте представления. В onDraw() можно выполнять дополнительные операции рисования. Например, можно рисовать фигуры после отрисовки растрового изображения.

На этом этапе вы рисуете рамку по краю рисунка.

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

Задача (необязательно): сохранение данных в пути

В текущем приложении информация о чертежах хранится в растровом изображении. Хотя это хорошее решение, это не единственный возможный способ хранения информации о чертежах. Способ хранения истории рисования зависит от приложения и ваших требований. Например, если вы рисуете фигуры, вы можете сохранить список фигур с их расположением и размерами. В приложении MiniPaint можно сохранить контур как Path . Ниже приведено общее описание того, как это сделать, если хотите попробовать.

  1. В MyCanvasView удалите весь код для extraCanvas и extraBitmap .
  2. Добавьте переменные для текущего пути и пути, который рисуется в данный момент.
// Path representing the drawing so far
private val drawing = Path()

// Path representing what's currently being drawn
private val curPath = Path()
  1. В 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)
  1. В touchUp() добавьте текущий путь к предыдущему пути и сбросьте текущий путь.
// Add the current path to the drawing so far
drawing.addPath(curPath)
// Rewind the current path for the next touch
curPath.reset()
  1. Запустите свое приложение, и да, не должно быть никакой разницы.

Загрузите код для готовой лабораторной работы.

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


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

Скачать ZIP-архив

  • Canvas — это двухмерная поверхность для рисования, предоставляющая методы рисования.
  • Canvas можно связать с экземпляром View , который его отображает.
  • Объект Paint содержит информацию о стиле и цвете, необходимую для рисования геометрических фигур (таких как линии, прямоугольники, овалы и контуры) и текста.
  • Распространенным шаблоном работы с холстом является создание пользовательского представления и переопределение методов onDraw() и onSizeChanged() .
  • Переопределите метод onTouchEvent() чтобы он фиксировал касания пользователя и реагировал на них рисованием.
  • Вы можете использовать дополнительное растровое изображение для кэширования информации о рисунках, которые со временем меняются. В качестве альтернативы можно хранить фигуры или контуры.

Курс Udacity:

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

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

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

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

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

Ответьте на эти вопросы

Вопрос 1

Какие из следующих компонентов необходимы для работы с Canvas ? Выберите все подходящие варианты.

Bitmap

Paint

Path

View

Вопрос 2

Что делает вызов invalidate() (в общих чертах)?

▢ Отменяет и перезапускает ваше приложение.

▢ Стирает рисунок с растрового изображения.

▢ Указывает, что предыдущий код не следует запускать.

▢ Сообщает системе о необходимости перерисовки экрана.

Вопрос 3

Какова функция объектов Canvas , Bitmap и Paint ?

▢ Поверхность 2D-рисунка, растровое изображение, отображаемое на экране, информация о стиле для рисования.

▢ Поверхность 3D-рисунка, растровое изображение для кэширования пути, информация о стилях для рисования.

▢ Поверхность 2D-чертежа, растровое изображение, отображаемое на экране, стилизация для вида.

▢ Кэш для информации о чертежах, растровое изображение для рисования, информация о стилях для рисования.

Ссылки на другие практические занятия по этому курсу см. на целевой странице практических занятий по курсу Advanced Android in Kotlin.