Recently I have some time to introduce the process of gesture capture and analog input. I was trying to write an automated task application that would take advantage of Android’s frictionless gesture input, so I came up with this component.
rendering
Realize the principle of
In fact, the Android library already provides a similar function for us to look at this GestureOverlayView, the capture method is actually similar to this control.
create
- Start by creating a custom View inherited from FrameLayout
class GestureCatchView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr){ private val pathColor: Int = Color.BLACK private val pathWidth: Int = 20 private val fadeDelay: Long = 0 private var timestemp: Long = 0 private var curGestureItem: GestureItem? = null // Current gesture // Collected gesture information private var collecting = false // Start collecting gesture identifiers, true: Add collected gestures to the collection. Private val gestureInfoList: ArrayList<GestureInfo> = ArrayList() private val gestureItemList: ArrayList<GestureItem> = ArrayList()}Copy the code
- Set various custom parameters on the initializer. Note that you must set draw in order to execute draw in ViewGroup
setWillNotDraw(false)
Init {setWillNotDraw(false) // property omitted}Copy the code
- Define an information class for each gesture to perform draw and fade gradient animations independently
inner class GestureItem { private val path = Path() private val paint = Paint().apply { strokeWidth = 20f color = PathColor style = Paint.Style.STROKE strokeJoin = Paint IsAntiAlias = true} private val animator = valueanimator.offloat (1f, 0f).apply {interpolator = AccelerateInterpolator() // Implement the effect of first slow then fast duration = 1500 addUpdateListener {paint. Alpha = (pathColor.alpha * it.animatedValue as Float).toInt() paint.strokeWidth = pathWidth * it.animatedValue as Float invalidate() } } private var lastX = 0f private var lastY = 0f private val points = arrayListOf<GesturePoint>() Private val delayTime: Long = if (! collecting || timestemp == 0L) 0 else System.currentTimeMillis() - timestemp }Copy the code
Gesture capture
Get the MotionEvent on onTouchEvent and draw the points to the view.
override fun onTouchEvent(event: MotionEvent): Boolean {
super.onTouchEvent(event)
processEvent(event)
return true
}
Copy the code
Process the received MotionEvent
private var curGestureItem: GestureItem? Private fun processEvent(event: MotionEvent) {when (event.action) {motionEvent.action_down -> {// When pressed is considered to be the start of a gesture, Create a GestureItem val newGestureItem = GestureItem() // add gestureitemlist.add (newGestureItem) // start drawing the first dot newGestureItem.startPath(event) curGestureItem = newGestureItem invalidate() } MotionEvent.ACTION_MOVE -> { val item = CurGestureItem // Add the point captured by each movement of the current gesture to the Item. Item? .run {addToPath(event) invalidate()}} motionEvent.action_up -> { CurGestureItem?. Run {endPath(event) invalidate()}}}}Copy the code
Gesture drawing
inner class GestureItem{ //..... Omitting fun startPath (event: MotionEvent) { val x = event.x val y = event.y path.moveTo(x, y) path.lineTo(x, Y) lastX = x lastY = y // Add (GesturePoint(x, y, event.eventTime))}}Copy the code
Move the finger each time to add the point to the collection point
fun addToPath(event: MotionEvent) {val x = event.x val y = event.y // Take the previous point as the control point val controlX = lastX val controlY = lastY // The midpoint between the previous point and the current point is the end point Val endX = (controlX + x) / 2 Val endY = (controlY + y) / 2 EndY) lastX = x lastY = y // Add (GesturePoint(x, y, event.eventTime))}Copy the code
Complete a gesture
fun endPath(event: MotionEvent) { points.add(GesturePoint(x, y, Event.eventtime) animator.startDelay = fadeDelay animator.start() val curTime = system.currentTimemillis () // Create a GestureInfo, // it contains the Gesture, color delay time, total Gesture time, and points collected. val gestureInfo = GestureInfo( gesture = Gesture().apply { addStroke(GestureStroke(ArrayList(points))) }, pathColor = pathColor, delayTime = delayTime, duration = curTime - timestemp - delayTime, points = ArrayList(points) ) if (collecting) gestureInfoList.add(gestureInfo) timestemp = curTime points.clear() }Copy the code
Finally, draw Path to Canvas
override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas)
for (item in gestureItemList) {
item.draw(canvas)
}
}
Copy the code
And at the end you can see something like this,
Gesture import and simulation
Finally, how to simulate the collected gestures as they were typed. We know that each MotionEvent has an eventTime, which comes from systemClock. uptimeMillis is the reference time.
GesturePoint(event.x, Event.y, event.eventTime). We can convert these collected points into motionEvents and execute them into processEvent to simulate the input effect
fun loadGestureInfoWithAnim(list: ArrayList<GestureInfo>) {
list.forEach { fakeMotionEvent.addAll(it.points) }
fakeMotionEventIndex = 0
post(fakeMotionEventRunnable)
}
Copy the code
Each point records the interval of execution, and these points are executed again in turn
private val fakeMotionEvent = arrayListOf<GesturePoint>() private var fakeMotionEventIndex = -1 private val fakeMotionEventRunnable = object : Runnable { override fun run() { if (fakeMotionEventIndex >= 0) { val point = fakeMotionEvent[fakeMotionEventIndex] val Action = when (fakeMotionEventIndex) {// The first point is to press the action 0 -> motionEvent.action_down in 1 until fakemotionEvent.size -> ACTION_UP} val event = MotionEvent. Obtain (point. Timestamp, point.timestamp, action, x, y, FakeMotionEventIndex ++ if (fakeMotionEventIndex == fakEmotionEvent.size) {fakeMotionEventIndex == fakEmotionEvent.size) { fakeMotionEventIndex = -1 return } val nextPoint = fakeMotionEvent[fakeMotionEventIndex] val delayTime = Nextpoint.timestamp - event.eventtime // delay execution of the nextPoint postDelayed(this, delayTime)}}}Copy the code
supplement
You might ask why not just save MotionEvent? This is because every MotionEvent we receive is actually the same object in most cases, and if you try to save it, you’ll end up with a set of motionEvent.action = ACTION_UP, which is the event you picked up at the end.
Look at the source code for MotionEvent: its constructor is private
private MotionEvent() {
}
Copy the code
Motionevents can only be obtained using the static method obtain
static public MotionEvent obtain(long downTime, long eventTime, int action, float x, float y, Int metaState) {return obtain(downTime, eventTime, action, x, y, 1.0f, 1.0f, metaState, 1.0f, 1.0f, 0, 0); }Copy the code
In the end, this method will be executed. It can be seen that most of the motionEvents we get are from gRecyclerTop, rather than being new
static private MotionEvent obtain() {
final MotionEvent ev;
synchronized (gRecyclerLock) {
ev = gRecyclerTop;
if (ev == null) {
return new MotionEvent();
}
gRecyclerTop = ev.mNext;
gRecyclerUsed -= 1;
}
ev.mNext = null;
ev.prepareForReuse();
return ev;
}
Copy the code
When a MotionEvent is consumed, recycle is eventually executed, which is then assigned to gRecyclerTop
public final void recycle() { super.recycle(); synchronized (gRecyclerLock) { if (gRecyclerUsed < MAX_RECYCLED) { gRecyclerUsed++; mNext = gRecyclerTop; gRecyclerTop = this; }}}Copy the code
Finally, attach the address of the project
GestureCatchView
Friends can give a little star support if they think it’s good. Any suggestions or questions are welcome to discuss.