Android自定義view實現(xiàn)列表內(nèi)左滑刪除Item
前言
上一篇文章自定義了一個左滑刪除的RecyclerView,把view事件分發(fā)三個函數(shù)dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent實際運(yùn)用了一下,一些原理通過出現(xiàn)的bug還是挺能加深印象,并且后面還在優(yōu)化上用上了TouchSlop、VelocityTracker以及GestureDetector,但是真不配那個一個控件搞定安卓自定義view,所以我把上篇博客標(biāo)題改了,并且希望在接下來的時間里,通過幾個自定義view較全面的去學(xué)習(xí)自定義view的相關(guān)知識,話不多說,下面開始1
需求
上篇文章通過RecyclerView去實現(xiàn)了一個左滑的效果,后面突發(fā)奇想,既然能通過列表去實現(xiàn)item的左滑,那能不能通過item自己去實現(xiàn)左滑呢?這樣我們把item內(nèi)容寫在自定義的layout里面就可以實現(xiàn)左滑了,聽起來挺方便,于是就動手做了,少說多做總還是好的。
有了第一篇的內(nèi)容,item的左滑還是簡單多了,主要就是讓item跟隨滑動,右邊自動添加一個刪除按鈕就夠了吧,開始我是這么想的,并總結(jié)了三點(diǎn)核心思想:
- 一個容器,左右兩部分,左邊外部導(dǎo)入,右邊刪除框自動增加
- 在 View 右邊追加一個刪除框 ,需要在 View 內(nèi)攔截事件,根據(jù) x 軸滑動距離滑動
- 在 ConstraintLayout 內(nèi)部添加一個刪除框,左邊對其 parent 右邊
這里取巧了一下,繼承的 ConstraintLayout,這樣讓添加的刪除框?qū)R ConstraintLayout的右邊就行了。
運(yùn)行效果
編寫代碼
代碼不多,就直接上代碼了,注釋寫的很詳細(xì),后面再提下出現(xiàn)的主要問題:
import android.annotation.SuppressLint import android.content.Context import android.graphics.Color import android.os.Build import android.util.AttributeSet import android.util.TypedValue import android.view.Gravity import android.view.MotionEvent import android.view.View import android.widget.Scroller import android.widget.TextView import androidx.annotation.RequiresApi import androidx.constraintlayout.widget.ConstraintLayout import kotlin.math.abs /** * 左劃刪除控件 * 能在控件實現(xiàn)左滑嗎?如何傳入自定義的布局? * 思路: * 1、一個容器,左右兩部分,左邊外部導(dǎo)入,右邊刪除框 x 增加層級 * 2、在 View 右邊追加一個刪除款 x 需要在 View 內(nèi)攔截事件 * 3、在 ConstraintLayout 內(nèi)部添加一個刪除框,左邊對其 parent 右邊 * * @author silence * @date 2022-09-27 */ class LeftDeleteItemLayout : ConstraintLayout { private val mDeleteView: View? var mDeleteClickListener: OnClickListener? = null //流暢滑動 private var mScroller = Scroller(context) //上次事件的橫坐標(biāo) private var mLastX = -1f //控制控件結(jié)束的runnable private val stopMoveRunnable: Runnable = Runnable { stopMove() } constructor(context: Context) : this(context, null, 0) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) @RequiresApi(Build.VERSION_CODES.LOLLIPOP) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) init { //kotlin的初始化函數(shù) mDeleteView = makeDeleteView(context) addView(mDeleteView) } //創(chuàng)建刪除框,設(shè)置好位置對齊自身最右邊 private fun makeDeleteView(context: Context): View { val deleteView = TextView(context) //給當(dāng)前控件一個id,用于刪除控件約束 this.id = generateViewId() //設(shè)置布局參數(shù) deleteView.layoutParams = LayoutParams( dp2px(context, 100f), 0 ).apply { //設(shè)置約束條件 leftToRight = id topToTop = id bottomToBottom = id } //設(shè)置其他參數(shù) deleteView.text = "刪除" deleteView.gravity = Gravity.CENTER deleteView.setTextColor(Color.WHITE) deleteView.textSize = sp2px(context,18f).toFloat() deleteView.setBackgroundColor(Color.RED) //設(shè)置點(diǎn)擊回調(diào) deleteView.setOnClickListener(mDeleteClickListener) return deleteView } //攔截事件 override fun onInterceptTouchEvent(event: MotionEvent?): Boolean { event?.let { when(event.action) { //down事件記錄x,不攔截,當(dāng)move的時候才會用到 MotionEvent.ACTION_DOWN -> mLastX = event.x //攔截本控件內(nèi)的移動事件 MotionEvent.ACTION_MOVE -> return true } } return super.onInterceptTouchEvent(event) } //處理事件 @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent?): Boolean { event?.let { when(event.action) { MotionEvent.ACTION_MOVE -> moveItem(event) MotionEvent.ACTION_UP -> stopMove() } } return super.onTouchEvent(event) } private fun moveItem(e: MotionEvent) { //Log.e("TAG", "moveItem: mLastX=$mLastX") //如果沒有收到down事件,不應(yīng)該移動 if (mLastX == -1f) return val dx = mLastX - e.x //更新點(diǎn)擊的橫坐標(biāo) mLastX = e.x //檢查mItem移動后應(yīng)該在[-deleteLength, 0]內(nèi) val deleteWidth = mDeleteView!!.width if ((scrollX + dx) <= deleteWidth && (scrollX + dx) >= 0) { //觸發(fā)移動 scrollBy(dx.toInt(), 0) } //如果一段時間沒有移動時間,mLastX還沒被stopMove重置為-1,那就是移動到其他地方了 //設(shè)置200毫秒沒有新事件就觸發(fā)stopMove removeCallbacks(stopMoveRunnable) postDelayed(stopMoveRunnable, 200) } private fun stopMove() { //如果移動過半了,應(yīng)該判定左滑成功 val deleteWidth = mDeleteView!!.width if (abs(scrollX) >= deleteWidth / 2f) { //觸發(fā)移動至完全展開 mScroller.startScroll(scrollX, 0, deleteWidth - scrollX, 0) }else { //如果移動沒過半應(yīng)該恢復(fù)狀態(tài),則恢復(fù)到原來狀態(tài) mScroller.startScroll(scrollX, 0, - scrollX, 0) } invalidate() //清除狀態(tài) mLastX = -1f } //流暢地滑動 override fun computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.currX, mScroller.currY) postInvalidate() } } //單位轉(zhuǎn)換 @Suppress("SameParameterValue") private fun dp2px(context: Context, dpVal: Float): Int { return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, dpVal, context.resources .displayMetrics ).toInt() } @Suppress("SameParameterValue") private fun sp2px(context: Context, spVal: Float): Int { val fontScale = context.resources.displayMetrics.scaledDensity return (spVal * fontScale + 0.5f).toInt() }
主要問題
動態(tài)生成TextView
這個主要就是通過代碼生成一個TextView,不是很難,提一下。
將TextView對齊到當(dāng)前容器右端
這里利用ConstraintLayout取巧做的還是不錯的,因為如果要自己去實現(xiàn)一個在屏幕外的對齊,至少要在onMeasure中獲得寬度,再去onLayout里面擺放到右側(cè)屏幕外。
這里也有一些問題,首先是設(shè)置動態(tài)生成的TextView參數(shù),然后是設(shè)置ConstraintLayout內(nèi)的約束條件,因為約束標(biāo)記必須要用到id,還得為當(dāng)前控件生成一個id,最后就是做一個回調(diào)接口了。
滑動出界問題
還有一個沒有預(yù)料到的問題是當(dāng)滑動超過當(dāng)前view的范圍時,ACTION_MOVE和ACTION_UP都無法接收到,這就沒法知道移動是否結(jié)束了。這里因為我們的自定義view是一個viewgroup,所以沒法消耗ACTION_DOWN事件,所以后續(xù)的事件序列并不會交到當(dāng)前的item上,這就麻煩了,所以這個需求本質(zhì)上就是不合理的,但是還是要解決問題吧!
這里我通過View類的postDelayed,延遲運(yùn)行一個runnable去停止滑動,當(dāng)每次滑動的時候又去停止這個runnable。整個邏輯運(yùn)行起來就是,滑動沒有出界,移動的時候先移除延遲的停止邏輯,再發(fā)送延遲的停止邏輯,直到ACTION_UP觸發(fā)停止,若滑動出界了,沒有去移除延遲的停止邏輯,就會在一端時間后自動觸發(fā)停止。
有點(diǎn)繞,但是還是挺簡單的,里面的原理也簡單講一下。實際上View的postDelayed會通過主線程的handler去延遲執(zhí)行,如果有了解handler機(jī)制,可以知道handler并不僅僅可以發(fā)送message,同樣也可以發(fā)送runnable,類似移除message,同樣也可以移除runnable。
滑動開始判定
另一個預(yù)料之外的問題是當(dāng)滑動從其他item移動到當(dāng)前item的時候,即使沒有收到ACTION_DOWN事件,也會觸發(fā)滑動,這個很不符合邏輯。我這就在stopMove里面將mLastX改為了-1,初始值也是-1,如果在moveItem中值是-1,就說明沒有被ACTION_DOWN事件設(shè)定mLastX,即按下的時候并不在當(dāng)前item,應(yīng)當(dāng)舍棄滑動。
后續(xù)訂正
onTouchEvent有誤
//處理事件 @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { when(event.action) { MotionEvent.ACTION_DOWN -> return true MotionEvent.ACTION_MOVE -> moveItem(event) MotionEvent.ACTION_UP -> stopMove() } return super.onTouchEvent(event) }
增加對ACTION_DOWN的攔截,因為如果ACTION_DOWN沒在view處有被處理的話,會被丟棄,如果被view攔截了的話,move事件又不會經(jīng)過onInterceptTouchEvent函數(shù)。真不知道當(dāng)時寫的時候是怎么運(yùn)行通過的。。。
到此這篇關(guān)于Android自定義view實現(xiàn)列表內(nèi)左滑刪除Item的文章就介紹到這了,更多相關(guān)Android左滑刪除Item內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android動態(tài)修改應(yīng)用圖標(biāo)與名稱的方法實例
這篇文章主要給大家介紹了關(guān)于Android動態(tài)修改應(yīng)用圖標(biāo)與名稱的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-01-01Android如何通過Retrofit提交Json格式數(shù)據(jù)
本篇文章主要介紹了Android如何通過Retrofit提交Json格式數(shù)據(jù),具有一定的參考價值,有興趣的可以了解一下2017-08-08Android 實現(xiàn)調(diào)用系統(tǒng)照相機(jī)拍照和錄像的功能
這篇文章主要介紹了Android 實現(xiàn)調(diào)用系統(tǒng)照相機(jī)拍照和錄像的功能的相關(guān)資料,需要的朋友可以參考下2016-11-11