先上个效果图,如下:
公司项目上有一个需求:用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的上面
}
android {
buildFeatures {
viewBinding true
}
}
<?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>
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()
}