您当前的位置:首页 > 计算机 > 编程开发 > 安卓(android)开发

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

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

前言

先上个效果图,如下:

在这里插入图片描述

公司项目上有一个需求:用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()
    }
    
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门