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

  1. 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
  1. Set various custom parameters on the initializer. Note that you must set draw in order to execute draw in ViewGroupsetWillNotDraw(false)
Init {setWillNotDraw(false) // property omitted}Copy the code
  1. 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.