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 
hypotnull32 fun hypot(x: Float, y: Float): Float {
33     return Math.hypot(x.toDouble(), y.toDouble()).toFloat()
34 }
35 
invlerpnull36 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()
runnull104         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 
initnull140     private fun init() {
141         loadDevicePressureData()
142     }
143 
onAttachedToWindownull144     override fun onAttachedToWindow() {
145         super.onAttachedToWindow()
146 
147         setupBitmaps()
148 
149         if (zenMode) {
150             handler.postDelayed(fadeRunnable, ZEN_RATE)
151         }
152     }
153 
onSizeChangednull154     override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
155         super.onSizeChanged(w, h, oldw, oldh)
156         setupBitmaps()
157     }
158 
onDetachedFromWindownull159     override fun onDetachedFromWindow() {
160         if (zenMode) {
161             removeCallbacks(fadeRunnable)
162         }
163         super.onDetachedFromWindow()
164     }
165 
onTrimMemorynull166     fun onTrimMemory() {
167     }
168 
onApplyWindowInsetsnull169     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 
powfnull177     private fun powf(a: Float, b: Float): Float {
178         return Math.pow(a.toDouble(), b.toDouble()).toFloat()
179     }
180 
plotnull181     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 
loadDevicePressureDatanull214     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 
adjustPressurenull227     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 
onTouchEventnull233     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 
onDrawnull267     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
clearnull276     fun clear() {
277         bitmap = null
278         setupBitmaps()
279         invalidate()
280     }
281 
sampleAtnull282     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 
setPaintColornull288     fun setPaintColor(color: Int) {
289         _drawPaint.color = color
290     }
291 
getPaintColornull292     fun getPaintColor(): Int {
293         return _drawPaint.color
294     }
295 
setBrushWidthnull296     fun setBrushWidth(w: Float) {
297         _brushWidth = w
298     }
299 
getBrushWidthnull300     fun getBrushWidth(): Float {
301         return _brushWidth
302     }
303 
setupBitmapsnull304     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.setMatrix(matrix)
341             }
342             // paint the old artwork atop the new
343             c.drawBitmap(oldBits, 0f, 0f, _drawPaint)
344             c.setMatrix(Matrix())
345         } else {
346             c.drawColor(paperColor)
347         }
348 
349         bitmap = bits
350         _paintCanvas = c
351     }
352 
invertContentsnull353     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