前言
- 程序员:你看我写的RecyclerView多美呀,看看这颜色搭配多好看
- (突然有天)产品:把这个列表给我拖动起来,记得加个动画让他贴边
- 程序员(华丽的辞藻):那我走?
- 程序员:这RecyclerView业务遗留太久,改起来有点难度,5天工作量吧
- 产品(丰富的中国传统文化):那我走?
效果如下
主要功能
- 长按能拖动
- 列表的拖动和长按拖动不冲突
- 松手后能贴边
实现
整个方案实现原理最难在于拖动时候的处理:
- 如何判断长按后,然后开始拖动
- 长按的边界判断,不让界面拖出设置的边界
- 贴边动画实现
1、定义布局
在实际开发中,不仅只是拖动个RecyclerView就完事了,随着业务的增加,在RecyclerView上可能还有其他业务的布局,这里也是模仿实际业务,拖动的是整个父布局
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#50ffffff"
tools:context=".MainActivity">
<!--拖动的父布局-->
<FrameLayout
android:id="@+id/drag_view_root"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<!--父布局跟着动,他就会跟着走-->
<com.example.uitest.recylerview.DragRecyclerView
android:id="@+id/rv_drag"
android:layout_width="72dp"
android:layout_height="120dp"
android:background="#5000ff00" />
<!--依附在列表上的其他业务,也需要跟着走-->
......
</FrameLayout>
</FrameLayout>
2、定义变量
class DragRecyclerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
var TAG = "DragRecyclerView"
//总开关
private var isOpen = false
private var screenWidth = 0
private var screenHeight = 0
//计算用
private var lastLeft = 0F
private var lastTop = 0F
private var lastX = 0F
private var lastY = 0F
//拖动事件
var isDownStart: (() -> Unit)? = null
var isDragStart: (() -> Unit)? = null
var isDragUp: (() -> Unit)? = null
//真正拖动的父布局
private var dragView: View? = null
//是否拖动
private var drag = false
//限制拖动的区域
@Volatile
private var leftLimit = 0F
@Volatile
private var rightLimit = 0F
@Volatile
private var topLimit = 0F
@Volatile
private var bottomLimit = 0F
//默认在有右边
var isLeft = false
//默认在竖屏
private var isLandScape = false
//长按事件
var longPressRunnable: Runnable = Runnable {
Log.i(TAG, "长按")
drag = true
isDragStart?.invoke()
}
init {
screenWidth = resources.displayMetrics.widthPixels
screenHeight = resources.displayMetrics.heightPixels
}
//设置拖动的父布局
fun setDragView(view: View?) {
this.dragView = view
}
//设置拖动的开关
fun setDragOpen(isOpen: Boolean) {
this.isOpen = isOpen
}
//设置拖动限制拖动的区域(竖屏)
fun setProperticLimitRect(l: Float, t: Float, r: Float, b: Float) {
Log.i(TAG, "setProperticLimitRect l=$l t=$t r=$r b=$b")
isLandScape = false
leftLimit = l
rightLimit = screenWidth - r
topLimit = t
bottomLimit = screenHeight - b
}
//设置拖动限制拖动的区域(横屏)
fun setLandScapeLimitRect(l: Float, t: Float, r: Float, b: Float) {
Log.i(TAG, "setLandScapeLimitRect l=$l t=$t r=$r b=$b")
isLandScape = true
leftLimit = l
rightLimit = screenHeight - r
topLimit = t
bottomLimit = screenWidth - b
invalidate()
}
}
3、长按逻辑判断
长按依赖的是用户手指点下之后,在短时间内,没有抬起来,则会执行长按的Runnable
,表示用户长按拖动,当然这个方案不是很完美,这里的缺陷是,如果用户在滑动列表超过长按时长,也会被认为是长按拖动,如果追求完美的话,就是需要增加一个判断,判断这段时间内,用户在滑动事件中,是否已经达到列表滑动的标准或者是长按的标准
override fun dispatchTouchEvent(e: MotionEvent): Boolean {
if (isOpen) {
when (e.action) {
MotionEvent.ACTION_DOWN -> {
postDelayed(longPressRunnable, ViewConfiguration.getLongPressTimeout().toLong())
}
MotionEvent.ACTION_MOVE -> {
//如果追求完美,需要在此通过判断dx和dy,是否达到阈值,太小则表示长按,太大,就表示用户在列表滑动
}
MotionEvent.ACTION_UP -> {
removeCallbacks(longPressRunnable)
}
}
}
return super.dispatchTouchEvent(e)
}
4、拖动和贴边实现
- 拖动时候计算拖动距离
- 拖动时候让原先的父布局移动
- 拖动时候计算边缘位置
- 松手后贴边处理
同样这里有个缺陷,由于底部边缘有虚拟导航栏,对于底部的限制区域,需要判断有导航栏显示的时候需要减去这部分的高度
override fun dispatchTouchEvent(e: MotionEvent): Boolean {
if (isOpen) {
when (e.action) {
MotionEvent.ACTION_DOWN -> {
isDownStart?.invoke()
dragView?.let {
lastLeft = it.x.toFloat()
lastTop = it.y.toFloat()
}
lastX = e.rawX
lastY = e.rawY
drag = false
postDelayed(longPressRunnable, ViewConfiguration.getLongPressTimeout().toLong())
}
MotionEvent.ACTION_MOVE -> {
dragView?.let {
//计算拖动距离
val dx = e.rawX - lastX
val dy = e.rawY - lastY
//让原先的父布局移动的位置
var left: Float = (lastLeft + dx).toFloat()
var top: Float = (lastTop + dy).toFloat()
//限制拖动范围
if (top < topLimit) {
top = topLimit.toFloat()
}
if (top + height > bottomLimit) {
top = (bottomLimit - height).toFloat()
}
if (left < leftLimit) {
left = leftLimit.toFloat()
}
if (left + width > rightLimit) {
left = (rightLimit - width).toFloat()
}
Log.i(TAG, "left=$left top=$top")
if (drag) {
dragView?.x = left.toFloat()
dragView?.y = top.toFloat()
return true
}
}
}
MotionEvent.ACTION_UP -> {
if (drag) {
dragView?.let {
val center = if (isLandScape) screenHeight / 2 else screenWidth / 2
val currentLeft = width / 2 + it.x
if (center - currentLeft > 0) {
isLeft = true
//向左贴边
it.animate()
.setInterpolator(AccelerateInterpolator())
.setDuration(300)
.x(leftLimit.toFloat())
.setListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {
}
override fun onAnimationCancel(animation: Animator?) {
}
override fun onAnimationStart(animation: Animator?) {
}
override fun onAnimationEnd(animation: Animator?) {
dragView?.let {
lastLeft = it.x.toFloat()
lastTop = it.y.toFloat()
Log.i(TAG, "lastLeft=$lastLeft lastTop=$lastTop")
}
}
})
.start()
} else {
isLeft = false
//向右贴边
it.animate()
.setInterpolator(AccelerateInterpolator())
.setDuration(300)
.x(rightLimit - width.toFloat())
.setListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {
}
override fun onAnimationCancel(animation: Animator?) {
}
override fun onAnimationStart(animation: Animator?) {
}
override fun onAnimationEnd(animation: Animator?) {
dragView?.let {
lastLeft = it.x.toFloat()
lastTop = it.y.toFloat()
Log.i(TAG, "lastLeft=$lastLeft lastTop=$lastTop")
}
}
})
.start()
}
}
}
drag = false
removeCallbacks(longPressRunnable)
isDragUp?.invoke()
}
}
}
return super.dispatchTouchEvent(e)
}
使用
- 实现Recycler列表
- 打开拖动开关
- 设置拖动限制范围
- 设置拖动事件回调
class MainActivity : AppCompatActivity() {
var TAG = "MainActivity"
var drag_view: DragRecyclerView? = null
var drag_view_root: FrameLayout? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
drag_view_root = findViewById(R.id.drag_view_root)
drag_view = findViewById(R.id.rv_drag)
drag_view?.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
drag_view?.adapter = object : RvAdapter(this@MainActivity) {}
drag_view?.setDragOpen(true)
drag_view?.setProperticLimitRect(20f, 20f, 20f, 20f)
drag_view?.setDragView(drag_view_root)
drag_view?.isDragStart = {
Log.i("Hensen", "[拖动开始]")
}
drag_view?.isDownStart = {
Log.i("Hensen", "[手指点下]")
}
drag_view?.isDragUp = {
Log.i("Hensen", "[手指抬起]")
}
}
}