1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.egg.paint 18 19 import android.content.Context 20 import android.graphics.* 21 import android.provider.Settings 22 import android.util.AttributeSet 23 import android.util.DisplayMetrics 24 import android.view.MotionEvent 25 import android.view.View 26 import android.view.WindowInsets 27 import java.util.concurrent.TimeUnit 28 import android.provider.Settings.System 29 30 import org.json.JSONObject 31 32 fun hypot(x: Float, y: Float): Float { 33 return Math.hypot(x.toDouble(), y.toDouble()).toFloat() 34 } 35 36 fun invlerp(x: Float, a: Float, b: Float): Float { 37 return if (b > a) { 38 (x - a) / (b - a) 39 } else 1.0f 40 } 41 42 public class Painting : View, SpotFilter.Plotter { 43 companion object { 44 val FADE_MINS = TimeUnit.MINUTES.toMillis(3) // about how long a drawing should last 45 val ZEN_RATE = TimeUnit.SECONDS.toMillis(2) // how often to apply the fade 46 val ZEN_FADE = Math.max(1f, ZEN_RATE / FADE_MINS * 255f) 47 48 val FADE_TO_WHITE_CF = ColorMatrixColorFilter(ColorMatrix(floatArrayOf( 49 1f, 0f, 0f, 0f, ZEN_FADE, 50 0f, 1f, 0f, 0f, ZEN_FADE, 51 0f, 0f, 1f, 0f, ZEN_FADE, 52 0f, 0f, 0f, 1f, 0f 53 ))) 54 55 val FADE_TO_BLACK_CF = ColorMatrixColorFilter(ColorMatrix(floatArrayOf( 56 1f, 0f, 0f, 0f, -ZEN_FADE, 57 0f, 1f, 0f, 0f, -ZEN_FADE, 58 0f, 0f, 1f, 0f, -ZEN_FADE, 59 0f, 0f, 0f, 1f, 0f 60 ))) 61 62 val INVERT_CF = ColorMatrixColorFilter(ColorMatrix(floatArrayOf( 63 -1f, 0f, 0f, 0f, 255f, 64 0f, -1f, 0f, 0f, 255f, 65 0f, 0f, -1f, 0f, 255f, 66 0f, 0f, 0f, 1f, 0f 67 ))) 68 69 val TOUCH_STATS = "touch.stats" // Settings.System key 70 } 71 72 var devicePressureMin = 0f; // ideal value 73 var devicePressureMax = 1f; // ideal value 74 75 var zenMode = true 76 set(value) { 77 if (field != value) { 78 field = value 79 removeCallbacks(fadeRunnable) 80 if (value && isAttachedToWindow) { 81 handler.postDelayed(fadeRunnable, ZEN_RATE) 82 } 83 } 84 } 85 86 var bitmap: Bitmap? = null 87 var paperColor: Int = 0xFFFFFFFF.toInt() 88 89 private var _paintCanvas: Canvas? = null 90 private val _bitmapLock = Object() 91 92 private var _drawPaint = Paint(Paint.ANTI_ALIAS_FLAG) 93 private var _lastX = 0f 94 private var _lastY = 0f 95 private var _lastR = 0f 96 private var _insets: WindowInsets? = null 97 98 private var _brushWidth = 100f 99 100 private var _filter = SpotFilter(10, 0.5f, 0.9f, this) 101 102 private val fadeRunnable = object : Runnable { 103 private val pt = Paint() 104 override fun run() { 105 val c = _paintCanvas 106 if (c != null) { 107 pt.colorFilter = 108 if (paperColor.and(0xFF) > 0x80) 109 FADE_TO_WHITE_CF 110 else 111 FADE_TO_BLACK_CF 112 113 synchronized(_bitmapLock) { 114 bitmap?.let { 115 c.drawBitmap(bitmap!!, 0f, 0f, pt) 116 } 117 } 118 invalidate() 119 } 120 postDelayed(this, ZEN_RATE) 121 } 122 } 123 124 constructor(context: Context) : super(context) { 125 init() 126 } 127 128 constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { 129 init() 130 } 131 132 constructor( 133 context: Context, 134 attrs: AttributeSet, 135 defStyle: Int 136 ) : super(context, attrs, defStyle) { 137 init() 138 } 139 140 private fun init() { 141 loadDevicePressureData() 142 } 143 144 override fun onAttachedToWindow() { 145 super.onAttachedToWindow() 146 147 setupBitmaps() 148 149 if (zenMode) { 150 handler.postDelayed(fadeRunnable, ZEN_RATE) 151 } 152 } 153 154 override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { 155 super.onSizeChanged(w, h, oldw, oldh) 156 setupBitmaps() 157 } 158 159 override fun onDetachedFromWindow() { 160 if (zenMode) { 161 removeCallbacks(fadeRunnable) 162 } 163 super.onDetachedFromWindow() 164 } 165 166 fun onTrimMemory() { 167 } 168 169 override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets { 170 _insets = insets 171 if (insets != null && _paintCanvas == null) { 172 setupBitmaps() 173 } 174 return super.onApplyWindowInsets(insets) 175 } 176 177 private fun powf(a: Float, b: Float): Float { 178 return Math.pow(a.toDouble(), b.toDouble()).toFloat() 179 } 180 181 override fun plot(s: MotionEvent.PointerCoords) { 182 val c = _paintCanvas 183 if (c == null) return 184 synchronized(_bitmapLock) { 185 var x = _lastX 186 var y = _lastY 187 var r = _lastR 188 val newR = Math.max(1f, powf(adjustPressure(s.pressure), 2f).toFloat() * _brushWidth) 189 190 if (r >= 0) { 191 val d = hypot(s.x - x, s.y - y) 192 if (d > 1f && (r + newR) > 1f) { 193 val N = (2 * d / Math.min(4f, r + newR)).toInt() 194 195 val stepX = (s.x - x) / N 196 val stepY = (s.y - y) / N 197 val stepR = (newR - r) / N 198 for (i in 0 until N - 1) { // we will draw the last circle below 199 x += stepX 200 y += stepY 201 r += stepR 202 c.drawCircle(x, y, r, _drawPaint) 203 } 204 } 205 } 206 207 c.drawCircle(s.x, s.y, newR, _drawPaint) 208 _lastX = s.x 209 _lastY = s.y 210 _lastR = newR 211 } 212 } 213 214 private fun loadDevicePressureData() { 215 try { 216 val touchDataJson = Settings.System.getString(context.contentResolver, TOUCH_STATS) 217 val touchData = JSONObject( 218 if (touchDataJson != null) touchDataJson else "{}") 219 if (touchData.has("min")) devicePressureMin = touchData.getDouble("min").toFloat() 220 if (touchData.has("max")) devicePressureMax = touchData.getDouble("max").toFloat() 221 if (devicePressureMin < 0) devicePressureMin = 0f 222 if (devicePressureMax < devicePressureMin) devicePressureMax = devicePressureMin + 1f 223 } catch (e: Exception) { 224 } 225 } 226 227 private fun adjustPressure(pressure: Float): Float { 228 if (pressure > devicePressureMax) devicePressureMax = pressure 229 if (pressure < devicePressureMin) devicePressureMin = pressure 230 return invlerp(pressure, devicePressureMin, devicePressureMax) 231 } 232 233 override fun onTouchEvent(event: MotionEvent?): Boolean { 234 val c = _paintCanvas 235 if (event == null || c == null) return super.onTouchEvent(event) 236 237 /* 238 val pt = Paint(Paint.ANTI_ALIAS_FLAG) 239 pt.style = Paint.Style.STROKE 240 pt.color = 0x800000FF.toInt() 241 _paintCanvas?.drawCircle(event.x, event.y, 20f, pt) 242 */ 243 244 when (event.actionMasked) { 245 MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { 246 _filter.add(event) 247 _filter.finish() 248 invalidate() 249 } 250 251 MotionEvent.ACTION_DOWN -> { 252 _lastR = -1f 253 _filter.add(event) 254 invalidate() 255 } 256 257 MotionEvent.ACTION_MOVE -> { 258 _filter.add(event) 259 260 invalidate() 261 } 262 } 263 264 return true 265 } 266 267 override fun onDraw(canvas: Canvas) { 268 super.onDraw(canvas) 269 270 bitmap?.let { 271 canvas.drawBitmap(bitmap!!, 0f, 0f, _drawPaint) 272 } 273 } 274 275 // public api 276 fun clear() { 277 bitmap = null 278 setupBitmaps() 279 invalidate() 280 } 281 282 fun sampleAt(x: Float, y: Float): Int { 283 val localX = (x - left).toInt() 284 val localY = (y - top).toInt() 285 return bitmap?.getPixel(localX, localY) ?: Color.BLACK 286 } 287 288 fun setPaintColor(color: Int) { 289 _drawPaint.color = color 290 } 291 292 fun getPaintColor(): Int { 293 return _drawPaint.color 294 } 295 296 fun setBrushWidth(w: Float) { 297 _brushWidth = w 298 } 299 300 fun getBrushWidth(): Float { 301 return _brushWidth 302 } 303 304 private fun setupBitmaps() { 305 val dm = DisplayMetrics() 306 display.getRealMetrics(dm) 307 val w = dm.widthPixels 308 val h = dm.heightPixels 309 val oldBits = bitmap 310 var bits = oldBits 311 if (bits == null || bits.width != w || bits.height != h) { 312 bits = Bitmap.createBitmap( 313 w, 314 h, 315 Bitmap.Config.ARGB_8888 316 ) 317 } 318 if (bits == null) return 319 320 val c = Canvas(bits) 321 322 if (oldBits != null) { 323 if (oldBits.width < oldBits.height != bits.width < bits.height) { 324 // orientation change. let's rotate things so they fit better 325 val matrix = Matrix() 326 if (bits.width > bits.height) { 327 // now landscape 328 matrix.postRotate(-90f) 329 matrix.postTranslate(0f, bits.height.toFloat()) 330 } else { 331 // now portrait 332 matrix.postRotate(90f) 333 matrix.postTranslate(bits.width.toFloat(), 0f) 334 } 335 if (bits.width != oldBits.height || bits.height != oldBits.width) { 336 matrix.postScale( 337 bits.width.toFloat() / oldBits.height, 338 bits.height.toFloat() / oldBits.width) 339 } 340 c.matrix = matrix 341 } 342 // paint the old artwork atop the new 343 c.drawBitmap(oldBits, 0f, 0f, _drawPaint) 344 c.matrix = Matrix() 345 } else { 346 c.drawColor(paperColor) 347 } 348 349 bitmap = bits 350 _paintCanvas = c 351 } 352 353 fun invertContents() { 354 val invertPaint = Paint() 355 invertPaint.colorFilter = INVERT_CF 356 synchronized(_bitmapLock) { 357 bitmap?.let { 358 _paintCanvas?.drawBitmap(bitmap!!, 0f, 0f, invertPaint) 359 } 360 } 361 invalidate() 362 } 363 } 364