In the App interface, some controls are too small to be easily clicked. I always solve this problem by adding padding. This is easy to pull a whole body, especially for complex interfaces, often change the size of one control, the position of other controls also move. Is there a better way to solve this problem? In reading the source code for touch events, I stumbled upon a more decoupled approach.

Read the source long knowledge series of articles as follows, the series from the source extract and use in the actual project.

  1. Read the source code long knowledge better RecyclerView | click listener

  2. Android custom controls | source there is treasure in the automatic line feed control

  3. Android custom controls | three implementation of little red dot (below)

  4. Reading knowledge source long | dynamic extension class and bind the new way of the life cycle

  5. Reading knowledge source long | Android caton true because “frame”?

  6. Read the source code long | knowledge can expand the clickable area

primers

Source code analysis of touch events can be click here, now quoted conclusions are as follows:

  • ActivityWhen the touch event is received, it is passed toPhoneWindowAnd pass it toDecorViewBy theDecorViewcallViewGroup.dispatchTouchEvent()Distribute from the top downACTION_DOWNTouch events.
  • ACTION_DOWNEvents throughViewGroup.dispatchTouchEvent()fromDecorViewThrough a number ofViewGroupIt goes on and on until it gets thereView.View.dispatchTouchEvent()Is invoked.
  • View.dispatchTouchEvent()Is the end of the delivery event, the beginning of the consumption event. It will be calledonTouchEvent()orOnTouchListener.onTouch()To consume events.
  • Each level can be passed inonTouchEvent()orOnTouchListener.onTouch()returntrueTo tell their parent control that the touch event was consumed. Only if the underlying control does not consume touch events does the parent control have a chance to consume them.
  • The delivery of touch events is a top-down “recursive” process from the root view, while the consumption of touch events is a bottom-up “recursive” process.

Touch the agent

The View’s consumption logic for touch events is concentrated in onTouchEvent(), which is the endpoint of touch event delivery and the starting point of consumption. But it actually passes the touch event to someone else:

public class View {
    // Touch the proxy
    private TouchDelegate mTouchDelegate = null;
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final intaction = event.getAction(); .// Distribute the touch event to the touch agent
        if(mTouchDelegate ! =null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true; }}}}Copy the code

In onTouchEvent(), the touch event is passed to the mTouchDelegate before being consumed. It is a touch proxy instance:

public class TouchDelegate {
    // Proxy control
    private View mDelegateView;
    // The area where the proxy control responds to touch events
    private Rect mBounds;
    
    // constructor
    public TouchDelegate(Rect bounds, View delegateView) { mBounds = bounds; mDelegateView = delegateView; . }// Handle touch events
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        int x = (int)event.getX();
        int y = (int)event.getY();
        // Whether to pass the touch event to the agent
        boolean sendToDelegate = false;
        boolean handled = false;

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                // If the DOWN event occurs in the specified region, all events are passed to it.sendToDelegate = mBounds.contains(x, y); .break; . }if (sendToDelegate) {
            // Change the position of the touch event to pretend that it happens in the center of the proxy control
            event.setLocation(mDelegateView.getWidth() / 2, mDelegateView.getHeight() / 2);  
            // Pass the touch event to the proxy control
            handled = mDelegateView.dispatchTouchEvent(event);
        }
        returnhandled; }}Copy the code

The area in the constructor of the touch proxy to pass in the proxy control and its response to the touch event. If the touch event falls within this region, the event is passed to the proxy control for consumption.

Therefore, you only need to artificially increase the response area of the proxy control to achieve the expansion of the click area:

val viewGroup: ViewGroup
val childView: View

// To get the position of the child control relative to the parent control, pSOT is required
viewGroup.post {
    val rect = Rect()
    // Gets the position of the child control relative to the parent control and records it in recT
    ViewGroupUtils.getDescendantRect(viewGroup, childView, rect)
    // Expand the rect by 100 pixels horizontally and vertically
    rect.inset(- 100, - 100)
    // Set the touch proxy for the parent control
    viewGroup.touchDelegate = TouchDelegate(childView, rect)
}
Copy the code

The touch agent must be set on the parent control, because the touch events of the child control are transmitted through the parent control, and only the touch agent in the parent control can handle the events first.

Enlarging the click area for two controls in a row with the above code does not work…

Custom touch agents

Because there’s only one TouchDelegate member in the View, and there’s only one delegate control in the TouchDelegate.

In order for the touch proxy to serve multiple controls, it has to be extended by inheritance:

// Multitouch proxy
class MultiTouchDelegate(bound: Rect? = null, delegateView: View) 
    : TouchDelegate(bound, delegateView) {
    // A container that holds multiple proxy controls and their touch areas
    val delegateViewMap = mutableMapOf<View, Rect>()
    // The current proxy control
    private var delegateView: View? = null
    
    // Add a proxy control
    fun addDelegateView(delegateView: View, rect: Rect) {
        delegateViewMap[delegateView] = rect
    }
    
    // Completely override to mask parent logic
    override fun onTouchEvent(event: MotionEvent): Boolean {
        val x = event.x.toInt()
        val y = event.y.toInt()
        var handled = false
        when (event.actionMasked) {
            MotionEvent.ACTION_DOWN -> {
                // Find the corresponding coordinate of the proxy control when DOWN occurs
                delegateView = findDelegateViewUnder(x, y)
            }
            MotionEvent.ACTION_CANCEL -> {
                delegateView = null}}// If the proxy control is found, all events are passed to it for consumptiondelegateView? .let { event.setLocation(it.width /2f, it.height / 2f)
            handled = it.dispatchTouchEvent(event)
        }
        return handled
    }

    // Returns the proxy control whose touch area contains the specified coordinates
    private fun findDelegateViewUnder(x: Int, y: Int): View? {
        delegateViewMap.forEach { entry -> if (entry.value.contains(x, y)) return entry.key }
        return null}}Copy the code

You can then enlarge the click area for multiple controls like this:

val viewGroup: ViewGroup
val childView1: View
val childView2: View
val multiTouchDelegate = MultiTouchDelegate(childView1)
viewGroup.touchDelegate = multiTouchDelegate

viewGroup.post {
    val rect1 = Rect()
    ViewGroupUtils.getDescendantRect(viewGroup, childView1, rect1)
    rect1.inset(- 100, - 100)
    multiTouchDelegate.addDelegateView(childView1, rect1)
    
    val rect2 = Rect()
    ViewGroupUtils.getDescendantRect(viewGroup, childView2, rect2)
    rect2.inset(- 200, - 200)
    multiTouchDelegate.addDelegateView(childView2, rect2)
}
Copy the code

Kotlin syntax sugar refactoring

This is still too expensive to use, and the business-friendly approach would be to pass only enlarged pixel values without worrying about implementation details such as “Rect object creation” and “touch proxy object creation”.

Let’s use Kotlin’s extension method to refactor:

// Add expand to View
fun View.expand(dx: Int, dy: Int) {
    // Put the newly defined proxy class inside the method. The caller does not need to know these details
    class MultiTouchDelegate(bound: Rect? = null, delegateView: View) : TouchDelegate(bound, delegateView) {
        val delegateViewMap = mutableMapOf<View, Rect>()
        private var delegateView: View? = null

        override fun onTouchEvent(event: MotionEvent): Boolean {
            val x = event.x.toInt()
            val y = event.y.toInt()
            var handled = false
            when (event.actionMasked) {
                MotionEvent.ACTION_DOWN -> {
                    delegateView = findDelegateViewUnder(x, y)
                }
                MotionEvent.ACTION_CANCEL -> {
                    delegateView = null} } delegateView? .let { event.setLocation(it.width /2f, it.height / 2f)
                handled = it.dispatchTouchEvent(event)
            }
            return handled
        }

        private fun findDelegateViewUnder(x: Int, y: Int): View? {
            delegateViewMap.forEach { entry -> if (entry.value.contains(x, y)) return entry.key }
            return null}}// Gets the parent control of the current control
    val parentView = parent as? ViewGroup
    // If the parent control is not a ViewGroup, return it directlyparentView ? :return

    // If the parent control does not set a touch delegate, build the MultiTouchDelegate and set it to it
    if (parentView.touchDelegate == null) parentView.touchDelegate = MultiTouchDelegate(delegateView = this)
    post {
        val rect = Rect()
        // Gets the region of the child control in the parent control
        ViewGroupUtils.getDescendantRect(parentView, this, rect)
        // Expand the response area
        rect.inset(- dx, - dy)
        // Add the child control as a proxy control to the MultiTouchDelegate
        (parentView.touchDelegate as? MultiTouchDelegate)? .delegateViewMap?.put(this, rect)
    }
}
Copy the code

The business layer can then easily expand the click area like this:

val childView1: View
val childView2: View

childView1.expand(100.100)
childView2.expand(200.200)
Copy the code

talk is cheap, show me the code

View.expand() is in the layout.kt file of this warehouse

Recommended reading

  1. “Recursion” and “Return” of Android Touch Event Distribution (1)
  2. “Recursion” and “Return” of Android Touch Event Distribution (II)
  3. Read the source code long | knowledge can expand the clickable area