blob: a4a3d3d835e086a45567e961f78ffcd6c86ddf4d [file] [log] [blame]
/*
* 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.content.res.Resources
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.util.Log
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) {
c.drawBitmap(bitmap, 0f, 0f, pt)
}
invalidate()
}
postDelayed(this, ZEN_RATE)
}
}
constructor(context: Context) : super(context) {
init(null, 0)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(attrs, 0)
}
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) {
init(attrs, defStyle)
}
private fun init(attrs: AttributeSet?, defStyle: Int) {
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) {
_paintCanvas?.drawBitmap(bitmap, 0f, 0f, invertPaint)
}
invalidate()
}
}