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.
Read the source code long knowledge better RecyclerView | click listener
Android custom controls | source there is treasure in the automatic line feed control
Android custom controls | three implementation of little red dot (below)
Reading knowledge source long | dynamic extension class and bind the new way of the life cycle
Reading knowledge source long | Android caton true because “frame”?
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:
Activity
When the touch event is received, it is passed toPhoneWindow
And pass it toDecorView
By theDecorView
callViewGroup.dispatchTouchEvent()
Distribute from the top downACTION_DOWN
Touch events.ACTION_DOWN
Events throughViewGroup.dispatchTouchEvent()
fromDecorView
Through a number ofViewGroup
It 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 in
onTouchEvent()
orOnTouchListener.onTouch()
returntrue
To 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
- “Recursion” and “Return” of Android Touch Event Distribution (1)
- “Recursion” and “Return” of Android Touch Event Distribution (II)
- Read the source code long | knowledge can expand the clickable area