/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.egg.paint import android.content.Context import android.graphics.* import android.provider.Settings import android.util.AttributeSet import android.util.DisplayMetrics import android.view.MotionEvent import android.view.View import android.view.WindowInsets import java.util.concurrent.TimeUnit import android.provider.Settings.System import org.json.JSONObject fun hypot(x: Float, y: Float): Float { return Math.hypot(x.toDouble(), y.toDouble()).toFloat() } fun invlerp(x: Float, a: Float, b: Float): Float { return if (b > a) { (x - a) / (b - a) } else 1.0f } public class Painting : View, SpotFilter.Plotter { companion object { val FADE_MINS = TimeUnit.MINUTES.toMillis(3) // about how long a drawing should last val ZEN_RATE = TimeUnit.SECONDS.toMillis(2) // how often to apply the fade val ZEN_FADE = Math.max(1f, ZEN_RATE / FADE_MINS * 255f) val FADE_TO_WHITE_CF = ColorMatrixColorFilter(ColorMatrix(floatArrayOf( 1f, 0f, 0f, 0f, ZEN_FADE, 0f, 1f, 0f, 0f, ZEN_FADE, 0f, 0f, 1f, 0f, ZEN_FADE, 0f, 0f, 0f, 1f, 0f ))) val FADE_TO_BLACK_CF = ColorMatrixColorFilter(ColorMatrix(floatArrayOf( 1f, 0f, 0f, 0f, -ZEN_FADE, 0f, 1f, 0f, 0f, -ZEN_FADE, 0f, 0f, 1f, 0f, -ZEN_FADE, 0f, 0f, 0f, 1f, 0f ))) val INVERT_CF = ColorMatrixColorFilter(ColorMatrix(floatArrayOf( -1f, 0f, 0f, 0f, 255f, 0f, -1f, 0f, 0f, 255f, 0f, 0f, -1f, 0f, 255f, 0f, 0f, 0f, 1f, 0f ))) val TOUCH_STATS = "touch.stats" // Settings.System key } var devicePressureMin = 0f; // ideal value var devicePressureMax = 1f; // ideal value var zenMode = true set(value) { if (field != value) { field = value removeCallbacks(fadeRunnable) if (value && isAttachedToWindow) { handler.postDelayed(fadeRunnable, ZEN_RATE) } } } var bitmap: Bitmap? = null var paperColor: Int = 0xFFFFFFFF.toInt() private var _paintCanvas: Canvas? = null private val _bitmapLock = Object() private var _drawPaint = Paint(Paint.ANTI_ALIAS_FLAG) private var _lastX = 0f private var _lastY = 0f private var _lastR = 0f private var _insets: WindowInsets? = null private var _brushWidth = 100f private var _filter = SpotFilter(10, 0.5f, 0.9f, this) private val fadeRunnable = object : Runnable { private val pt = Paint() override fun run() { val c = _paintCanvas if (c != null) { pt.colorFilter = if (paperColor.and(0xFF) > 0x80) FADE_TO_WHITE_CF else FADE_TO_BLACK_CF synchronized(_bitmapLock) { bitmap?.let { c.drawBitmap(bitmap!!, 0f, 0f, pt) } } invalidate() } postDelayed(this, ZEN_RATE) } } constructor(context: Context) : super(context) { init() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { init() } constructor( context: Context, attrs: AttributeSet, defStyle: Int ) : super(context, attrs, defStyle) { init() } private fun init() { loadDevicePressureData() } override fun onAttachedToWindow() { super.onAttachedToWindow() setupBitmaps() if (zenMode) { handler.postDelayed(fadeRunnable, ZEN_RATE) } } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) setupBitmaps() } override fun onDetachedFromWindow() { if (zenMode) { removeCallbacks(fadeRunnable) } super.onDetachedFromWindow() } fun onTrimMemory() { } override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets { _insets = insets if (insets != null && _paintCanvas == null) { setupBitmaps() } return super.onApplyWindowInsets(insets) } private fun powf(a: Float, b: Float): Float { return Math.pow(a.toDouble(), b.toDouble()).toFloat() } override fun plot(s: MotionEvent.PointerCoords) { val c = _paintCanvas if (c == null) return synchronized(_bitmapLock) { var x = _lastX var y = _lastY var r = _lastR val newR = Math.max(1f, powf(adjustPressure(s.pressure), 2f).toFloat() * _brushWidth) if (r >= 0) { val d = hypot(s.x - x, s.y - y) if (d > 1f && (r + newR) > 1f) { val N = (2 * d / Math.min(4f, r + newR)).toInt() val stepX = (s.x - x) / N val stepY = (s.y - y) / N val stepR = (newR - r) / N for (i in 0 until N - 1) { // we will draw the last circle below x += stepX y += stepY r += stepR c.drawCircle(x, y, r, _drawPaint) } } } c.drawCircle(s.x, s.y, newR, _drawPaint) _lastX = s.x _lastY = s.y _lastR = newR } } private fun loadDevicePressureData() { try { val touchDataJson = Settings.System.getString(context.contentResolver, TOUCH_STATS) val touchData = JSONObject( if (touchDataJson != null) touchDataJson else "{}") if (touchData.has("min")) devicePressureMin = touchData.getDouble("min").toFloat() if (touchData.has("max")) devicePressureMax = touchData.getDouble("max").toFloat() if (devicePressureMin < 0) devicePressureMin = 0f if (devicePressureMax < devicePressureMin) devicePressureMax = devicePressureMin + 1f } catch (e: Exception) { } } private fun adjustPressure(pressure: Float): Float { if (pressure > devicePressureMax) devicePressureMax = pressure if (pressure < devicePressureMin) devicePressureMin = pressure return invlerp(pressure, devicePressureMin, devicePressureMax) } override fun onTouchEvent(event: MotionEvent?): Boolean { val c = _paintCanvas if (event == null || c == null) return super.onTouchEvent(event) /* val pt = Paint(Paint.ANTI_ALIAS_FLAG) pt.style = Paint.Style.STROKE pt.color = 0x800000FF.toInt() _paintCanvas?.drawCircle(event.x, event.y, 20f, pt) */ when (event.actionMasked) { MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { _filter.add(event) _filter.finish() invalidate() } MotionEvent.ACTION_DOWN -> { _lastR = -1f _filter.add(event) invalidate() } MotionEvent.ACTION_MOVE -> { _filter.add(event) invalidate() } } return true } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) bitmap?.let { canvas.drawBitmap(bitmap!!, 0f, 0f, _drawPaint) } } // public api fun clear() { bitmap = null setupBitmaps() invalidate() } fun sampleAt(x: Float, y: Float): Int { val localX = (x - left).toInt() val localY = (y - top).toInt() return bitmap?.getPixel(localX, localY) ?: Color.BLACK } fun setPaintColor(color: Int) { _drawPaint.color = color } fun getPaintColor(): Int { return _drawPaint.color } fun setBrushWidth(w: Float) { _brushWidth = w } fun getBrushWidth(): Float { return _brushWidth } private fun setupBitmaps() { val dm = DisplayMetrics() display.getRealMetrics(dm) val w = dm.widthPixels val h = dm.heightPixels val oldBits = bitmap var bits = oldBits if (bits == null || bits.width != w || bits.height != h) { bits = Bitmap.createBitmap( w, h, Bitmap.Config.ARGB_8888 ) } if (bits == null) return val c = Canvas(bits) if (oldBits != null) { if (oldBits.width < oldBits.height != bits.width < bits.height) { // orientation change. let's rotate things so they fit better val matrix = Matrix() if (bits.width > bits.height) { // now landscape matrix.postRotate(-90f) matrix.postTranslate(0f, bits.height.toFloat()) } else { // now portrait matrix.postRotate(90f) matrix.postTranslate(bits.width.toFloat(), 0f) } if (bits.width != oldBits.height || bits.height != oldBits.width) { matrix.postScale( bits.width.toFloat() / oldBits.height, bits.height.toFloat() / oldBits.width) } c.matrix = matrix } // paint the old artwork atop the new c.drawBitmap(oldBits, 0f, 0f, _drawPaint) c.matrix = Matrix() } else { c.drawColor(paperColor) } bitmap = bits _paintCanvas = c } fun invertContents() { val invertPaint = Paint() invertPaint.colorFilter = INVERT_CF synchronized(_bitmapLock) { bitmap?.let { _paintCanvas?.drawBitmap(bitmap!!, 0f, 0f, invertPaint) } } invalidate() } }