Android自定義view實(shí)現(xiàn)列表內(nèi)左滑刪除Item
前言
上一篇文章自定義了一個(gè)左滑刪除的RecyclerView,把view事件分發(fā)三個(gè)函數(shù)dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent實(shí)際運(yùn)用了一下,一些原理通過(guò)出現(xiàn)的bug還是挺能加深印象,并且后面還在優(yōu)化上用上了TouchSlop、VelocityTracker以及GestureDetector,但是真不配那個(gè)一個(gè)控件搞定安卓自定義view,所以我把上篇博客標(biāo)題改了,并且希望在接下來(lái)的時(shí)間里,通過(guò)幾個(gè)自定義view較全面的去學(xué)習(xí)自定義view的相關(guān)知識(shí),話不多說(shuō),下面開(kāi)始1
需求
上篇文章通過(guò)RecyclerView去實(shí)現(xiàn)了一個(gè)左滑的效果,后面突發(fā)奇想,既然能通過(guò)列表去實(shí)現(xiàn)item的左滑,那能不能通過(guò)item自己去實(shí)現(xiàn)左滑呢?這樣我們把item內(nèi)容寫(xiě)在自定義的layout里面就可以實(shí)現(xiàn)左滑了,聽(tīng)起來(lái)挺方便,于是就動(dòng)手做了,少說(shuō)多做總還是好的。
有了第一篇的內(nèi)容,item的左滑還是簡(jiǎn)單多了,主要就是讓item跟隨滑動(dòng),右邊自動(dòng)添加一個(gè)刪除按鈕就夠了吧,開(kāi)始我是這么想的,并總結(jié)了三點(diǎn)核心思想:
- 一個(gè)容器,左右兩部分,左邊外部導(dǎo)入,右邊刪除框自動(dòng)增加
- 在 View 右邊追加一個(gè)刪除框 ,需要在 View 內(nèi)攔截事件,根據(jù) x 軸滑動(dòng)距離滑動(dòng)
- 在 ConstraintLayout 內(nèi)部添加一個(gè)刪除框,左邊對(duì)其 parent 右邊
這里取巧了一下,繼承的 ConstraintLayout,這樣讓添加的刪除框?qū)R ConstraintLayout的右邊就行了。
運(yùn)行效果

編寫(xiě)代碼
代碼不多,就直接上代碼了,注釋寫(xiě)的很詳細(xì),后面再提下出現(xiàn)的主要問(wè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
/**
* 左劃刪除控件
* 能在控件實(shí)現(xiàn)左滑嗎?如何傳入自定義的布局?
* 思路:
* 1、一個(gè)容器,左右兩部分,左邊外部導(dǎo)入,右邊刪除框 x 增加層級(jí)
* 2、在 View 右邊追加一個(gè)刪除款 x 需要在 View 內(nèi)攔截事件
* 3、在 ConstraintLayout 內(nèi)部添加一個(gè)刪除框,左邊對(duì)其 parent 右邊
*
* @author silence
* @date 2022-09-27
*/
class LeftDeleteItemLayout : ConstraintLayout {
private val mDeleteView: View?
var mDeleteClickListener: OnClickListener? = null
//流暢滑動(dòng)
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è)置好位置對(duì)齊自身最右邊
private fun makeDeleteView(context: Context): View {
val deleteView = TextView(context)
//給當(dāng)前控件一個(gè)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的時(shí)候才會(huì)用到
MotionEvent.ACTION_DOWN -> mLastX = event.x
//攔截本控件內(nèi)的移動(dòng)事件
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")
//如果沒(méi)有收到down事件,不應(yīng)該移動(dòng)
if (mLastX == -1f) return
val dx = mLastX - e.x
//更新點(diǎn)擊的橫坐標(biāo)
mLastX = e.x
//檢查mItem移動(dòng)后應(yīng)該在[-deleteLength, 0]內(nèi)
val deleteWidth = mDeleteView!!.width
if ((scrollX + dx) <= deleteWidth && (scrollX + dx) >= 0) {
//觸發(fā)移動(dòng)
scrollBy(dx.toInt(), 0)
}
//如果一段時(shí)間沒(méi)有移動(dòng)時(shí)間,mLastX還沒(méi)被stopMove重置為-1,那就是移動(dòng)到其他地方了
//設(shè)置200毫秒沒(méi)有新事件就觸發(fā)stopMove
removeCallbacks(stopMoveRunnable)
postDelayed(stopMoveRunnable, 200)
}
private fun stopMove() {
//如果移動(dòng)過(guò)半了,應(yīng)該判定左滑成功
val deleteWidth = mDeleteView!!.width
if (abs(scrollX) >= deleteWidth / 2f) {
//觸發(fā)移動(dòng)至完全展開(kāi)
mScroller.startScroll(scrollX, 0, deleteWidth - scrollX, 0)
}else {
//如果移動(dòng)沒(méi)過(guò)半應(yīng)該恢復(fù)狀態(tài),則恢復(fù)到原來(lái)狀態(tài)
mScroller.startScroll(scrollX, 0, - scrollX, 0)
}
invalidate()
//清除狀態(tài)
mLastX = -1f
}
//流暢地滑動(dòng)
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()
}主要問(wèn)題
動(dòng)態(tài)生成TextView
這個(gè)主要就是通過(guò)代碼生成一個(gè)TextView,不是很難,提一下。
將TextView對(duì)齊到當(dāng)前容器右端
這里利用ConstraintLayout取巧做的還是不錯(cuò)的,因?yàn)槿绻约喝?shí)現(xiàn)一個(gè)在屏幕外的對(duì)齊,至少要在onMeasure中獲得寬度,再去onLayout里面擺放到右側(cè)屏幕外。
這里也有一些問(wèn)題,首先是設(shè)置動(dòng)態(tài)生成的TextView參數(shù),然后是設(shè)置ConstraintLayout內(nèi)的約束條件,因?yàn)榧s束標(biāo)記必須要用到id,還得為當(dāng)前控件生成一個(gè)id,最后就是做一個(gè)回調(diào)接口了。
滑動(dòng)出界問(wèn)題
還有一個(gè)沒(méi)有預(yù)料到的問(wèn)題是當(dāng)滑動(dòng)超過(guò)當(dāng)前view的范圍時(shí),ACTION_MOVE和ACTION_UP都無(wú)法接收到,這就沒(méi)法知道移動(dòng)是否結(jié)束了。這里因?yàn)槲覀兊淖远xview是一個(gè)viewgroup,所以沒(méi)法消耗ACTION_DOWN事件,所以后續(xù)的事件序列并不會(huì)交到當(dāng)前的item上,這就麻煩了,所以這個(gè)需求本質(zhì)上就是不合理的,但是還是要解決問(wèn)題吧!
這里我通過(guò)View類(lèi)的postDelayed,延遲運(yùn)行一個(gè)runnable去停止滑動(dòng),當(dāng)每次滑動(dòng)的時(shí)候又去停止這個(gè)runnable。整個(gè)邏輯運(yùn)行起來(lái)就是,滑動(dòng)沒(méi)有出界,移動(dòng)的時(shí)候先移除延遲的停止邏輯,再發(fā)送延遲的停止邏輯,直到ACTION_UP觸發(fā)停止,若滑動(dòng)出界了,沒(méi)有去移除延遲的停止邏輯,就會(huì)在一端時(shí)間后自動(dòng)觸發(fā)停止。
有點(diǎn)繞,但是還是挺簡(jiǎn)單的,里面的原理也簡(jiǎn)單講一下。實(shí)際上View的postDelayed會(huì)通過(guò)主線程的handler去延遲執(zhí)行,如果有了解handler機(jī)制,可以知道handler并不僅僅可以發(fā)送message,同樣也可以發(fā)送runnable,類(lèi)似移除message,同樣也可以移除runnable。
滑動(dòng)開(kāi)始判定
另一個(gè)預(yù)料之外的問(wèn)題是當(dāng)滑動(dòng)從其他item移動(dòng)到當(dāng)前item的時(shí)候,即使沒(méi)有收到ACTION_DOWN事件,也會(huì)觸發(fā)滑動(dòng),這個(gè)很不符合邏輯。我這就在stopMove里面將mLastX改為了-1,初始值也是-1,如果在moveItem中值是-1,就說(shuō)明沒(méi)有被ACTION_DOWN事件設(shè)定mLastX,即按下的時(shí)候并不在當(dāng)前item,應(yīng)當(dā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)
}
增加對(duì)ACTION_DOWN的攔截,因?yàn)槿绻鸄CTION_DOWN沒(méi)在view處有被處理的話,會(huì)被丟棄,如果被view攔截了的話,move事件又不會(huì)經(jīng)過(guò)onInterceptTouchEvent函數(shù)。真不知道當(dāng)時(shí)寫(xiě)的時(shí)候是怎么運(yùn)行通過(guò)的。。。
到此這篇關(guān)于Android自定義view實(shí)現(xiàn)列表內(nèi)左滑刪除Item的文章就介紹到這了,更多相關(guān)Android左滑刪除Item內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android動(dòng)態(tài)修改應(yīng)用圖標(biāo)與名稱的方法實(shí)例
這篇文章主要給大家介紹了關(guān)于Android動(dòng)態(tài)修改應(yīng)用圖標(biāo)與名稱的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-01-01
Flutter 插件url_launcher簡(jiǎn)介
最近項(xiàng)目需求是打開(kāi)一個(gè)連接跳轉(zhuǎn)到安卓或蘋(píng)果默認(rèn)的瀏覽器。雖然開(kāi)始一個(gè)簡(jiǎn)單的要求,其中的一個(gè)細(xì)節(jié)就是執(zhí)行打開(kāi)網(wǎng)頁(yè)這一操作后,不能看上去像在應(yīng)用內(nèi)部打開(kāi),看上去要在應(yīng)用外部打開(kāi),今天小編給大家介紹Flutter 插件url_launcher的相關(guān)知識(shí),感興趣的朋友一起看看吧2020-04-04
android實(shí)現(xiàn)切換日期左右無(wú)限滑動(dòng)效果
本篇內(nèi)容給大家分享了android開(kāi)發(fā)時(shí)候?qū)崿F(xiàn)自定義的日期無(wú)限左右滑動(dòng)效果以及控件使用的技巧。2017-11-11
Android如何通過(guò)Retrofit提交Json格式數(shù)據(jù)
本篇文章主要介紹了Android如何通過(guò)Retrofit提交Json格式數(shù)據(jù),具有一定的參考價(jià)值,有興趣的可以了解一下2017-08-08
Android開(kāi)發(fā)常用經(jīng)典代碼段集錦
這篇文章主要介紹了Android開(kāi)發(fā)常用經(jīng)典代碼段,涉及Android開(kāi)發(fā)過(guò)程中針對(duì)手機(jī)、聯(lián)系人、圖片、存儲(chǔ)卡等的相關(guān)操作技巧,非常簡(jiǎn)單實(shí)用,需要的朋友可以參考下2016-02-02
Android 實(shí)現(xiàn)調(diào)用系統(tǒng)照相機(jī)拍照和錄像的功能
這篇文章主要介紹了Android 實(shí)現(xiàn)調(diào)用系統(tǒng)照相機(jī)拍照和錄像的功能的相關(guān)資料,需要的朋友可以參考下2016-11-11

