Android自定義view實(shí)現(xiàn)左滑刪除的RecyclerView詳解
概述
最近安卓自定義view的知識(shí)看的很熟,但是卻很久沒(méi)動(dòng)手了,這幾天用kotlin手撕了原先一個(gè)左滑刪除的RecyclerView,居然弄得有點(diǎn)懵逼。后面又慢慢改進(jìn)、加?xùn)|西,發(fā)現(xiàn)這樣一個(gè)例子下來(lái),自定義View以及事件分發(fā)的知識(shí)居然覆蓋的差不多了,所以有了寫(xiě)博客的想法。下面我會(huì)從我的思路一點(diǎn)點(diǎn)的寫(xiě)下去,碰到的各種問(wèn)題就是知識(shí)的實(shí)際應(yīng)用了,通過(guò)問(wèn)題學(xué)知識(shí),我覺(jué)得這樣的方式非常好!
需求
這里我要做的是一個(gè)左滑刪除列表項(xiàng)的功能,之前拿過(guò)一個(gè)別人的用,所以有了一點(diǎn)思路,但是不深刻。于是我開(kāi)始從零出發(fā),先寫(xiě)個(gè)大致思路再一步步去解決,首先肯定的是通過(guò)繼承RecyclerView去實(shí)現(xiàn),后面思路大致如下:
- 在 down 事件中,判斷在列表內(nèi)位置,得到對(duì)應(yīng) item
- 攔截 move 事件,item 跟隨滑動(dòng),最大距離為刪除按鈕長(zhǎng)度
- 在 up 事件中,確定最終狀態(tài),固定 item 位置
編寫(xiě)代碼I
根據(jù)上面三點(diǎn)思路,我刷刷地就寫(xiě)下了下面的代碼:
class SlideDeleteRecyclerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
//流暢滑動(dòng)
private var mScroller = Scroller(context)
//當(dāng)前選中item
private var mItem: ViewGroup? = null
//上次按下橫坐標(biāo)
private var mLastX = 0f
override fun onInterceptTouchEvent(e: MotionEvent?): Boolean {
e?.let {
when(e.action) {
MotionEvent.ACTION_DOWN -> {
//獲取點(diǎn)擊位置
getSelectItem(e)
//設(shè)置點(diǎn)擊的橫坐標(biāo)
mLastX = e.x
}
MotionEvent.ACTION_MOVE -> {
//不管左右都應(yīng)該讓item跟隨滑動(dòng)
moveItem(e)
//攔截事件
return true
}
MotionEvent.ACTION_UP -> {
//判斷結(jié)果
stopMove(e)
}
}
}
return super.onInterceptTouchEvent(e)
}
//滑動(dòng)結(jié)束
//版本一:判斷一下結(jié)束的位置,補(bǔ)充或恢復(fù)位置
private fun stopMove(e: MotionEvent) {
mItem?.let {
val dx = e.x - mLastX
//如果移動(dòng)過(guò)半了,應(yīng)該判定左滑成功
val deleteWidth = it.getChildAt(it.childCount - 1).width
if (abs(dx) >= deleteWidth / 2) {
//觸發(fā)移動(dòng)
val left = if (dx > 0) {
deleteWidth - dx
}else {
- deleteWidth + dx
}
mScroller.startScroll(0, 0, left.toInt(),0)
invalidate()
}else {
//如果移動(dòng)沒(méi)過(guò)半應(yīng)該恢復(fù)狀態(tài)
mScroller.startScroll(0, 0, - dx.toInt(),0)
invalidate()
}
//清除狀態(tài)
mLastX = 0f
mItem = null
}
}
//移動(dòng)item
//版本一:絕對(duì)值小于刪除按鈕長(zhǎng)度隨便移動(dòng),大于則不移動(dòng)
private fun moveItem(e: MotionEvent) {
mItem?.let {
val dx = e.x - mLastX
//這里默認(rèn)最后一個(gè)view是刪除按鈕
if (abs(dx) < it.getChildAt(it.childCount - 1).width) {
//觸發(fā)移動(dòng)
mScroller.startScroll(0, 0, dx.toInt(), 0)
invalidate()
}
}
}
//獲取點(diǎn)擊位置
//版本一:通過(guò)點(diǎn)擊的y坐標(biāo)除于item高度得出
private fun getSelectItem(e: MotionEvent) {
val firstChild = getChildAt(0)
firstChild?.let {
val pos = (e.x / firstChild.height).toInt()
mItem = getChildAt(pos) as ViewGroup
}
}
//流暢地滑動(dòng)
override fun computeScroll() {
if (mScroller.computeScrollOffset()) {
mItem?.scrollBy(mScroller.currX, mScroller.currY)
postInvalidate()
}
}
}注意啊,這里的代碼是沒(méi)法用的,滑動(dòng)后選不中正確的item,距離也有問(wèn)題,所以里面有很多問(wèn)題!
kotlin的構(gòu)造
其實(shí)上來(lái)最懵逼的就是kotlin的構(gòu)造函數(shù),自己寫(xiě)了幾次,感覺(jué)都不對(duì),還是搜了下,有兩種寫(xiě)法,我還是覺(jué)得使用JvmOverloads的比較方便,不過(guò)好像在API版本>21時(shí)還有個(gè)defStyleRes,我這就不相敘了,可以查資料。
獲取的item位置不對(duì)
這里獲取的item明顯不對(duì),其實(shí)這個(gè)問(wèn)題很好發(fā)現(xiàn),因?yàn)槭录膞是屏幕的x啊,這里使用列表去計(jì)算明顯不行,而且考慮了可見(jiàn)性嗎?考慮可滑動(dòng)隱藏了嗎?考慮了第一個(gè)item子顯示部分嗎?
結(jié)合上面這些問(wèn)題,應(yīng)該如何去正確獲取item的位置呢?看下面代碼:
private fun getSelectItem(e: MotionEvent) {
val frame = Rect()
forEach {
if (it.visibility != GONE) {
it.getHitRect(frame)
if (frame.contains(e.x.toInt(), e.y.toInt())) {
mItem = it as ViewGroup
}
}
}
}這里參考了別人的代碼,通過(guò)遍歷子item,檢查事件坐標(biāo)是否在其中,在的話得到選中的item,不再需要position了,還是挺好理解的。
移動(dòng)的計(jì)算不對(duì)
上面的代碼將mLastX只記錄down事件,而每次的是事件和dwon事件橫坐標(biāo)差值,明顯錯(cuò)了。
首先mLastX這里應(yīng)該記錄的是每個(gè)事件的x,包含move的事件,移動(dòng)的差值應(yīng)該是一個(gè)小的差值。
MotionEvent.ACTION_MOVE -> {
//移動(dòng)控件
moveItem(e)
//更新點(diǎn)擊的橫坐標(biāo)
mLastX = e.x
//攔截事件
return true
}
private fun moveItem(e: MotionEvent) {
mItem?.let {
val dx = mLastX - e.x
//檢查mItem移動(dòng)后應(yīng)該在[-deleteLength, 0]內(nèi)
val deleteWidth = it.getChildAt(it.childCount - 1).width
if ((it.scrollX + dx) <= deleteWidth && (it.scrollX + dx) >= 0) {
//觸發(fā)移動(dòng)
it.scrollBy(dx.toInt(), 0)
}
}
}
滑動(dòng)結(jié)束結(jié)束判斷不對(duì)
上面的mLastX修改后,滑動(dòng)結(jié)束結(jié)束的判斷不對(duì),而且原本就是不對(duì)的哈!mScroller的移動(dòng)就錯(cuò)了,正確的看下面:
private fun stopMove() {
mItem?.let {
//如果移動(dòng)過(guò)半了,應(yīng)該判定左滑成功
val deleteWidth = it.getChildAt(it.childCount - 1).width
if (abs(it.scrollX) >= deleteWidth / 2) {
//觸發(fā)移動(dòng)至完全展開(kāi)
mScroller.startScroll(it.scrollX, 0, - deleteWidth,0)
invalidate()
}else {
//如果移動(dòng)沒(méi)過(guò)半應(yīng)該恢復(fù)狀態(tài)
mScroller.startScroll(it.scrollX, 0, 0,0)
invalidate()
}
//清除狀態(tài)
mLastX = 0f
mItem = null
}
}編寫(xiě)代碼II
改完上面代碼大致就有了第二版,下面看全部代碼,看看還有什么問(wèn)題?。?/p>
class SlideDeleteRecyclerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
//流暢滑動(dòng)
private var mScroller = Scroller(context)
//當(dāng)前選中item
private var mItem: ViewGroup? = null
//上次按下橫坐標(biāo)
private var mLastX = 0f
override fun onInterceptTouchEvent(e: MotionEvent?): Boolean {
e?.let {
when(e.action) {
MotionEvent.ACTION_DOWN -> {
//獲取點(diǎn)擊位置
getSelectItem(e)
//設(shè)置點(diǎn)擊的橫坐標(biāo)
mLastX = e.x
}
MotionEvent.ACTION_MOVE -> {
//移動(dòng)控件
moveItem(e)
//更新點(diǎn)擊的橫坐標(biāo)
mLastX = e.x
//攔截事件
return true
}
MotionEvent.ACTION_UP -> {
//判斷結(jié)果
stopMove()
}
}
}
return super.onInterceptTouchEvent(e)
}
//滑動(dòng)結(jié)束
//版本一:判斷一下結(jié)束的位置,補(bǔ)充或恢復(fù)位置
//問(wèn)題:mLast不應(yīng)該是down的位置
//版本二:
private fun stopMove() {
mItem?.let {
//如果移動(dòng)過(guò)半了,應(yīng)該判定左滑成功
val deleteWidth = it.getChildAt(it.childCount - 1).width
if (abs(it.scrollX) >= deleteWidth / 2) {
//觸發(fā)移動(dòng)至完全展開(kāi)
mScroller.startScroll(it.scrollX, 0, - deleteWidth,0)
invalidate()
}else {
//如果移動(dòng)沒(méi)過(guò)半應(yīng)該恢復(fù)狀態(tài)
mScroller.startScroll(it.scrollX, 0, 0,0)
invalidate()
}
//清除狀態(tài)
mLastX = 0f
mItem = null
}
}
//移動(dòng)item
//版本一:絕對(duì)值小于刪除按鈕長(zhǎng)度隨便移動(dòng),大于則不移動(dòng)
//問(wèn)題:移動(dòng)方向反了,而且左右可以滑動(dòng),沒(méi)有限定住范圍,mLast只是記住down的位置
//版本二:通過(guò)整體移動(dòng)的數(shù)值,和每次更新的數(shù)值,判斷是否在范圍內(nèi),再移動(dòng)
private fun moveItem(e: MotionEvent) {
mItem?.let {
val dx = mLastX - e.x
//檢查mItem移動(dòng)后應(yīng)該在[-deleteLength, 0]內(nèi)
val deleteWidth = it.getChildAt(it.childCount - 1).width
if ((it.scrollX + dx) <= deleteWidth && (it.scrollX + dx) >= 0) {
//觸發(fā)移動(dòng)
it.scrollBy(dx.toInt(), 0)
}
}
}
//獲取點(diǎn)擊位置
//版本一:通過(guò)點(diǎn)擊的y坐標(biāo)除于item高度得出
//問(wèn)題:沒(méi)考慮列表項(xiàng)的可見(jiàn)性、列表滑動(dòng)的情況,并且x和屏幕有關(guān)不僅僅是列表
//版本二:通過(guò)遍歷子view檢查事件在哪個(gè)view內(nèi),得到點(diǎn)擊的item
private fun getSelectItem(e: MotionEvent) {
//獲得第一個(gè)可見(jiàn)的item的position
val frame = Rect()
forEach {
if (it.visibility != GONE) {
it.getHitRect(frame)
if (frame.contains(e.x.toInt(), e.y.toInt())) {
mItem = it as ViewGroup
}
}
}
}
//流暢地滑動(dòng)
override fun computeScroll() {
if (mScroller.computeScrollOffset()) {
mItem?.scrollBy(mScroller.currX, mScroller.currY)
postInvalidate()
}
}
}代碼改完,運(yùn)行,誒,怎么只能滑動(dòng)一小下?打斷點(diǎn)試一下,選中的item正確了,但是怎么ACTION_MOVE只觸發(fā)一次?怎么ACTION_UP不觸發(fā)呢?這里就要注意下ACTION_MOVE里的代碼:
MotionEvent.ACTION_MOVE -> {
//移動(dòng)控件
moveItem(e)
//更新點(diǎn)擊的橫坐標(biāo)
mLastX = e.x
//攔截事件
return true
}這里返回了true?攔截事件?那后續(xù)的一系列事件不就是被當(dāng)前view攔截了嗎?果然僅僅一個(gè)onInterceptTouchEvent是搞不定的啊!
其實(shí)這里還有一個(gè)隱藏問(wèn)題,computeScroll里面真的寫(xiě)對(duì)了嗎?scrollBy和scrollTo有了解嗎?
下面看再次改進(jìn)的代碼,主要就是改的上面兩點(diǎn),改動(dòng)篇幅有點(diǎn)大,就全貼出來(lái)了。
編寫(xiě)代碼III
class SlideDeleteRecyclerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
//流暢滑動(dòng)
private var mScroller = Scroller(context)
//當(dāng)前選中item
private var mItem: ViewGroup? = null
//上次按下橫坐標(biāo)
private var mLastX = 0f
override fun onInterceptTouchEvent(e: MotionEvent?): Boolean {
e?.let {
when(e.action) {
MotionEvent.ACTION_DOWN -> {
//獲取點(diǎn)擊位置
getSelectItem(e)
//設(shè)置點(diǎn)擊的橫坐標(biāo)
mLastX = e.x
}
MotionEvent.ACTION_MOVE -> {
//判斷是否攔截
return moveItem(e)
}
// MotionEvent.ACTION_UP -> {
// //判斷結(jié)果
// stopMove()
// }
}
}
return super.onInterceptTouchEvent(e)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(e: MotionEvent?): Boolean {
e?.let {
when(e.action) {
//攔截了ACTION_MOVE后,后面一系列event都會(huì)交到本view處理
MotionEvent.ACTION_MOVE -> {
//移動(dòng)控件
moveItem(e)
//更新點(diǎn)擊的橫坐標(biāo)
mLastX = e.x
}
MotionEvent.ACTION_UP -> {
//判斷結(jié)果
stopMove()
}
}
}
return super.onTouchEvent(e)
}
//滑動(dòng)結(jié)束
//版本一:判斷一下結(jié)束的位置,補(bǔ)充或恢復(fù)位置
//問(wèn)題:mLast不應(yīng)該是down的位置
//版本二:改進(jìn)結(jié)果判斷
//問(wèn)題:onInterceptTouchEvent的ACTION_UP不觸發(fā)
//版本三:改進(jìn)補(bǔ)充或恢復(fù)位置的邏輯
private fun stopMove() {
mItem?.let {
//如果移動(dòng)過(guò)半了,應(yīng)該判定左滑成功
val deleteWidth = it.getChildAt(it.childCount - 1).width
if (abs(it.scrollX) >= deleteWidth / 2f) {
//觸發(fā)移動(dòng)至完全展開(kāi)
mScroller.startScroll(it.scrollX, 0, deleteWidth - it.scrollX,0)
invalidate()
}else {
//如果移動(dòng)沒(méi)過(guò)半應(yīng)該恢復(fù)狀態(tài)
mScroller.startScroll(it.scrollX, 0, -it.scrollX,0)
invalidate()
}
//清除狀態(tài)
mLastX = 0f
//不能為null,后續(xù)流暢滑動(dòng)要用到
//mItem = null
}
}
//移動(dòng)item
//版本一:絕對(duì)值小于刪除按鈕長(zhǎng)度隨便移動(dòng),大于則不移動(dòng)
//問(wèn)題:移動(dòng)方向反了,而且左右可以滑動(dòng),沒(méi)有限定住范圍,mLast只是記住down的位置
//版本二:通過(guò)整體移動(dòng)的數(shù)值,和每次更新的數(shù)值,判斷是否在范圍內(nèi),再移動(dòng)
//問(wèn)題:onInterceptTouchEvent的ACTION_MOVE只觸發(fā)一次
//版本三:放在onTouchEvent內(nèi)執(zhí)行,并且在onInterceptTouchEvent給出一個(gè)攔截判斷
private fun moveItem(e: MotionEvent): Boolean {
mItem?.let {
val dx = mLastX - e.x
//檢查mItem移動(dòng)后應(yīng)該在[-deleteLength, 0]內(nèi)
val deleteWidth = it.getChildAt(it.childCount - 1).width
if ((it.scrollX + dx) <= deleteWidth && (it.scrollX + dx) >= 0) {
//觸發(fā)移動(dòng)
it.scrollBy(dx.toInt(), 0)
return true
}
}
return false
}
//獲取點(diǎn)擊位置
//版本一:通過(guò)點(diǎn)擊的y坐標(biāo)除于item高度得出
//問(wèn)題:沒(méi)考慮列表項(xiàng)的可見(jiàn)性、列表滑動(dòng)的情況,并且x和屏幕有關(guān)不僅僅是列表
//版本二:通過(guò)遍歷子view檢查事件在哪個(gè)view內(nèi),得到點(diǎn)擊的item
//問(wèn)題:沒(méi)有問(wèn)題,成功拿到了mItem
private fun getSelectItem(e: MotionEvent) {
//獲得第一個(gè)可見(jiàn)的item的position
val frame = Rect()
//防止點(diǎn)擊其他地方,保持上一個(gè)item
mItem = null
forEach {
if (it.visibility != GONE) {
it.getHitRect(frame)
if (frame.contains(e.x.toInt(), e.y.toInt())) {
mItem = it as ViewGroup
}
}
}
}
//流暢地滑動(dòng)
override fun computeScroll() {
if (mScroller.computeScrollOffset()) {
mItem?.scrollTo(mScroller.currX, mScroller.currY)
postInvalidate()
}
}
}把上面代碼運(yùn)行下,果然就十分完美了,可是是不是覺(jué)得沒(méi)徹底搞定?。縿e急下面我們?cè)偌狱c(diǎn)東西.
優(yōu)化
優(yōu)化一:TouchSlop
TouchSlop是一個(gè)移動(dòng)的最小距離,由系統(tǒng)提供,可以用它來(lái)判斷一個(gè)滑動(dòng)距離是否有效。
優(yōu)化二:VelocityTracker
VelocityTracker是一個(gè)速度計(jì)算的工具,由native提供,可以計(jì)算移動(dòng)像素點(diǎn)的速度,我們可以利用它判斷當(dāng)滑動(dòng)速度很快時(shí)也展開(kāi)刪除按鈕。
優(yōu)化三:GestureDetector
GestureDetector是手勢(shì)控制類,可以很方便的判斷各種手勢(shì),我們這可以設(shè)計(jì)它雙擊展開(kāi)刪除按鈕。
優(yōu)化后代碼
class SlideDeleteRecyclerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
//系統(tǒng)最小移動(dòng)距離
private val mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
//最小有效速度
private val mMinVelocity = 600
//增加手勢(shì)控制,雙擊快速完成側(cè)滑,還是為了練習(xí)
private var isDoubleClick = false
private var mGestureDetector: GestureDetector
= GestureDetector(context, object : GestureDetector.SimpleOnGestureListener(){
override fun onDoubleTap(e: MotionEvent?): Boolean {
e?.let { event->
getSelectItem(event)
mItem?.let {
val deleteWidth = it.getChildAt(it.childCount - 1).width
//觸發(fā)移動(dòng)至完全展開(kāi)deleteWidth
if (it.scrollX == 0) {
mScroller.startScroll(0, 0, deleteWidth, 0)
}else {
mScroller.startScroll(it.scrollX, 0, -it.scrollX, 0)
}
isDoubleClick = true
invalidate()
return true
}
}
//不進(jìn)行攔截,只是作為工具判斷下雙擊
return false
}
})
//使用速度控制器,增加側(cè)滑速度判定滑動(dòng)成功,主要為了是練習(xí)
//VelocityTracker 由 native 實(shí)現(xiàn),需要及時(shí)釋放內(nèi)存
private var mVelocityTracker: VelocityTracker? = null
//流暢滑動(dòng)
private var mScroller = Scroller(context)
//當(dāng)前選中item
private var mItem: ViewGroup? = null
//上次事件的橫坐標(biāo)
private var mLastX = 0f
//當(dāng)前RecyclerView被上層viewGroup分發(fā)到事件,所有事件都會(huì)通過(guò)dispatchTouchEvent給到
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
//
mGestureDetector.onTouchEvent(ev)
return super.dispatchTouchEvent(ev)
}
//viewGroup對(duì)子控件的事件攔截,一旦攔截,后續(xù)事件序列不會(huì)再調(diào)用onInterceptTouchEvent
override fun onInterceptTouchEvent(e: MotionEvent?): Boolean {
e?.let {
when (e.action) {
MotionEvent.ACTION_DOWN -> {
//這里的優(yōu)化會(huì)阻止雙擊滑動(dòng)的使用,實(shí)際也沒(méi)什么好優(yōu)化的
// //防止快速按下情況出問(wèn)題
// if (!mScroller.isFinished) {
// mScroller.abortAnimation()
// }
//獲取點(diǎn)擊位置
getSelectItem(e)
//設(shè)置點(diǎn)擊的橫坐標(biāo)
mLastX = e.x
}
MotionEvent.ACTION_MOVE -> {
//判斷是否攔截
//如果攔截了ACTION_MOVE,后續(xù)事件就不觸發(fā)onInterceptTouchEvent了
return moveItem(e)
}
//攔截了ACTION_MOVE,ACTION_UP也不會(huì)觸發(fā)
// MotionEvent.ACTION_UP -> {
// //判斷結(jié)果
// stopMove()
// }
}
}
return super.onInterceptTouchEvent(e)
}
//攔截后對(duì)事件的處理,或者子控件不處理,返回到父控件處理,在onTouch之后,在onClick之前
//如果不消耗,則在同一事件序列中,當(dāng)前View無(wú)法再次接受事件
//performClick會(huì)被onTouchEvent攔截,我們這不需要點(diǎn)擊,全都交給super實(shí)現(xiàn)去了
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(e: MotionEvent?): Boolean {
e?.let {
when (e.action) {
//沒(méi)有攔截,也不能攔截,所以不需要處理
// MotionEvent.ACTION_DOWN -> {}
//攔截了ACTION_MOVE后,后面一系列event都會(huì)交到本view處理
MotionEvent.ACTION_MOVE -> {
//移動(dòng)控件
moveItem(e)
//更新點(diǎn)擊的橫坐標(biāo)
mLastX = e.x
}
MotionEvent.ACTION_UP -> {
//判斷結(jié)果
stopMove()
}
}
}
return super.onTouchEvent(e)
}
//滑動(dòng)結(jié)束
//版本一:判斷一下結(jié)束的位置,補(bǔ)充或恢復(fù)位置
//問(wèn)題:mLast不應(yīng)該是down的位置
//版本二:改進(jìn)結(jié)果判斷
//問(wèn)題:onInterceptTouchEvent的ACTION_UP不觸發(fā)
//版本三:改進(jìn)補(bǔ)充或恢復(fù)位置的邏輯
private fun stopMove() {
mItem?.let {
//如果移動(dòng)過(guò)半了,應(yīng)該判定左滑成功
val deleteWidth = it.getChildAt(it.childCount - 1).width
//如果整個(gè)移動(dòng)過(guò)程速度大于600,也判定滑動(dòng)成功
//注意如果沒(méi)有攔截ACTION_MOVE,mVelocityTracker是沒(méi)有初始化的
var velocity = 0f
mVelocityTracker?.let { tracker ->
tracker.computeCurrentVelocity(1000)
velocity = tracker.xVelocity
}
//判斷結(jié)束情況,移動(dòng)過(guò)半或者向左速度很快都展開(kāi)
if ( (abs(it.scrollX) >= deleteWidth / 2f) || (velocity < - mMinVelocity) ) {
//觸發(fā)移動(dòng)至完全展開(kāi)
mScroller.startScroll(it.scrollX, 0, deleteWidth - it.scrollX, 0)
invalidate()
}else {
//如果移動(dòng)沒(méi)過(guò)半應(yīng)該恢復(fù)狀態(tài),或者向右移動(dòng)很快則恢復(fù)到原來(lái)狀態(tài)
mScroller.startScroll(it.scrollX, 0, -it.scrollX, 0)
invalidate()
}
//清除狀態(tài)
mLastX = 0f
//不能為null,后續(xù)mScroller要用到
//mItem = null
//mVelocityTracker由native實(shí)現(xiàn),需要及時(shí)釋放
mVelocityTracker?.apply {
clear()
recycle()
}
mVelocityTracker = null
}
}
//移動(dòng)item
//版本一:絕對(duì)值小于刪除按鈕長(zhǎng)度隨便移動(dòng),大于則不移動(dòng)
//問(wèn)題:移動(dòng)方向反了,而且左右可以滑動(dòng),沒(méi)有限定住范圍,mLast只是記住down的位置
//版本二:通過(guò)整體移動(dòng)的數(shù)值,和每次更新的數(shù)值,判斷是否在范圍內(nèi),再移動(dòng)
//問(wèn)題:onInterceptTouchEvent的ACTION_MOVE只觸發(fā)一次
//版本三:放在onTouchEvent內(nèi)執(zhí)行,并且在onInterceptTouchEvent給出一個(gè)攔截判斷
@SuppressLint("Recycle")
private fun moveItem(e: MotionEvent): Boolean {
mItem?.let {
val dx = mLastX - e.x
//最小的移動(dòng)距離應(yīng)該舍棄,onInterceptTouchEvent不攔截,onTouchEvent內(nèi)才更新mLastX
if(abs(dx) > mTouchSlop) {
//檢查mItem移動(dòng)后應(yīng)該在[-deleteLength, 0]內(nèi)
val deleteWidth = it.getChildAt(it.childCount - 1).width
if ((it.scrollX + dx) <= deleteWidth && (it.scrollX + dx) >= 0) {
//觸發(fā)移動(dòng)
it.scrollBy(dx.toInt(), 0)
//觸發(fā)速度計(jì)算
//這里Recycle不存在問(wèn)題,一旦返回true,就會(huì)攔截事件,就會(huì)到達(dá)ACTION_UP去回收
mVelocityTracker = mVelocityTracker ?: VelocityTracker.obtain()
mVelocityTracker!!.addMovement(e)
return true
}
}
}
return false
}
//獲取點(diǎn)擊位置
//版本一:通過(guò)點(diǎn)擊的y坐標(biāo)除于item高度得出
//問(wèn)題:沒(méi)考慮列表項(xiàng)的可見(jiàn)性、列表滑動(dòng)的情況,并且x和屏幕有關(guān)不僅僅是列表
//版本二:通過(guò)遍歷子view檢查事件在哪個(gè)view內(nèi),得到點(diǎn)擊的item
//問(wèn)題:沒(méi)有問(wèn)題,成功拿到了mItem
private fun getSelectItem(e: MotionEvent) {
//獲得第一個(gè)可見(jiàn)的item的position
val frame = Rect()
//防止點(diǎn)擊其他地方,保持上一個(gè)item
mItem = null
forEach {
if (it.visibility != GONE) {
it.getHitRect(frame)
if (frame.contains(e.x.toInt(), e.y.toInt())) {
mItem = it as ViewGroup
}
}
}
}
//流暢地滑動(dòng)
override fun computeScroll() {
if (mScroller.computeScrollOffset()) {
mItem?.scrollTo(mScroller.currX, mScroller.currY)
postInvalidate()
}
}
}TouchSlop、VelocityTracker和GestureDetector的用法都很簡(jiǎn)單,但是有一點(diǎn)必須得說(shuō)一下,那就是在dispatchTouchEvent中傳遞事件給GestureDetector,為什么呢?因?yàn)閛nInterceptTouchEvent攔截后就搜不到事件了,onTouchEvent的執(zhí)行和自身及子控件有關(guān),有不確定性,只有dispatchTouchEvent中的事件一定會(huì)收到!
總結(jié)
一篇文章下來(lái),代碼貼的有點(diǎn)多了,篇幅很長(zhǎng),但是如果仔細(xì)品的話,你會(huì)發(fā)現(xiàn)從事件分發(fā)到攔截都從問(wèn)題里面學(xué)到了,幾種滑動(dòng)方式以及滑動(dòng)的相對(duì)性也涉及了,坐標(biāo)系也有了一定理解,其他幾個(gè)工具TouchSlop、VelocityTracker和GestureDetector都用到了,還算可以吧!
到此這篇關(guān)于Android自定義view實(shí)現(xiàn)左滑刪除的RecyclerView詳解的文章就介紹到這了,更多相關(guān)Android RecyclerView內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android Webview與ScrollView的滾動(dòng)兼容及留白處理的方法
本篇文章主要介紹了Android Webview與ScrollView的滾動(dòng)兼容及留白處理的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11
Android即時(shí)通訊設(shè)計(jì)(騰訊IM接入和WebSocket接入)
本文主要介紹了Android即時(shí)通訊設(shè)計(jì)(騰訊IM接入和WebSocket接入),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04
Android開(kāi)發(fā)中Intent傳遞對(duì)象的方法分析
這篇文章主要介紹了Android開(kāi)發(fā)中Intent傳遞對(duì)象的方法,結(jié)合實(shí)例分析了Intent傳遞對(duì)象所涉及的具體方法、實(shí)現(xiàn)步驟與相關(guān)注意事項(xiàng),需要的朋友可以參考下2016-02-02
Android Gradle Build Error:Some file crunching failed, see l
這篇文章主要介紹了Android Gradle Build Error:Some file crunching failed, see logs for details的快速解決方法的相關(guān)資料,需要的朋友可以參考下2016-10-10
Android中ViewPager實(shí)現(xiàn)滑動(dòng)條及與Fragment結(jié)合的實(shí)例教程
ViewPager類主要被用來(lái)實(shí)現(xiàn)可滑動(dòng)的視圖功能,這里我們就來(lái)共同學(xué)習(xí)Android中ViewPager實(shí)現(xiàn)滑動(dòng)條及與Fragment結(jié)合的實(shí)例教程,需要的朋友可以參考下2016-06-06
Android定時(shí)開(kāi)機(jī)的流程詳解
這篇文章給大家分享了Android定時(shí)開(kāi)機(jī)及其實(shí)現(xiàn)流程,對(duì)此知識(shí)點(diǎn)有興趣的朋友,可以學(xué)習(xí)參考下。2018-07-07
Android實(shí)現(xiàn)二級(jí)列表購(gòu)物車功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)二級(jí)列表購(gòu)物車功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10
Android控制文本輸入框最多輸入10個(gè)字符長(zhǎng)度
這篇文章主要為大家詳細(xì)介紹了Android控制文本輸入框最多輸入10個(gè)字符長(zhǎng)度,即最多輸入5個(gè)漢字,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10

