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