Эта кодовая лаборатория является частью курса Advanced Android in Kotlin. Вы получите максимальную отдачу от этого курса, если будете последовательно работать с лабораториями кода, но это не обязательно. Все кодовые лаборатории курса перечислены на целевой странице Advanced Android in Kotlin codelabs .
Введение
В Android у вас есть несколько методов для реализации пользовательской 2D-графики и анимации в представлениях.
Помимо использования drawables , вы можете создавать 2D-рисунки, используя методы рисования класса 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
создайте New > Kotlin File/Class с именем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()
также вызывается после того, как действие сначала создает и расширяет его. Таким образом, этот метод 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
на холсте, связанном с представлением. МетодCanvas
drawBitmap()
в нескольких версиях. В этом коде вы предоставляете растровое изображение, координаты 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)
}
-
color
paint
— этоdrawColor
, который вы определили ранее. -
isAntiAlias
определяет, следует ли применять сглаживание краев. Установка дляisAntiAlias
true
сглаживает края нарисованного, не затрагивая форму. -
isDither
, когдаtrue
, влияет на то, как цвета с более высокой точностью, чем у устройства, уменьшаются. Например, сглаживание является наиболее распространенным средством уменьшения цветового диапазона изображений до 256 (или менее) цветов. -
style
задает тип рисования, который должен быть выполнен в виде штриха, который по сути представляет собой линию.Paint.Style
указывает, является ли рисуемый примитив заполненным, обведенным или и тем, и другим (одним и тем же цветом). По умолчанию заливается объект, на который наносится краска. («Заливка» окрашивает внутреннюю часть фигуры, а «обводка» следует за контуром.) -
strokeJoin
ofPaint.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
— это 2D-поверхность для рисования, предоставляющая методы для рисования. -
Canvas
может быть связан с экземпляромView
, который его отображает. - Объект
Paint
содержит информацию о стиле и цвете о том, как рисовать геометрию (например, линию, прямоугольник, овал и пути) и текст. - Распространенным шаблоном работы с холстом является создание пользовательского представления и переопределение
onDraw()
иonSizeChanged()
. - Переопределите метод
onTouchEvent()
, чтобы фиксировать прикосновения пользователя и реагировать на них рисованием. - Вы можете использовать дополнительное растровое изображение для кэширования информации для рисунков, которые со временем изменяются. В качестве альтернативы вы можете хранить фигуры или путь.
Удасити курс:
Документация для разработчиков Android:
-
Canvas
класс - Класс
Bitmap
-
View
класс - Класс
Paint
- Конфигурации
Bitmap.config
- Класс
Path
- Кривые Безье Страница Википедии
- Холст и чертежи
- Серия статей по графической архитектуре (дополнительно)
- чертежи
- при рисовании()
- 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 codelabs.