在 ViewGroup
中处理触摸事件时要特别注意,因为 ViewGroup
通常会有子项作为与 ViewGroup
本身不同触摸事件的目标。为了确保每个视图都能正确接收专门发给它的轻触事件,请替换 onInterceptTouchEvent()
方法。
请参阅以下相关资源:
在 ViewGroup 中拦截触摸事件
每当在 ViewGroup
的表面(包括其子级的表面)上检测到触摸事件时,就会调用 onInterceptTouchEvent()
方法。如果 onInterceptTouchEvent()
返回 true
,MotionEvent
会被拦截,这意味着它不会传递给子级,而是父级的 onTouchEvent()
方法。
onInterceptTouchEvent()
方法让父视图有机会先于其子项看到轻触事件。如果您从 onInterceptTouchEvent()
返回 true
,则之前处理轻触事件的子视图会收到 ACTION_CANCEL
,且从该时间点开始的事件会发送到父视图的 onTouchEvent()
方法以进行常规处理。onInterceptTouchEvent()
还可以返回 false
,并在事件沿着视图层次结构向下传递到常用目标时监视事件,这些目标使用自己的 onTouchEvent()
处理事件。
在以下代码段中,MyViewGroup
类扩展了 ViewGroup
。MyViewGroup
包含多个子视图。如果您在子视图上水平拖动手指,子视图将不再获取触摸事件,而 MyViewGroup
会通过滚动其内容来处理触摸事件。不过,如果您点按子视图中的按钮,或垂直滚动子视图,父视图不会拦截这些触摸事件,因为子视图是预期目标。在这些情况下,onInterceptTouchEvent()
会返回 false
,并且系统不会调用 MyViewGroup
类的 onTouchEvent()
。
Kotlin
class MyViewGroup @JvmOverloads constructor( context: Context, private val mTouchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop ) : ViewGroup(context) { ... override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { // This method only determines whether you want to intercept the motion. // If this method returns true, onTouchEvent is called and you can do // the actual scrolling there. return when (ev.actionMasked) { // Always handle the case of the touch gesture being complete. MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { // Release the scroll. mIsScrolling = false false // Don't intercept the touch event. Let the child handle it. } MotionEvent.ACTION_MOVE -> { if (mIsScrolling) { // You're currently scrolling, so intercept the touch event. true } else { // If the user drags their finger horizontally more than the // touch slop, start the scroll. // Left as an exercise for the reader. val xDiff: Int = calculateDistanceX(ev) // Touch slop is calculated using ViewConfiguration constants. if (xDiff > mTouchSlop) { // Start scrolling! mIsScrolling = true true } else { false } } } ... else -> { // In general, don't intercept touch events. The child view // handles them. false } } } override fun onTouchEvent(event: MotionEvent): Boolean { // Here, you actually handle the touch event. For example, if the action // is ACTION_MOVE, scroll this container. This method is only called if // the touch event is intercepted in onInterceptTouchEvent. ... } }
Java
public class MyViewGroup extends ViewGroup { private int mTouchSlop; ... ViewConfiguration vc = ViewConfiguration.get(view.getContext()); mTouchSlop = vc.getScaledTouchSlop(); ... @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // This method only determines whether you want to intercept the motion. // If this method returns true, onTouchEvent is called and you can do // the actual scrolling there. final int action = MotionEventCompat.getActionMasked(ev); // Always handle the case of the touch gesture being complete. if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { // Release the scroll. mIsScrolling = false; return false; // Don't intercept touch event. Let the child handle it. } switch (action) { case MotionEvent.ACTION_MOVE: { if (mIsScrolling) { // You're currently scrolling, so intercept the touch event. return true; } // If the user drags their finger horizontally more than the // touch slop, start the scroll. // Left as an exercise for the reader. final int xDiff = calculateDistanceX(ev); // Touch slop is calculated using ViewConfiguration constants. if (xDiff > mTouchSlop) { // Start scrolling. mIsScrolling = true; return true; } break; } ... } // In general, don't intercept touch events. The child view handles them. return false; } @Override public boolean onTouchEvent(MotionEvent ev) { // Here, you actually handle the touch event. For example, if the // action is ACTION_MOVE, scroll this container. This method is only // called if the touch event is intercepted in onInterceptTouchEvent. ... } }
请注意,ViewGroup
还提供了 requestDisallowInterceptTouchEvent()
方法。当子级不希望父级及其祖先使用 onInterceptTouchEvent()
拦截触摸事件时,ViewGroup
会调用此方法。
处理 ACTION_OUTSIDE 事件
如果 ViewGroup
收到带有 ACTION_OUTSIDE
的 MotionEvent
,默认情况下,系统不会将该事件分派给其子对象。如需使用 ACTION_OUTSIDE
处理 MotionEvent
,请替换 dispatchTouchEvent(MotionEvent event)
以分派给相应的 View
,或者在相关 Window.Callback
中对其进行处理(例如 Activity
)。
使用 ViewConfiguration 常量
上述代码段使用当前的 ViewConfiguration
来初始化一个名为 mTouchSlop
的变量。您可以使用 ViewConfiguration
类访问 Android 系统常用的距离、速度和时间。
“Touch slop”是指在系统将手势解释为滚动之前,用户的轻触手势可以滑动的距离(以像素为单位)。Touch slop 通常用于防止在用户执行其他触摸操作(例如轻触屏幕上的元素)时发生意外滚动。
另外两种常用的 ViewConfiguration
方法为 getScaledMinimumFlingVelocity()
和 getScaledMaximumFlingVelocity()
。
这两个方法分别返回最小和最大速度,以启动以像素/秒为单位的快滑。例如:
Kotlin
private val vc: ViewConfiguration = ViewConfiguration.get(context) private val mSlop: Int = vc.scaledTouchSlop private val mMinFlingVelocity: Int = vc.scaledMinimumFlingVelocity private val mMaxFlingVelocity: Int = vc.scaledMaximumFlingVelocity ... MotionEvent.ACTION_MOVE -> { ... val deltaX: Float = motionEvent.rawX - mDownX if (Math.abs(deltaX) > mSlop) { // A swipe occurs, do something. } return false } ... MotionEvent.ACTION_UP -> { ... if (velocityX in mMinFlingVelocity..mMaxFlingVelocity && velocityY < velocityX) { // The criteria are satisfied, do something. } }
Java
ViewConfiguration vc = ViewConfiguration.get(view.getContext()); private int mSlop = vc.getScaledTouchSlop(); private int mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); private int mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); ... case MotionEvent.ACTION_MOVE: { ... float deltaX = motionEvent.getRawX() - mDownX; if (Math.abs(deltaX) > mSlop) { // A swipe occurs, do something. } ... case MotionEvent.ACTION_UP: { ... } if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity && velocityY < velocityX) { // The criteria are satisfied, do something. } }
扩展子视图的可轻触区域
Android 提供了 TouchDelegate
类,让父视图能够将子视图的可轻触区域扩展到子视图的边界之外。当子项必须很小但需要更大的触摸区域时,这非常有用。您还可以使用这种方法缩小子视图的触摸区域。
在以下示例中,ImageButton
是_delegate 视图,即父视图扩展的触摸区域的子视图。其布局文件如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/parent_layout" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" > <ImageButton android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@null" android:src="@drawable/icon" /> </RelativeLayout>
以下代码段会完成这些任务:
- 获取父视图,并在界面线程上发布
Runnable
。这样可以确保父视图在调用getHitRect()
方法之前对子视图进行布局。getHitRect()
方法在父视图的坐标中获取子视图的点击矩形(或可触摸区域)。 - 查找
ImageButton
子视图并调用getHitRect()
以获取子项可轻触区域的边界。 - 扩展
ImageButton
子视图的点击矩形的边界。 - 实例化
TouchDelegate
,并传入展开的点击矩形和ImageButton
子视图作为参数。 - 在父视图上设置
TouchDelegate
,以便将触摸委托边界内的触摸操作传送到子视图。
作为 ImageButton
子视图的轻触代理,父视图会收到所有轻触事件。如果触摸事件发生在子级的点击矩形内,则父级会将触摸事件传递给子级进行处理。
Kotlin
public class MainActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // Post in the parent's message queue to make sure the parent lays out // its children before you call getHitRect(). findViewById<View>(R.id.parent_layout).post { // The bounds for the delegate view, which is an ImageButton in this // example. val delegateArea = Rect() val myButton = findViewById<ImageButton>(R.id.button).apply { isEnabled = true setOnClickListener { Toast.makeText( this@MainActivity, "Touch occurred within ImageButton touch region.", Toast.LENGTH_SHORT ).show() } // The hit rectangle for the ImageButton. getHitRect(delegateArea) } // Extend the touch area of the ImageButton beyond its bounds on the // right and bottom. delegateArea.right += 100 delegateArea.bottom += 100 // Set the TouchDelegate on the parent view so that touches within // the touch delegate bounds are routed to the child. (myButton.parent as? View)?.apply { // Instantiate a TouchDelegate. "delegateArea" is the bounds in // local coordinates of the containing view to be mapped to the // delegate view. "myButton" is the child view that receives // motion events. touchDelegate = TouchDelegate(delegateArea, myButton) } } } }
Java
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Get the parent view. View parentView = findViewById(R.id.parent_layout); parentView.post(new Runnable() { // Post in the parent's message queue to make sure the parent lays // out its children before you call getHitRect(). @Override public void run() { // The bounds for the delegate view, which is an ImageButton in // this example. Rect delegateArea = new Rect(); ImageButton myButton = (ImageButton) findViewById(R.id.button); myButton.setEnabled(true); myButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Toast.makeText(MainActivity.this, "Touch occurred within ImageButton touch region.", Toast.LENGTH_SHORT).show(); } }); // The hit rectangle for the ImageButton. myButton.getHitRect(delegateArea); // Extend the touch area of the ImageButton beyond its bounds on // the right and bottom. delegateArea.right += 100; delegateArea.bottom += 100; // Instantiate a TouchDelegate. "delegateArea" is the bounds in // local coordinates of the containing view to be mapped to the // delegate view. "myButton" is the child view that receives // motion events. TouchDelegate touchDelegate = new TouchDelegate(delegateArea, myButton); // Set the TouchDelegate on the parent view so that touches // within the touch delegate bounds are routed to the child. if (View.class.isInstance(myButton.getParent())) { ((View) myButton.getParent()).setTouchDelegate(touchDelegate); } } }); } }