Android实现扩大View点击区域的三种方式

 更新时间:2024年08月15日 08:27:58   作者:_小马快跑_  
在 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点击区域的资料请关注脚本之家其它相关文章!

相关文章

  • Android开发之图片切割工具类定义与用法示例

    Android开发之图片切割工具类定义与用法示例

    这篇文章主要介绍了Android开发之图片切割工具类定义与用法,结合实例形式分析了Android图片切割工具类的定义与简单使用方法,需要的朋友可以参考下
    2017-11-11
  • Android Studio 3.0中mipmap-anydpi-v26是什么东东

    Android Studio 3.0中mipmap-anydpi-v26是什么东东

    在Android Studio 3.0中一旦我们创建了一个项目,一个名为mipmap-anydpi-v26自动创建的文件夹在res文件夹下。它究竟能干什么?为什么我们需要这个?我们在开发时该如何利用它,下面通过本文给大家介绍下
    2017-12-12
  • Android基础之获取LinearLayout的宽高

    Android基础之获取LinearLayout的宽高

    LinearLayout是线性布局控件,它包含的子控件将以横向或竖向的方式排列,按照相对位置来排列所有的widgets或者其他的containers,超过边界时,某些控件将缺失或消失。有的时候,我们需要想获取LinearLayout宽高,下面通过这篇文章来跟着小编一起学习学习吧。
    2016-11-11
  • Android中ImageView实现选择本地图片并显示功能

    Android中ImageView实现选择本地图片并显示功能

    本文主要介绍了android中ImageView实现选择本地图片并显示功能的示例代码。具有很好的参考价值。下面跟着小编一起来看下吧
    2017-04-04
  • Android中FileProvider的各种场景应用详解

    Android中FileProvider的各种场景应用详解

    这篇文章主要为大家介绍了Android中FileProvider的各种场景应用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • Android 暂停和恢复Activity

    Android 暂停和恢复Activity

    在正常的应用程序使用,前台activity有时会被其他可视化组件遮挡,从而 造成activity的暂停。例如,当一个半透明的activity打开时(如在一个风格对话框),以前的activity就暂停了。只要 activity仍然是部分可见,但目前没有获得焦点,它就依然处于暂停状态
    2016-03-03
  • Android中google Zxing实现二维码与条形码扫描

    Android中google Zxing实现二维码与条形码扫描

    这篇文章主要介绍了Android中google Zxing实现二维码与条形码扫描的相关资料,需要的朋友可以参考下
    2017-05-05
  • Android开发中使用Intent打开第三方应用及验证可用性的方法详解

    Android开发中使用Intent打开第三方应用及验证可用性的方法详解

    这篇文章主要介绍了Android开发中使用Intent打开第三方应用及验证可用性的方法,结合实例形式分析了Android使用Intent打开第三方应用的三种常用方式及使用注意事项,需要的朋友可以参考下
    2017-11-11
  • Android GridView实现滚动到指定位置的方法

    Android GridView实现滚动到指定位置的方法

    这篇文章主要介绍了Android GridView实现滚动到指定位置的方法,本文介绍了4个相关的方法,分别对它们做了讲解,需要的朋友可以参考下
    2015-06-06
  • 解决EditText编辑时hint 在6.0 手机上显示不出来的问题

    解决EditText编辑时hint 在6.0 手机上显示不出来的问题

    下面小编就为大家带来一篇解决EditText编辑时hint 在6.0 手机上显示不出来的问题。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-05-05

最新评论