Освой Android играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Котошоп. Графический редактор (Kotlin)

Создадим простейший графический редактор для рисования лапами. Пример основан на курсе 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
    }
}

Дополнительное чтение

Реклама