Освой Android играючи
/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
Создадим простейший графический редактор для рисования лапами. Пример основан на курсе Udacity, сохранил у себя на память.
Заведём ресурсы для цвета в res/values/colors.xml.
<color name="colorBackground">#FFFF5500</color>
<color name="colorPaint">#FFFFEB3B</color>
Если хотите иметь полноэкранный режим для рисования, то выберите стиль.
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
Создадим класс для рисования CatCanvasView.kt. Напоминаю, что рисовать можно только котов.
package ru.alexanderklimov.hellokot
import android.content.Context
import android.view.View
class CatCanvasView(context: Context) : View(context)
Подключаем к активности.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val catCanvasView = CatCanvasView(this)
catCanvasView.systemUiVisibility = SYSTEM_UI_FLAG_FULLSCREEN
setContentView(catCanvasView)
}
Получим белый экран - перед нами холст для рисования пушистых созданий.
Вернёмся к классу CatCanvasView и переопределим метод onSizeChanged(), который вызывается при каждом изменении размера холста. В ней будем создавать кэшируемую растровую картинку Bitmap и связанный с ней холст Canvas. Холст заливаем цветом. Чтобы избежать утечки памяти, вызываем метод recycle(), чтобы освобождать старые ресурсы при каждом новом создании растра.
class CatCanvasView(context: Context) : View(context){
// for caching
private lateinit var extraCanvas: Canvas
private lateinit var extraBitmap: Bitmap
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (::extraBitmap.isInitialized) extraBitmap.recycle()
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
extraCanvas = Canvas(extraBitmap)
extraCanvas.drawColor(backgroundColor)
}
}
Само рисование происходит в методе onDraw(). Переопределим его. Если запустить проект, то экран окрасится в оранжевый цвет.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
}
Для рисования понадобится кисть Paint. Определим константу STROKE_WIDTH на уровне файла. Внутри класса CatCanvasView зададим цвет для кисти и саму кисть. Также зададим контур Path.
private const val STROKE_WIDTH = 12f
// цвет для кисти
private val drawColor = ResourcesCompat.getColor(resources, R.color.colorPaint, null)
// Кисть для рисования
private val paint = Paint().apply {
color = drawColor
isAntiAlias = true
isDither = true // Dithering affects how colors with higher-precision than the device are down-sampled
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)
}
private var path = Path()
Добавим обработку касаний экрана.
private var motionTouchEventX = 0f
private var motionTouchEventY = 0f
// Заготовки для функций
private fun touchStart() {}
private fun touchMove() {}
private fun touchUp() {}
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
}
Метод touchStart() запоминает координаты касания экрана. Когда пользователь уберёт палец с экрана и снова коснётся его, то опять начинаем с начала.
private var currentX = 0f
private var currentY = 0f
private fun touchStart() {
path.reset()
path.moveTo(motionTouchEventX, motionTouchEventY)
currentX = motionTouchEventX
currentY = motionTouchEventY
}
Код для движения пальцем по экрану в методе touchMove(). Приподнимаем палец - сбрасываем контур для рисования в методе touchUp().
private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlop
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()
}
private fun touchUp() {
// Reset the path so it doesn't get drawn again.
path.reset()
}
Запускайте проект и рисуйте кривые линии.
Нарисуем рамку вокруг холста.
private lateinit var frame: Rect
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
...
// Calculate a rectangular frame around the picture.
val inset = 40
frame = Rect(inset, inset, width - inset, height - inset)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
// Draw a frame around the canvas.
canvas.drawRect(frame, paint)
}
Полный листинг.
package ru.alexanderklimov.hellokot
import android.content.Context
import android.graphics.*
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import androidx.core.content.res.ResourcesCompat
import kotlin.math.abs
private const val STROKE_WIDTH = 12f
class CatCanvasView(context: Context) : View(context){
// for caching
private lateinit var extraCanvas: Canvas
private lateinit var extraBitmap: Bitmap
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)
private val drawColor = ResourcesCompat.getColor(resources, R.color.colorPaint, null)
// Кисть для рисования
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)
}
private var path = Path()
private var motionTouchEventX = 0f
private var motionTouchEventY = 0f
private lateinit var frame: Rect
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (::extraBitmap.isInitialized) extraBitmap.recycle()
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
extraCanvas = Canvas(extraBitmap)
extraCanvas.drawColor(backgroundColor)
// Calculate a rectangular frame around the picture.
val inset = 40
frame = Rect(inset, inset, width - inset, height - inset)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
// Draw a frame around the canvas.
canvas.drawRect(frame, paint)
}
private var currentX = 0f
private var currentY = 0f
private fun touchStart() {
path.reset()
path.moveTo(motionTouchEventX, motionTouchEventY)
currentX = motionTouchEventX
currentY = motionTouchEventY
}
private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlop
private fun touchMove() {
val dx = abs(motionTouchEventX - currentX)
val dy = 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()
}
private fun touchUp() {
// Reset the path so it doesn't get drawn again.
path.reset()
}
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
}
}