2025年3月18日 星期二 甲辰(龙)年 月十七 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > 安卓(android)开发

在一个父View中,如何知道按下的位置是否在某个子View上。

时间:02-06来源:作者:点击数:52

前言

先上个效果图,如下:

在这里插入图片描述

公司项目上有一个需求:用RecyclerView实现了类型桌面的应用图标,长按应用图标后在图标上显示删除按钮,如果此时在其他空白的地方点击的话就隐藏删除按钮,就像苹果手机删除应用的那个效果。

长按应用出现删除按钮,点击删除按钮卸载应用,这些功能都没问题,都实现了,难点是点击应用图标之外的地方时隐藏删除按钮,这之外的地方总的来说包含两个区域:RecyclerView的空白区域、非RecyclerView区域。这说难也不难,在这个界面上除了RecyclerView还有很多其他的控件,比较笨的办法就是给每一个其他的View注册点击事件,在点击之后把删除按钮隐藏,但是这样的话感觉太笨了,于是想找一个通用的方法。

思路和踩坑过程

刚开始的思路是通过触摸事件,给根布局注册OnTouchListener来接收触摸事件,通过触摸事件来判断手指按下的位置上是否有应用图标,如果没有的话就隐藏删除按钮。后来发现注册OnTouchListener行不通,当我们按到根布局的一些按钮上时,这些按钮就已经消费了触摸事件,则我们的监听器就收不到事件,后来的解决方案是通过写一个根布局的子类,覆盖dispatchTouchEvent方法来接收事件,因为不管事件被哪个子View消费,最开始都是从dispatchTouchEvent分发出去的,所以可以确保收到触摸事件。刚开始是想在ACTION_UP事件中做处理的,后来发现也有问题,当在根布局的一些空白区域点击时,收不到ACTION_UP事件,是因为在ACTION_DOWN事件的时候没有任何的View消费它,所以后面的ACTION_UP事件就不会再分发过来了,所以只能改成在ACTION_DOWN事件的时候做处理。在ACTION_DOWN事件的时候,如何判断按下的位置是否在应用图标上呢?百度是找不到答案的,通过看ViewGroup的dispatchTouchEvent方法的源码,发现有一个方法叫isTransformedTouchPointInView(x, y, child, null),这个方法可以通过按下的坐标来判断它的某个子View是否也在这个按下的坐标点上,这个函数不是公开函数,需要使用反射来调用,后来发现这个函数也是有问题的,因为他只能判断它的直接子View,不能判断间接子View,我们的RecyclerView是根布局的间接子View,不能判断间直子View的原因是,它接收的x、y是相对坐标,是相对于接收触摸事件的那个View的左上角为0,0坐标起始点的,在真实的触摸事件分发的时候,父View把MotionEvent分发给子View的时候,会修改此对象的x、y的值,以使其是相对于子View的左上角坐标,所以MotionEvent对象在传递的过程中,x、y的值是一直在改变的。

不过还好的是,有了这个思路,我们就按这个思路重新实现即可,即通过按下的坐标判断子View是否也在这个坐标上。首先,我们需要统一坐标系,即MotionEvent中获取按下位置的坐标,要和获取子View位置的坐标必须是同一个坐标系,这样比较位置才有意义,通过百度和摸索,找到了相同的坐标系,如下代码,获取的坐标0,0点都是手机屏幕的左上角:

  • // 手指按下的坐标
  • val rawX = motionEvent.rawX
  • val rawY = motionEvent.rawY
  • // View的左上角坐标
  • val leftTopPoint = IntArray(2)
  • view.getLocationOnScreen(leftTopPoint)
  • // View的右下角坐标
  • val rightBottomPoint= IntArray(2)
  • rightBottomPoint[0] = leftTopPoint[0] + view.width
  • rightBottomPoint[1] = leftTopPoint[1] + view.height

如上代码,只要知道了手指按下的坐标,并知道一个view的左上角坐标和view的右下角坐标,我们就能知道手指是否按在了view的上面,公式如下:

  • if (rawX >= leftTopPoint[0] && rawY >= leftTopPoint[1] && rawX <= rightBottomPoint[0] && rawY <= rightBottomPoint[1]) {
  • // 手指按在了view的上面
  • }

实现Demo

  1. 效果展示,这里只实现判断按下位置是否在RecyclerView的item上,其他逻辑都不要。
    在这里插入图片描述
  2. 开启ViewBinding
    • android {
    • buildFeatures {
    • viewBinding true
    • }
    • }
  3. 布局
    • <?xml version="1.0" encoding="utf-8"?>
    • <cn.android666.draggridview.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    • xmlns:tools="http://schemas.android.com/tools"
    • android:layout_height="match_parent"
    • android:layout_width="match_parent"
    • android:orientation="vertical"
    • android:id="@+id/root_view"
    • android:gravity="center_horizontal">
    • <LinearLayout
    • android:id="@+id/ll_title"
    • android:layout_width="match_parent"
    • android:layout_height="50dp"
    • android:background="#0000FF"
    • android:gravity="center">
    • <TextView
    • android:layout_width="wrap_content"
    • android:layout_height="wrap_content"
    • android:textColor="@android:color/white"
    • android:text="我是标题"/>
    • </LinearLayout>
    • <androidx.recyclerview.widget.RecyclerView
    • android:id="@+id/recyclerView"
    • android:layout_width="match_parent"
    • android:layout_height="wrap_content"
    • tools:context=".MainActivity" />
    • <Button
    • android:layout_width="wrap_content"
    • android:layout_height="wrap_content"
    • android:layout_marginTop="16dp"
    • android:text="哈哈"/>
    • </cn.android666.draggridview.MyLinearLayout>
  4. 代码,一共2个类:MyLinearLayout、MainActivity
    • class MyLinearLayout(context: Context, attrs: AttributeSet): LinearLayout(context, attrs) {
    • private var touchListener: OnTouchListener? = null
    • override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
    • touchListener?.onTouch(this, ev)
    • return super.dispatchTouchEvent(ev)
    • }
    • fun setTouchListener(touchListener: OnTouchListener) {
    • this.touchListener = touchListener
    • }
    • }
    • class MainActivity : AppCompatActivity(), View.OnTouchListener {
    • private lateinit var binding: ActivityMainBinding
    • /** 指示是否在应用图标上按下了, 如果是在图标上按下了,则为true,否则为false。 */
    • private var pressDownOnAppIcon = false
    • /** 左上角坐标点 */
    • private val leftTopPoint = IntArray(2)
    • /** 右下角坐标点 */
    • private val rightBottomPoint = IntArray(2)
    • override fun onCreate(savedInstanceState: Bundle?) {
    • super.onCreate(savedInstanceState)
    • supportActionBar?.hide()
    • binding = ActivityMainBinding.inflate(layoutInflater)
    • setContentView(binding.root)
    • val myAdapter = MyAdapter()
    • binding.recyclerView.layoutManager = GridLayoutManager(this, 3)
    • binding.recyclerView.adapter = myAdapter
    • binding.rootView.setTouchListener(this)
    • }
    • class MyAdapter: RecyclerView.Adapter<MyViewHolder>() {
    • private val dataList = mutableListOf(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16)
    • private val random = Random(System.currentTimeMillis())
    • override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
    • val textView = LayoutInflater.from(parent.context).inflate(android.R.layout.simple_list_item_1, parent, false) as TextView
    • textView.setBackgroundColor(randomColor())
    • return MyViewHolder(textView)
    • }
    • private fun randomColor(): Int {
    • val red = random.nextInt(256)
    • val green = random.nextInt(256)
    • val blue = random.nextInt(256)
    • return Color.rgb(red, green, blue)
    • }
    • override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
    • holder.textView.text = dataList[position].toString()
    • }
    • override fun getItemCount() = dataList.size
    • }
    • class MyViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
    • @SuppressLint("ClickableViewAccessibility", "PrivateApi", "SoonBlockedPrivateApi")
    • override fun onTouch(v: View, ev: MotionEvent): Boolean {
    • if (ev.action != MotionEvent.ACTION_DOWN) {
    • // 不是Down事件时不需要做处理
    • return false
    • }
    • pressDownOnAppIcon = false // 初始化变量为false,下面代码开始查找是否按在了RecyclerView的item上
    • // 获取recyclerView左上角坐标点
    • binding.recyclerView.getLocationOnScreen(leftTopPoint)
    • // 通过左上角坐标和view的宽高计算出右下角坐标
    • rightBottomPoint[0] = leftTopPoint[0] + binding.recyclerView.width
    • rightBottomPoint[1] = leftTopPoint[1] + binding.recyclerView.height
    • if (ev.rawX >= leftTopPoint[0] && ev.rawY >= leftTopPoint[1] && ev.rawX <= rightBottomPoint[0] && ev.rawY <= rightBottomPoint[1]) {
    • // 需要再判断是否按在了RecyclerView的item上,如果是按在RecyclerView的空白区域也是要隐藏删除按钮的
    • for (index in 0 until binding.recyclerView.childCount) {
    • val child = binding.recyclerView.getChildAt(index)
    • // 获取子View的左上角坐标点
    • child.getLocationOnScreen(leftTopPoint)
    • // 通过左上角坐标和view的宽高计算出右下角坐标
    • rightBottomPoint[0] = leftTopPoint[0] + child.width
    • rightBottomPoint[1] = leftTopPoint[1] + child.height
    • if (ev.rawX >= leftTopPoint[0] && ev.rawY >= leftTopPoint[1] && ev.rawX <= rightBottomPoint[0] && ev.rawY <= rightBottomPoint[1]) {
    • pressDownOnAppIcon = true
    • toast("按在了RecyclerView的${(child as TextView).text}号item上")
    • break
    • }
    • }
    • if (!pressDownOnAppIcon) {
    • toast("按在了RecyclerView的空白区域")
    • }
    • } else {
    • toast("按在了其他控件上")
    • }
    • return false
    • }
    • private fun toast(text: String) = Toast.makeText(this, text, Toast.LENGTH_SHORT).apply { setGravity(Gravity.CENTER, 0, 0) }.show()
    • }
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门