Рисование на объектах холста

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

Введение

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

Помимо использования drawables , вы можете создавать 2D-рисунки, используя методы рисования класса 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 создайте New > Kotlin File/Class с именем 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() также вызывается после того, как действие сначала создает и расширяет его. Таким образом, этот метод 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 на холсте, связанном с представлением. Метод Canvas drawBitmap() в нескольких версиях. В этом коде вы предоставляете растровое изображение, координаты 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 of 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 — это 2D-поверхность для рисования, предоставляющая методы для рисования.
  • Canvas может быть связан с экземпляром View , который его отображает.
  • Объект Paint содержит информацию о стиле и цвете о том, как рисовать геометрию (например, линию, прямоугольник, овал и пути) и текст.
  • Распространенным шаблоном работы с холстом является создание пользовательского представления и переопределение onDraw() и onSizeChanged() .
  • Переопределите метод onTouchEvent() , чтобы фиксировать прикосновения пользователя и реагировать на них рисованием.
  • Вы можете использовать дополнительное растровое изображение для кэширования информации для рисунков, которые со временем изменяются. В качестве альтернативы вы можете хранить фигуры или путь.

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

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

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

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

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

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

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

Вопрос 1

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

▢ Растровое Bitmap

Paint

Path

View

вопрос 2

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

▢ Делает приложение недействительным и перезапускает его.

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

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

▢ Сообщает системе, что она должна перерисовать экран.

Вопрос 3

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

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

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

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

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

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