今天介紹下項目中用到的側滑刪除
- recycleview的側滑刪除
- 項目的具體應用
具體步驟:
1.recycleview垂直方向滑動,保證recycleview的item必須爲viewgroup,並且item佈局中的菜單view必須在最右邊(項目中默認向左滑動有效),出可見屏幕外,指定具體的寬度。LayoutManager採用LinearLayoutManager(也可用GridLayoutManager,只要是垂直方向就行)。item佈局如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="60dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="59dp"
android:textSize="15sp"
android:text="第幾個"
android:gravity="center"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/colorPrimaryDark"/>
</LinearLayout>
<TextView
android:id="@+id/tv_delete"
android:layout_width="60dp"
android:layout_height="60dp"
android:textSize="15sp"
android:gravity="center"
android:text="刪除"
android:textColor="@android:color/white"
android:background="@color/colorAccent"/>
</LinearLayout>
2.準備工作已好,接下來主要是對手勢的監聽:
- 手指按下,獲取當前子view(含菜單);關閉之前已打開的view(菜單)
- 手指移動,移動當前view,顯示隱藏的菜單
- 手指擡起,判斷是否顯示或隱藏菜單
- 隱藏的菜單完全顯示後,添加點擊監聽與回調
首先需要自定義RecycleView;之後子view用菜單進行代替;下面開始一一分析。
一、前三步都是對事件的處理,因此放在一起進行說明。在onInterceptTouchEvent()方法進行攔截和onTouchEvent()方法處理事件。
1.onInterceptTouchEvent()方法
①ACTION_DOWN
不能對ACTION_DOWN進行攔截(子view可能需要點擊),可以進行座標記錄,以及關閉非上一個已打開的菜單。
MotionEvent.ACTION_DOWN -> {
//關閉上一個菜單
closeNowMenu(e.x.toInt(), e.y.toInt())
//記錄座標值
updateLastXY(e.x, e.y)
}
private fun closeNowMenu(x: Int, y: Int) {
if (null != mMenuView && mMenuShowAllTag) {
if (!isNowMenu(x, y)) {
closeMenu(mMenuView!!)
mMenuShowAllTag = false
slideEffiectiveTag = false
}
}
}
private fun updateDownLastXY(x: Float, y: Float) {
downLastX = x
downLastY = y
}
private fun updateLastXY(x: Float, y: Float) {
lastX = x
lastY = y
}
②ACTION_MOVE
進行是否爲有效移動判斷,進行酌情攔截,哈哈。。。(見checkEffectiveSlideLength()方法)添加標誌位slideEffiectiveTag,標識此後所有的事件都和移動菜單有關進行攔截。並且需要確定當前觸摸的item(含菜單)mMenuView,( findMotionView()方法,見代碼,抄自Android的ABListView的方法,做了些修改)
private fun findMotionView(x: Int, y: Int) {
//檢查是否爲當前菜單
if (isNowMenu(x, y)) {
return
}
val frame = mTouchFrame
for (i in childCount - 1 downTo 0) {
val child = getChildAt(i)
if (child == mMenuView) {
continue
}
if (child.visibility == View.VISIBLE) {
child.getHitRect(frame)
if (frame.contains(x, y)) {
//當前觸碰的view
mMenuView = child as ViewGroup
}
}
}
}
/**
* 是否爲當前菜單
*/
private fun isNowMenu(x: Int, y: Int): Boolean {
mMenuView?.let {
it.getHitRect(mTouchFrame)
if (mTouchFrame.contains(x, y)) {
return true
}
}
return false
}
③ACTION_UP
若slideEffiectiveTag爲true則直接攔截。(之後一定要注意這個標誌的恢復初始化,我就是忘了,導致菜單的點擊事件無法響應,查了原因才知道ACTION_UP被攔截了,導致事件在傳遞過程中變成了ACTION_CANCEL)
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
when (e.action) {
MotionEvent.ACTION_DOWN -> {
//關閉上一個菜單
closeNowMenu(e.x.toInt(), e.y.toInt())
//記錄座標值
updateLastXY(e.x, e.y)
}
MotionEvent.ACTION_MOVE -> {
var slide = checkEffectiveSlideLength(e.x, e.y)
if (slide > 0f) {
//查找當前菜單
findMotionView(downLastX.toInt(), downLastY.toInt())
slideEffiectiveTag = true
return true
}
}
MotionEvent.ACTION_UP -> {
if (slideEffiectiveTag) {
return true
}
}
}
var onInterceptTouchEvent = super.onInterceptTouchEvent(e)
Log.e(TAG, "onInterceptTouchEvent: result=$onInterceptTouchEvent")
return onInterceptTouchEvent
}
2.onTouchEvent()方法
①ACTION_DOWN
執行此方法,說明子view沒有對事件進行消費,添加標誌位onTouchEventDownTag,用於當移動有效時獲取當前菜單
②ACTION_MOVE
進行是否爲有效移動判斷(checkEffectiveSlideLength()方法),這個有可能來自onInterceptTouchEvent對ACTION_MOVE的攔截或者onTouchEvent的ACTION_DOWN爲true。但是這一步都需要知道當前觸摸的菜單是哪一個。
如果onTouchEventDownTag爲true,則需要重新確認當前觸摸的item(含菜單)mMenuView。因爲onTouchEventDownTag爲true,說明事件ACTION_MOVE沒有經過onInterceptTouchEvent方法,則需要確認mMenuView。
然後進行移動操作(見moveToMenuView()方法),這裏藉助了scrollBy和scrollTo方法。這裏有一點需要注意下,當mMenuView水平滑動距離和手指的水平移動距離和大於mMenuView的寬度則直接完全展示菜單
若爲有效滑動或者當前菜單已展開,則消費此事件。還記得onInterceptTouchEvent方法中對ACTION_DOWN的處理麼(查看closeNowMenu()方法),噹噹前觸碰的菜單還是上一個時,沒有做關閉處理。因此此時遇到當前菜單展開,表示此事件還是和菜單滑動有關,所以進行消費。否則會導致recycleview響應上下滑動,有可能使已展開菜單滑出當前頁面。
MotionEvent.ACTION_MOVE -> {
//*****中間代碼忽略*****///
if (slideEffiectiveTag || mMenuShowAllTag) {
return true
}
}
/**
* 移動當前view
*/
private fun moveToMenuView(slide: Int) {
mMenuView?.let {
mMenuWidth = it.getChildAt(1).measuredWidth
if (it.scrollX + slide >= mMenuWidth) {
showMenu(it)
} else {
mMenuShowAllTag = false
it.scrollBy(slide, 0)
}
}
}
③ACTION_UP
恢復onTouchEventDownTag標誌位的初始值
若slideEffiectiveTag爲true即滑動有效。直接返回true,並且對mMenuView是否展開或隱藏進行判斷。這裏只是對mMenuView的露出部分進行了判斷;如果不小於mMenuView寬度的一半則顯示,否則隱藏。還有一點需要說明下,slideEffiectiveTag爲false時,但是有菜單完全展示了,需要將其關閉。這是防止點擊了item(含菜單)菜單之外的view,但其沒有對此事件進行消費。
/**
* 檢測有效的滑動距離
* 即是否符合我們要求的滑動
*/
private fun checkEffectiveSlideLength(x: Float, y: Float): Float {
var changeX = lastX - x
var changeY = lastY - y
Log.e(TAG, "checkEffectiveSlideLength: changeX=$changeX changeY=$changeY $mMinSlide")
if (changeX > 0 && changeX > Math.abs(changeY)) {//水平向右滑動
if (onTouchEventDownTag) {
findMotionView(downLastX.toInt(), downLastY.toInt())
//此標誌恢復初始值,已找到當前觸摸的菜單
onTouchEventDownTag = false
}
return changeX
}
updateLastXY(x, y)
return -1f
}
override fun onTouchEvent(e: MotionEvent): Boolean {
when (e.action) {
MotionEvent.ACTION_DOWN -> {
updateLastXY(e.x, e.y)
Log.e(TAG, "onTouchEvent: ACTION_DOWN")
//findMotionView(e.x.toInt(), e.y.toInt())
onTouchEventDownTag = true
}
MotionEvent.ACTION_MOVE -> {
Log.e(TAG, "onTouchEvent: ACTION_MOVE")
var slide = checkEffectiveSlideLength(e.x, e.y)
Log.e(TAG, "onTouchEvent: slide=$slide $slideEffiectiveTag")
if (slide > 0f) {
slideEffiectiveTag = true
moveToMenuView(slide.toInt())
return true
}
if (slideEffiectiveTag || mMenuShowAllTag) {
return true
}
}
MotionEvent.ACTION_UP -> {
Log.e(TAG, "onTouchEvent: ACTION_UP")
//此標誌恢復初始值
onTouchEventDownTag = false
if (slideEffiectiveTag) {
if (!mMenuShowAllTag)
upFinalMoveToMenuView()
//此標誌恢復初始值
slideEffiectiveTag = false
return true
} else {
//若沒有有效滑動,但已展開,則關閉菜單
if (mMenuShowAllTag) {
closeMenu()
}
}
}
}
var onTouchEvent = super.onTouchEvent(e)
Log.e(TAG, "onTouchEvent: result=$onTouchEvent")
return onTouchEvent
}
/**
* 菜單展開
*/
private fun showMenu(view: View) {
view.scrollTo(mMenuWidth, 0)
mMenuShowAllTag = true
}
/**
* 菜單關閉
*/
private fun closeMenu(view: View) {
view.scrollTo(0, 0)
Log.e(TAG, "closeMenu: ${view.hashCode()}")
mMenuShowAllTag = false
}
二、菜單的點擊回調
這個沒什麼好說的,直接在recycleview的adapter適配器中添加點擊監聽即可。將完成的自定義recycleview加入加項目中,查看效果,這些代碼就省略了,下面查看效果圖。需要注意的是點擊後別忘了調用closeMenu()方法。GitHub自定義recycleview代碼
至於其中的事件攔截與消費,本文不是重點。如有錯誤或指導,歡迎留言。
fun closeMenu() {
mMenuView?.let {
closeMenu(it)
}
}