xref: /aosp_15_r20/frameworks/base/tests/MultiDeviceInput/src/test/multideviceinput/DrawingView.kt (revision d57664e9bc4670b3ecf6748a746a57c557b6bc9e)
1 /*
<lambda>null2  * Copyright 2023 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 test.multideviceinput
18 
19 import android.content.Context
20 import android.graphics.Canvas
21 import android.graphics.Color
22 import android.graphics.Paint
23 import android.util.AttributeSet
24 import android.view.InputDevice
25 import android.view.InputDevice.SOURCE_MOUSE
26 import android.view.InputDevice.SOURCE_STYLUS
27 import android.view.InputDevice.SOURCE_TOUCHSCREEN
28 import android.view.InputDevice.SOURCE_TOUCHPAD
29 import android.view.MotionEvent
30 import android.view.MotionEvent.ACTION_CANCEL
31 import android.view.MotionEvent.ACTION_DOWN
32 import android.view.MotionEvent.ACTION_HOVER_EXIT
33 import android.view.MotionEvent.ACTION_UP
34 import android.view.ScaleGestureDetector
35 import android.view.View
36 
37 import java.util.Vector
38 
39 private fun drawLine(canvas: Canvas, from: MotionEvent, to: MotionEvent, paint: Paint) {
40     // Correct implementation here would require us to build a set of pointers and then iterate
41     // through them. Instead, we are taking a few shortcuts and ignore some of the events, which
42     // causes occasional gaps in the drawings.
43     if (from.pointerCount != to.pointerCount) {
44         return
45     }
46     // Now, 'from' is guaranteed to have as many pointers as the 'to' event. It doesn't
47     // necessarily mean they are the same pointers, though.
48     for (p in 0..<from.pointerCount) {
49         val x0 = from.getX(p)
50         val y0 = from.getY(p)
51         if (to.getPointerId(p) == from.getPointerId(p)) {
52             // This only works when the i-th pointer in "to" is the same pointer
53             // as the i-th pointer in "from"`. It's not guaranteed by the input APIs,
54             // but it works in practice.
55             val x1 = to.getX(p)
56             val y1 = to.getY(p)
57             // Ignoring historical data here for simplicity
58             canvas.drawLine(x0, y0, x1, y1, paint)
59         }
60     }
61 }
62 
drawCirclenull63 private fun drawCircle(canvas: Canvas, event: MotionEvent, paint: Paint, radius: Float) {
64     val x = event.x
65     val y = event.y
66     canvas.drawCircle(x, y, radius, paint)
67 }
68 
69 /**
70  * Draw the current stroke
71  */
72 class DrawingView : View {
73     private val TAG = "DrawingView"
74 
75     private var myState: SharedScaledPointerSize? = null
76     private var otherState: SharedScaledPointerSize? = null
77 
78     constructor(
79             context: Context,
80             myState: SharedScaledPointerSize,
81             otherState: SharedScaledPointerSize
82             ) : super(context) {
83         this.myState = myState
84         this.otherState = otherState
85         init()
86     }
87 
88     constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
89         init()
90     }
91 
92     val touchEvents = mutableMapOf<Int, Vector<Pair<MotionEvent, Paint>>>()
93     val hoverEvents = mutableMapOf<Int, MotionEvent>()
94 
95     val scaleGestureListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
96 
onScaleBeginnull97         override fun onScaleBegin(scaleGestureDetector: ScaleGestureDetector): Boolean {
98             return true
99         }
100 
onScalenull101         override fun onScale(scaleGestureDetector: ScaleGestureDetector): Boolean {
102             val scaleFactor = scaleGestureDetector.scaleFactor
103             when (otherState?.state) {
104                 PointerState.DOWN -> {
105                     otherState?.lineSize = (otherState?.lineSize ?: 5f) * scaleFactor
106                 }
107                 PointerState.HOVER -> {
108                     otherState?.circleSize = (otherState?.circleSize ?: 20f) * scaleFactor
109                 }
110                 else -> {}
111             }
112             return true
113         }
114     }
115     private val scaleGestureDetector = ScaleGestureDetector(context, scaleGestureListener, null)
116 
117     private var touchPaint = Paint()
118     private var touchpadPaint = Paint()
119     private var stylusPaint = Paint()
120     private var mousePaint = Paint()
121     private var drawingTabletPaint = Paint()
122 
initnull123     private fun init() {
124         touchPaint.color = Color.RED
125         touchPaint.strokeWidth = 5f
126         touchpadPaint.color = Color.BLACK
127         touchpadPaint.strokeWidth = 5f
128         stylusPaint.color = Color.YELLOW
129         stylusPaint.strokeWidth = 5f
130         mousePaint.color = Color.BLUE
131         mousePaint.strokeWidth = 5f
132         drawingTabletPaint.color = Color.GREEN
133         drawingTabletPaint.strokeWidth = 5f
134         setOnHoverListener { _, event -> processHoverEvent(event); true }
135     }
136 
resolvePaintnull137     private fun resolvePaint(event: MotionEvent): Paint? {
138         val inputDevice = InputDevice.getDevice(event.deviceId)
139         val isTouchpadDevice = inputDevice != null && inputDevice.supportsSource(SOURCE_TOUCHPAD)
140         return if (event.isFromSource(SOURCE_STYLUS or SOURCE_MOUSE)) {
141             // External stylus / drawing tablet
142             drawingTabletPaint
143         } else if (event.isFromSource(SOURCE_TOUCHSCREEN) && !event.isFromSource(SOURCE_STYLUS)) {
144             // Touchscreen event
145             touchPaint
146         } else if (event.isFromSource(SOURCE_MOUSE) &&
147             (event.isFromSource(SOURCE_TOUCHPAD) || isTouchpadDevice)) {
148             // Touchpad event
149             touchpadPaint
150         } else if (event.isFromSource(SOURCE_MOUSE)) {
151             // Mouse event
152             mousePaint
153         } else if (event.isFromSource(SOURCE_STYLUS)) {
154             // Stylus event
155             stylusPaint
156         } else {
157             // Drop the event
158             null
159         }
160     }
161 
processTouchEventnull162     private fun processTouchEvent(event: MotionEvent) {
163         scaleGestureDetector.onTouchEvent(event)
164         if (event.actionMasked == ACTION_DOWN) {
165             touchEvents.remove(event.deviceId)
166             myState?.state = PointerState.DOWN
167         } else if (event.actionMasked == ACTION_UP || event.actionMasked == ACTION_CANCEL) {
168             myState?.state = PointerState.NONE
169         }
170         val paint = resolvePaint(event)
171         if (paint != null) {
172             val vec = touchEvents.getOrPut(event.deviceId) { Vector<Pair<MotionEvent, Paint>>() }
173             val size = myState?.lineSize ?: 5f
174             paint.strokeWidth = size
175             vec.add(Pair(MotionEvent.obtain(event), Paint(paint)))
176             invalidate()
177         }
178     }
179 
processHoverEventnull180     private fun processHoverEvent(event: MotionEvent) {
181         hoverEvents.remove(event.deviceId)
182         if (event.actionMasked != ACTION_HOVER_EXIT) {
183             hoverEvents[event.deviceId] = MotionEvent.obtain(event)
184             myState?.state = PointerState.HOVER
185         } else {
186             myState?.state = PointerState.NONE
187         }
188         invalidate()
189     }
190 
onTouchEventnull191     override fun onTouchEvent(event: MotionEvent): Boolean {
192         processTouchEvent(event)
193         return true
194     }
195 
onDrawnull196     public override fun onDraw(canvas: Canvas) {
197         super.onDraw(canvas)
198 
199         // Draw touch and stylus MotionEvents
200         for ((_, vec) in touchEvents ) {
201             for (i in 1 until vec.size) {
202                 drawLine(canvas, vec[i - 1].first, vec[i].first, vec[i].second)
203             }
204         }
205         // Draw hovers
206         for ((_, event) in hoverEvents ) {
207             val paint = resolvePaint(event)
208             if (paint != null) {
209                 val size = myState?.circleSize ?: 20f
210                 drawCircle(canvas, event, paint, size)
211             }
212         }
213     }
214 }
215