今天介紹下項目中用到的側滑刪除
之前的文章只是實現了item的側滑,但在項目中需要添加滑動的優化以及以後的擴展
分析:
- 滑動優化是指從部分展開過度到安全展開,添加動畫機制或者使用Scroller控制滑動,使其不那麼生硬
- 添加移動速度,進行展開判斷
- 提供定製的item,封裝菜單view與內容view,各個部位的點擊
一、滑動優化
這裏本來想借助Scroller來控制滑動,但是發現關閉過程中打開,有衝突。想到了需要藉助item裏的的Scroller,進而更新。而不是recycleview的Scroller。這樣比較繁瑣,以此放棄採用動畫(ObjectAnimator)。有興趣的朋友可以嘗試下Scroller。
ObjectAnimator採用默認的插值器,因scrollTo方法的參數是Int類型,以此如下:
private val objectUpdateListener = object : ValueAnimator.AnimatorUpdateListener {
override fun onAnimationUpdate(animation: ValueAnimator) {
if (animation is ObjectAnimator) {
var target = animation.target as View
var slide = animation.animatedValue as Int
target.scrollTo(slide, 0)
}
}
}
private val closeAnimator: ObjectAnimator by lazy {
ObjectAnimator.ofInt(this@ItemSlideRecycleView, "slideClose", 0, 0)
.apply {
duration = 200
addUpdateListener(objectUpdateListener)
}
}
private val openAnimator: ObjectAnimator by lazy {
ObjectAnimator.ofInt(this@ItemSlideRecycleView, "slideOpen", 0, 0)
.apply {
duration = 200
addUpdateListener(objectUpdateListener)
}
}
private fun startCloseAnimator(view: View, start: Int, end: Int) {
closeAnimator.target = view
closeAnimator.setIntValues(start, end)
closeAnimator.start()
}
private fun startOpenAnimator(view: View, start: Int, end: Int) {
openAnimator.target = view
openAnimator.setIntValues(start, end)
openAnimator.start()
}
修改打開與關閉菜單方法:
/**
* 菜單展開
*/
private fun showMenu(view: View) {
startOpenAnimator(view, view.scrollX, mMenuWidth)
mMenuShowAllTag = true
}
/**
* 菜單關閉
*/
private fun closeMenu(view: View) {
if (closeAnimator.isRunning) {
closeAnimator.cancel()
(closeAnimator.target as View).scrollTo(0, 0)
}
startCloseAnimator(view, view.scrollX, 0)
Log.e(TAG, "closeMenu: ${view.hashCode()}")
mMenuShowAllTag = false
}
二、添加手指滑動速度判斷
當手指的移動速度大於一定值時,也認爲是打開菜單的意圖。所以在checkEffectiveSlideLength()添加判斷。
1.首先需要初始化相關類VelocityTracker
private val mVelocityTracker: VelocityTracker by lazy {
VelocityTracker.obtain()
}
2.添加速度判斷
/**
* 最小有效滑動速度
* 向左滑,速度爲負數
*/
private val MIN_SPEED = -400
/**
* 檢測滑動速度是否符合我們的要求
*/
private fun checkEffectiveSpeed(): Boolean {
mVelocityTracker.computeCurrentVelocity(1000)
return mVelocityTracker.xVelocity <= MIN_SPEED
}
3.我們需要在onInterceptTouchEvent方法處理ACTION_MOVE事件以及onTouchEvent方法處理ACTION_UP事件添加速度判斷。因爲一個決定是否攔截此事時間,一個判斷是否展開菜單,都需要速度的判斷。
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
mVelocityTracker.addMovement(e)
when (e.action) {
/*代碼省略*/
MotionEvent.ACTION_MOVE -> {
if (checkEffectiveSlide(e.x, e.y)) {
//查找當前菜單
findMotionView(downLastX.toInt(), downLastY.toInt())
slideEffiectiveTag = true
return true
}
}
/*代碼省略*/
}
var onInterceptTouchEvent = super.onInterceptTouchEvent(e)
Log.e(TAG, "onInterceptTouchEvent: result=$onInterceptTouchEvent")
return onInterceptTouchEvent
}
/**
* 檢測滑動是否符合我們的要求
*/
private fun checkEffectiveSlide(x: Float, y: Float): Boolean {
if (checkEffectiveSpeed()) {
return true
}
var changeX = lastX - x
var changeY = lastY - y
if (changeX > mMinSlide && changeX > Math.abs(changeY)) {//水平向右滑動
return true
}
return false
}
override fun onTouchEvent(e: MotionEvent): Boolean {
mVelocityTracker.addMovement(e)
when (e.action) {
/*代碼省略*/
MotionEvent.ACTION_UP -> {
Log.e(TAG, "onTouchEvent: ACTION_UP $slideEffiectiveTag")
//此標誌恢復初始值
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
}
/**
* 手指擡起,對目標view進行最後的移動
* 即決定菜單是否展開或關閉
*/
private fun upFinalMoveToMenuView() {
mMenuView?.let {
if (it.scrollX >= mMenuWidth / 2f || checkEffectiveSpeed()) {
showMenu(it)
} else {
closeMenu(it)
}
}
}
三、封裝內容與菜單以及點擊
這個說什麼好呢,好吧,先定義recycleview的適配器抽象類(ItemSlideAdapter),然後抽離相關方法。
1.指定容器viewgroup把內容和菜單分離。由適配器進行添加,這裏使用了LinearLayout,如下:
private fun createView(context: Context, contentView: View, menuView: View): ViewGroup {
return LinearLayout(context).apply {
layoutParams = RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT,
RecyclerView.LayoutParams.WRAP_CONTENT
)
orientation = LinearLayout.HORIZONTAL
addView(contentView)
var layoutParams = menuView.layoutParams
if (null == layoutParams || layoutParams.width <= 0) {
throw RuntimeException("getMenuView方法得到的view,必須設置固定的寬度")
}
addView(menuView)
}
}
2.定義內容Holder(ContentViewHolder)與菜單holder(MenuViewHolder)與內容view和菜單view進行綁定,有點類似listview的ViewHolder。這將在其子類實現。如下:
abstract class ContentViewHolder(val view: View)
abstract class MenuViewHolder(val view: View)
3.構造生成ContentViewHolder與MenuViewHolder抽象方法,提供viewType參數是爲了擴展。
//C : ItemSlideAdapter.ContentViewHolder, M : ItemSlideAdapter.MenuViewHolder
abstract fun getMenuViewHolder(parent: ViewGroup, viewType: Int): M
abstract fun getContentViewHolder(parent: ViewGroup, viewType: Int): C
4.創建RecyclerView.ViewHolder的實現類。因爲已經將內容和菜單分離下去了,item成了一個空殼,所以可以將其確定。但要包含ContentViewHolder和MenuViewHolder,如下:
class ItemSlideViewHolder(
view: ViewGroup,
var contentViewHolder: ContentViewHolder,
var menuViewHolder: MenuViewHolder
) : RecyclerView.ViewHolder(view)
5.這樣的話就可以onCreateViewHolder方法實現了,如下:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemSlideAdapter.ItemSlideViewHolder {
var contentViewHolder = getContentViewHolder(parent, viewType)
var menuViewHolder = getMenuViewHolder(parent, viewType)
return ItemSlideAdapter.ItemSlideViewHolder(
createView(
parent.context,
contentViewHolder.view,
menuViewHolder.view
), contentViewHolder, menuViewHolder
)
}
6.點擊的封裝,想了一下這個還是不加了吧,在抽象類的實現類裏自行加入。因爲菜單子view的數量不定,還有內容子view的數量也不定,哪些需要點擊也不定。除非都已確定,非要寫的話,需要添加好多方法,得不償失。
7.因此抽象類的全部代碼,如下
package com.xinheng.leftslidedeleterecycleview
import android.content.Context
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import java.lang.RuntimeException
/**
* Created by XinHeng on 2019/03/07.
* describe:
*/
abstract class ItemSlideAdapter<C : ItemSlideAdapter.ContentViewHolder, M : ItemSlideAdapter.MenuViewHolder> :
RecyclerView.Adapter<ItemSlideAdapter.ItemSlideViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemSlideAdapter.ItemSlideViewHolder {
var contentViewHolder = getContentViewHolder(parent, viewType)
var menuViewHolder = getMenuViewHolder(parent, viewType)
return ItemSlideAdapter.ItemSlideViewHolder(
createView(
parent.context,
contentViewHolder.view,
menuViewHolder.view
), contentViewHolder, menuViewHolder
)
}
override fun onBindViewHolder(holder: ItemSlideViewHolder, position: Int) {
var contentViewHolder = holder.contentViewHolder
var menuViewHolder = holder.menuViewHolder
onBindHolder(contentViewHolder, menuViewHolder, position)
}
private fun createView(context: Context, contentView: View, menuView: View): ViewGroup {
return LinearLayout(context).apply {
layoutParams = RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT,
RecyclerView.LayoutParams.WRAP_CONTENT
)
orientation = LinearLayout.HORIZONTAL
addView(contentView)
var layoutParams = menuView.layoutParams
if (null == layoutParams || layoutParams.width <= 0) {
throw RuntimeException("getMenuView方法得到的view,必須設置固定的寬度")
}
addView(menuView)
}
}
abstract fun onBindHolder(contentViewHolder: ContentViewHolder, menuViewHolder: MenuViewHolder, position: Int)
abstract fun getMenuViewHolder(parent: ViewGroup, viewType: Int): M
abstract fun getContentViewHolder(parent: ViewGroup, viewType: Int): C
class ItemSlideViewHolder(
view: ViewGroup,
var contentViewHolder: ContentViewHolder,
var menuViewHolder: MenuViewHolder
) : RecyclerView.ViewHolder(view)
abstract class ContentViewHolder(val view: View)
abstract class MenuViewHolder(val view: View)
}
8.提供一個例子,實現了多種菜單與多種內容搭配。GitHub項目地址,效果如下圖:
圖片待更新