Android实现扩大View点击区域的三种方式
作者:_小马快跑_
在 Android 应用开发中,有时候需要扩大 View 的点击区域以提高用户交互的便利性,尤其是当视图元素较小或用户界面密集时。扩大点击区域可以让用户更容易点击目标,改善用户体验。以下提供几种扩大点击区域的思路。
方式一:增加padding
通过设置padding来增大点击区域,如:
<TextView android:id="@+id/tv_view_delegate2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@color/gray_holo_light" android:padding="20dp" android:text="TouchDelegate2" />
上面的代码通过在XML中设置padding来扩大点击区域,当然也可以通过代码设置setPadding来实现。虽然设置padding可以起到效果,但是如果使用不当可能会影响视图的布局和外观,比如对ImageView设置padding的话可能会挤压其形状,所以使用Padding扩大点击区域时需要确保不影响视图的布局和外观。
方式二:TouchDelegate
TouchDelegate 类是 Android 中的一个辅助类,用于扩展 View 的触摸区域,使其大于实际的 View 边界。这对于增加某些 UI 元素的触控便捷性非常有用,比如小按钮。
TouchDelegate 使用示例:
/** * 扩展方法,扩大点击区域 * NOTE: 需要保证目标targetView有父View,否则无法扩大点击区域 * * @param expandSize 扩大的大小,单位px */ fun View.expandTouchView(expandSize: Int = 10.dp2px()) { val parentView = (parent as? View) parentView?.post { val rect = Rect() getHitRect(rect) //getHitRect(rect)将视图在父容器中所占据的区域存储到rect中。 log("rect = $rect") rect.left -= expandSize rect.top -= expandSize rect.right += expandSize rect.bottom += expandSize log("expandRect = $rect") parentView.touchDelegate = TouchDelegate(rect, this) } }
在Activity中使用:
private val tvExpandTouch: TextView by id(R.id.tv_view_delegate) tvExpandTouch.run { expandTouchView(50.dp2px()) //扩大点击区域 setOnClickListener { showToast("通过TouchDelegate扩大点击区域") } }
上面就实现了View扩大点击区域,继续来看下TouchDelegate 的源码:
public class TouchDelegate { private View mDelegateView; //需要接收触摸事件的 View,即代理 View。 private Rect mBounds;//本地坐标中的代理 View 的边界,用于初始命中测试。 private Rect mSlopBounds;//增加一定范围的 mBounds,用于追踪触摸事件是否应被视为在代理 View 内。 @UnsupportedAppUsage private boolean mDelegateTargeted; private TouchDelegateInfo mTouchDelegateInfo; public TouchDelegate(Rect bounds, View delegateView) { mBounds = bounds; mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop(); mSlopBounds = new Rect(bounds); mSlopBounds.inset(-mSlop, -mSlop); mDelegateView = delegateView; } //接收并处理触摸事件。若事件在 mBounds 内,则会将其转发到 mDelegateView。 public boolean onTouchEvent(@NonNull MotionEvent event) { int x = (int)event.getX(); int y = (int)event.getY(); boolean sendToDelegate = false; boolean hit = true; boolean handled = false; switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: mDelegateTargeted = mBounds.contains(x, y); sendToDelegate = mDelegateTargeted; break; case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_UP: case MotionEvent.ACTION_MOVE: sendToDelegate = mDelegateTargeted; if (sendToDelegate) { Rect slopBounds = mSlopBounds; if (!slopBounds.contains(x, y)) { hit = false; } } break; case MotionEvent.ACTION_CANCEL: sendToDelegate = mDelegateTargeted; mDelegateTargeted = false; break; } if (sendToDelegate) { if (hit) { // Offset event coordinates to be inside the target view event.setLocation(mDelegateView.getWidth() / 2, mDelegateView.getHeight() / 2); } else { // Offset event coordinates to be outside the target view (in case it does // something like tracking pressed state) int slop = mSlop; event.setLocation(-(slop * 2), -(slop * 2)); } //NOTE:重点看这里,最终是调用的代理View去处理事件了。 handled = mDelegateView.dispatchTouchEvent(event); } return handled; } }
View.java 源码中使用 TouchDelegate:
private TouchDelegate mTouchDelegate = null; public void setTouchDelegate(TouchDelegate delegate) { mTouchDelegate = delegate; } public TouchDelegate getTouchDelegate() { return mTouchDelegate; } public boolean onTouchEvent(MotionEvent event) { //...... if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } switch (action) { case MotionEvent.ACTION_DOWN: //...省略... case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: } }
可以看到在onTouchEvent中,优先去判断是否有TouchDelegate,如果有的话会先去找对应的代理View去处理事件。使用TouchDelegate的注意事项:
- 目标View必须有父View;
- 给多个目标View扩大点击区域时,不能是同一个父View,从View类的源码中可以看到,设置setTouchDelegate时,会把之前的覆盖掉。
方式三:RectF & getLocationOnScreen
RectF 是一个用于表示浮点坐标的矩形区域的类,而 getLocationOnScreen 则用于获取视图在整个屏幕中的绝对坐标。结合两者,可以检查触摸事件是否在子视图的“扩展区域”内,然后执行相应的操作。代码示例:
class ParentInnerTouchView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr) { private val tvChildView: TextView init { inflate(context, R.layout.expand_touch_view, this) tvChildView = findViewById(R.id.tv_expand_view) tvChildView.setOnClickListener { showToast("扩大了点击事件") } } @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent?): Boolean { event?.let { ev -> if (ev.action == MotionEvent.ACTION_DOWN) { val isChildHit = isHitExpandChildView(tvChildView, Pair(ev.rawX, ev.rawY)) if (isChildHit) { //将事件传递给子控件 tvChildView.performClick() } } } return super.onTouchEvent(event) } /** * 判断是否点击到了子 View 的扩大区域 * @param childView 子 View * @param touchPair 点击的位置 (x, y) * @param expandSize 扩大区域的大小 * @return 是否命中 */ private fun isHitExpandChildView( childView: View, touchPair: Pair<Float, Float>, expandSize: Int = 50.dp2px() ): Boolean { // 获取子 View 在屏幕上的位置 val location = IntArray(2) childView.getLocationOnScreen(location) val childX = location[0].toFloat() val childY = location[1].toFloat() val touchX = touchPair.first val touchY = touchPair.second // 扩大点击区域 val rect = RectF() rect.set( childX - expandSize, childY - expandSize, childX + childView.width + expandSize, childY + childView.height + expandSize ) // 判断点击是否在扩大的子 View 区域内 return rect.contains(touchX, touchY) } }
- getLocationOnScreen: 用于获取子视图在屏幕上的绝对坐标,返回一个包含 x 和 y 坐标的数组。利用这些坐标计算出子视图在屏幕上的位置。
- RectF: 创建一个矩形区域,通过调用 set 方法扩展矩形的上下左右边界,从而扩大点击区域。
- onTouchEvent: 监听触摸事件,如果点击位置在扩大的区域内,则调用 performClick 触发子视图的点击事件。
以上就是Android实现扩大View点击区域的三种方式的详细内容,更多关于Android View点击区域的资料请关注脚本之家其它相关文章!