| /* |
| * 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.setMatrix(matrix) |
| } |
| // paint the old artwork atop the new |
| c.drawBitmap(oldBits, 0f, 0f, _drawPaint) |
| c.setMatrix(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() |
| } |
| } |