Всем привет! Новый год уже совсем близко, значит, самое время добавить новогодней атмосферы.
Мы в Dodo стараемся делать наши приложения в первую очередь качественными, но и не забываем добавлять фановых фич для клиентов. Так, например, мы реализовали анимацию «Летающая Пицца», игру «Хвостики», а в канун Нового года решили сделать праздничную зимнюю анимацию под названием «Изморозь».
При при запуске над контентом приложения появляется слой изморози, как будто экран замёрз и пользователь может стереть её пальцем.
В этой статье хочу поделится технической стороной анимации: как добиться эффекта стирания картинки. Сделать её можно за несколько шагов. Не верите? Смотрите!
Что будем делать? Конечно же, рисовать на Canvas.
Let it snow!
Нам понадобятся две картинки:
-
изморозь, картинка с прозрачностью. Чем ближе к центру, тем прозрачнее;
-
обрамление в виде снежинок.
-
Первым делом создаём кастомный класс изморози:
class RimeView constructor(context: Context) : View(context) { // почти готово }
-
Переводим две картинки в bitmap в соответствии с размерами экрана в методе onSizeChanged, так как он вызывается в тот момент, когда определяются размеры кастомного вью:
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) //rime rimeBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) val rimeCanvas = Canvas(rimeBitmap) val rimeDrawable = ContextCompat.getDrawable(context, R.drawable.bg) rimeDrawable?.setBounds(0, 0, w, h) rimeDrawable?.draw(rimeCanvas) //snow snowBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) val snowCanvas = Canvas(snowBitmap) val snowDrawable = ContextCompat.getDrawable(context, R.drawable.snow) snowDrawable?.setBounds(0, 0, w, h) snowDrawable?.draw(snowCanvas) }
-
Рисуем две картинки по очереди в методе onDraw:
val paint = Paint() override fun onDraw(canvas: Canvas) { super.onDraw(canvas) //rime canvas.drawBitmap(rimeBitmap, 0f, 0f, paint) //snow canvas.drawBitmap(snowBitmap, 0f, 0f, paint) }
Хочу обратить внимание на кисточку: она у нас пока просто дефолтная paint = Paint()
Теперь нужен третий «буферный» bitmap, в который будем записывать результат стирания.
-
Определяем буферный bitmap в onSizeChanged, далее выносим переменную scratchCanvas в поле класса, так как будем на ней рисовать стирание:
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) //buffer bitmap scratchBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) scratchCanvas = Canvas(scratchBitmap) //rime rimeBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) val rimeCanvas = Canvas(rimeBitmap) val rimeDrawable = ContextCompat.getDrawable(context, R.drawable.bg) rimeDrawable?.setBounds(0, 0, w, h) rimeDrawable?.draw(rimeCanvas) //snow snowBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) val snowCanvas = Canvas(snowBitmap) val snowDrawable = ContextCompat.getDrawable(context, R.drawable.snow) snowDrawable?.setBounds(0, 0, w, h) snowDrawable?.draw(snowCanvas) }
-
Отрисовываем в onDraw:
canvas.drawBitmap(raimBitmap, 0f, 0f, paint) canvas.drawBitmap(snowBitmap, 0f, 0f, paint) //buffer bitmap canvas.drawBitmap(scratchBitmap, 0f, 0f, paint)
Тут важный момент — конфигурация наших кисточек,
override fun onDraw(canvas: Canvas) { super.onDraw(canvas) paint.xfermode = srcOverPorterDuffMode canvas.drawBitmap(rimeBitmap, 0f, 0f, paint) canvas.drawBitmap(snowBitmap, 0f, 0f, paint) paint.xfermode= dstOutPorterDuffMode canvas.drawBitmap(scratchBitmap, 0f, 0f, paint) }
в котором
private val srcOverPorterDuffMode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER) private val dstOutPorterDuffMode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
это как раз та самая магия стирания.
Давайте разберём чуть подробнее, как это работает.
В документации Android указано, что при наложении двух картинок друг на друга мы можем задать разную композицию.
Например, есть две картинки Destination image и Source image:
Давайте нарисуем их по очереди:
//1 val paint = Paint() //2 canvas.drawBitmap(destinationImage, 0f, 0f, paint) //3 val mode = // choose a PorterDuff.Mode //4 paint.xfermode = PorterDuffXfermode(mode) //5 canvas.drawBitmap(sourceImage, 0f, 0f, paint);
-
Определяем дефолтную кисточку.
-
Рисуем Destination image.
-
Определяем конфигурацию PorterDuff.Mode.
-
Задаём вышеуказанный мод для кисточки.
-
Рисуем Source image.
Исходя из того, какую конфигурацию PorterDuff.Mode мы задали для кисточки, у нас получаются разные композиции:
Нам подходит PorterDuff.Mode = Destination Out, то есть накладываемая сверху картинка должна обрезать область накладывания.
-
Теперь нужно отследить траекторию движения пальца по фону. Для этого мы создаём объект Path(), в который будем записывать путь:
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) ... if (path == null) { path = Path() } }
-
И переопределяем onTouchEvent, в котором берём координаты в момент нажатия пальцем на экран и в момент убирания пальца и рисуем линию между ними:
override fun onTouchEvent(event: MotionEvent): Boolean { val currentTouchX = event.x val currentTouchY = event.y when (event.action) { MotionEvent.ACTION_DOWN -> { path?.reset() path?.moveTo(event.x, event.y) } MotionEvent.ACTION_UP -> { path?.lineTo(currentTouchX, currentTouchY) } MotionEvent.ACTION_MOVE -> { //пока пусто, мы определим его чуть ниже } } scratchCanvas?.drawPath(path, innerPaint) mLastTouchX = currentTouchX mLastTouchY = currentTouchY invalidate() return true }
innerPaint — это дефолтная кисточка.
-
Определим некий контейнер и добавим в него наш RimeView
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#9C3333" tools:context=".MainActivity" />
val container = findViewById<FrameLayout>(R.id.container) val rimeView = RimeView(this) rimeView.layoutParams = FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) container.addView(rimeView)
И у нас получается вот такой предварительный результат:
-
Добавим отрисовку по ходу ведения пальца:
MotionEvent.ACTION_MOVE -> { val dx =abs(currentTouchX - mLastTouchX) val dy =abs(currentTouchY - mLastTouchY) if (dx >= 4 || dy >= 4) { val x1 = mLastTouchX val y1 = mLastTouchY val x2 = (currentTouchX + mLastTouchX) / 2 val y2 = (currentTouchY + mLastTouchY) / 2 mPath?.quadTo(x1, y1, x2, y2) } }
Здесь мы рисуем квадратичную кривую Безье, если палец прошёл более 4 пикселей в одну из сторон (значение получено опытным путём), для того чтобы был эффект закругления. И получаем вот такой конечный результат:
Вот и всё!
Если вам всё ещё не верится, что мы сделали эту анимацию за несколько шагов, то вспомните, что в канун нового года случаются чудеса. =)
Всех с наступающим Новым годом!
ссылка на оригинал статьи https://habr.com/ru/company/dododev/blog/708104/
Добавить комментарий