
效果如动图所示,Android中要对布局中的控件进行自由拖动,一般有两种实现方法:
方法1:重写父类点击事件的方法, 对触摸事件进行处理。
方法2: 利用ViewDragHelper接管触摸操作来处理触摸事件。
gif图所用的便是方法2。ViewDragHelper提供的callback(ViewDragHelper.Callback)针对 ViewGroup 中的拖拽和重新定位 views 操作时提供了一系列非常有用的方法。下面贴出Demo的核心代码:
布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/relativelayout_root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.change.demox.views.bottomsheet.widget.DragLayout
android:id="@+id/drawLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/dragView"
android:layout_width="match_parent"
android:layout_height="32dp"
android:background="@drawable/bg_top_radius_white"
android:elevation="1dp">
<View
android:layout_width="44dp"
android:layout_height="5dp"
android:layout_centerInParent="true"
android:background="@drawable/bg_grey_radius" />
</RelativeLayout>
<FrameLayout
android:id="@+id/frame_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/dragView"
android:background="@color/white"
android:elevation="10dp"
android:padding="1dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_machine_part"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</com.change.demox.views.bottomsheet.widget.DragLayout>
</RelativeLayout>
自定义组件:
需要注意的说明,都写在注释里了
package com.change.demox.views.bottomsheet.widget
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.os.Parcelable
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.widget.FrameLayout
import android.widget.RelativeLayout
import androidx.core.view.ViewCompat
import androidx.customview.widget.ViewDragHelper
import androidx.recyclerview.widget.RecyclerView
import com.change.demox.R
/**
* レイアウトをドラッグ、ボトムシートの使用
*
*/
class DragLayout : RelativeLayout {
/**
* ViewDragHelper是针对 ViewGroup 中的拖拽和重新定位 views 操作时提供了一系列非常有用的方法和状态追踪。基本上使用在自定义ViewGroup处理拖拽中
*
* Android布局中要对布局中的控件进行自由拖动,一种方法是重写父类点击事件的方法, 对触摸事件进行处理, 这种方法代码量过大暂不讨论.
* 另一种方法是利用ViewDragHelper来处理触摸事件
*/
private var dragHelper: ViewDragHelper? = null
//拖拽布局的横条
private var dragViewLayout: View? = null
//拖拽显示出来的列表父布局
private var contentView: FrameLayout? = null
//拖拽显示出来的列表
private var recyclerView: RecyclerView? = null
private var dragRange = 0
private var topMargin = 0
var currentState = State.AT_BOTTOM
companion object {
const val SUPER_STATUS_KEY = "superState"
const val CURRENT_STATUS_KEY = "state"
}
object State {
const val AT_BOTTOM = 0
const val AT_MIDDLE = 1
const val AT_TOP = 2
}
constructor(context: Context?) : super(context) {
init()
}
constructor(context: Context?, attrs: AttributeSet?) : super(
context,
attrs
) {
init()
}
constructor(
context: Context?,
attrs: AttributeSet?,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr) {
init()
}
private fun init() {
dragHelper = ViewDragHelper.create(this, callback)
}
override fun onFinishInflate() {
super.onFinishInflate()
dragViewLayout = findViewById(R.id.dragView)
contentView = findViewById(R.id.frame_list)
recyclerView = findViewById(R.id.rv_machine_part)
}
/**
* 处理CallBack逻辑:
*
CallBack中判断控件是否可以拖动有以下几个关键方法:
tryCaptureView() : 判断View是否是可拖动, 返回true表示可该view可拖动
clampViewPositionHorizontal() / clampViewPositionVertical() : 决定子view在水平/垂直方向上应该移动到的位置, 返回0表示不允许该方向上的运动
getViewHorizontalDragRange() / getViewVerticalDragRange() : 以像素为单位返回子view在水平/垂直方向上可移动的距离, 返回0表示不能在该方向上进行移动
onViewPositionChanged()会在控件位置变化时不断被回调
onViewReleased()则是在手指松开时进行回调
*/
private val callback: ViewDragHelper.Callback = object : ViewDragHelper.Callback() {
override fun tryCaptureView(
child: View,
pointerId: Int
): Boolean {
//若控件是dragView,那么可拖动
return child === dragViewLayout
}
/**
* 限制View纵向的拖拉操作
*
* 返回0表示不允许该方向上的运动
*/
override fun clampViewPositionVertical(
child: View,
top: Int,
dy: Int
): Int {
//滑动限制距离,控制view纵向不超出屏幕
val topBound = height - dragRange - dragViewLayout!!.height
val bottomBound = height - dragViewLayout!!.height
topMargin = top
return topBound.coerceAtLeast(top).coerceAtMost(bottomBound)
}
override fun getViewVerticalDragRange(child: View): Int {
return dragRange
}
/**
* 重写, 对自定义布局进行一些处理
*/
override fun onViewReleased(
releasedChild: View,
xVel: Float,
yVel: Float
) {
super.onViewReleased(releasedChild, xVel, yVel)
//根据上边距,判断滑动显示的bottomSheet是显示在屏幕中的什么位置:top,middle,bottom
if (topMargin < dragRange / 4) {
smoothToTop()
} else if (topMargin > dragRange / 4 && topMargin < 3 * dragRange / 4) {
smoothToMid()
} else if (topMargin > 3 * dragRange / 4) {
smoothToBottom()
}
}
/**
* 重写, 对自定义布局进行一些处理
*
* 记录控件所在的位置, 然后ViewGroup的onLayout()方法并指定其位置就可以了
*/
override fun onViewPositionChanged(
changedView: View,
left: Int,
top: Int,
dx: Int,
dy: Int
) {
dragViewLayout!!.layout(
width - dragViewLayout!!.width,
top,
width,
top + dragViewLayout!!.height
)
contentView!!.layout(
width - contentView!!.width,
top + dragViewLayout!!.height - 2,
width,
height
)
recyclerView!!.layout(
0,
0,
contentView!!.width,
dragRange - top
)
}
}
/**
* 测量控件的高度,可以得到每个控件的最终高度
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
dragRange = contentView!!.measuredHeight
}
override fun onLayout(
changed: Boolean,
l: Int,
t: Int,
r: Int,
b: Int
) {
super.onLayout(changed, l, t, r, b)
reLayout(currentState)
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
val pointY = ev?.y
val pointX = ev?.x
if (pointY != null && pointX != null) {
return if (pointY >= dragViewLayout?.top!! && pointY <= dragViewLayout!!.bottom && pointX >= dragViewLayout!!.left && pointX <= dragViewLayout!!.right) {
if (ev.action == MotionEvent.ACTION_DOWN) {
onTouchEvent(ev)
} else {
super.dispatchTouchEvent(ev)
}
} else
super.dispatchTouchEvent(ev)
}
return super.dispatchTouchEvent(ev)
}
/**
* 写拖拽的同时,一般要重写onTouchEvent()方法, 使ViewDragHelper接管触摸事件的处理
*
*/
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
//通过这个方法判断是否处理拦截的触摸事件,这里使ViewDragHelper接管触摸事件的处理
dragHelper!!.processTouchEvent(event)
return true
}
override fun computeScroll() {
if (dragHelper!!.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this)
}
}
override fun onSaveInstanceState(): Parcelable? {
val bundle = Bundle()
bundle.putParcelable(SUPER_STATUS_KEY, super.onSaveInstanceState())
bundle.putInt(CURRENT_STATUS_KEY, currentState)
return bundle
}
override fun onRestoreInstanceState(state: Parcelable?) {
var mState = state
if (mState is Bundle) {
val bundle = mState
currentState = bundle.getInt(CURRENT_STATUS_KEY)
mState = bundle.getParcelable(SUPER_STATUS_KEY)
reLayout(currentState)
}
super.onRestoreInstanceState(mState)
}
private fun reLayout(currentState: Int) {
when (currentState) {
State.AT_BOTTOM -> {
dragViewLayout!!.layout(
width - dragViewLayout!!.width,
height - dragViewLayout!!.height,
width,
height
)
contentView!!.layout(width - dragViewLayout!!.width, height, width, height)
}
State.AT_MIDDLE -> {
dragViewLayout!!.layout(
width - dragViewLayout!!.width,
(height - dragViewLayout!!.height) / 2,
width,
(height + dragViewLayout!!.height) / 2
)
contentView!!.layout(
width - dragViewLayout!!.width,
(height + dragViewLayout!!.height) / 2 - 2,
width,
height
)
recyclerView!!.layout(
0,
0,
contentView!!.width,
contentView!!.height
)
recyclerView!!.layoutParams.height = (height - dragViewLayout!!.height) / 2
}
State.AT_TOP -> {
dragViewLayout!!.layout(
width - dragViewLayout!!.width,
height - dragRange - dragViewLayout!!.height,
width,
height - dragRange
)
contentView!!.layout(
width - dragViewLayout!!.width,
dragViewLayout!!.height - 2,
width,
height
)
recyclerView!!.layout(
0,
0,
contentView!!.width,
contentView!!.height
)
recyclerView!!.layoutParams.height = height - dragViewLayout!!.height
}
}
}
private fun smoothToTop() {
currentState = State.AT_TOP
recyclerView!!.layoutParams.height = height - dragViewLayout!!.height
if (dragHelper!!.smoothSlideViewTo(
dragViewLayout!!,
width - dragViewLayout!!.width,
height - dragRange - dragViewLayout!!.height
)
) {
ViewCompat.postInvalidateOnAnimation(this)
}
}
private fun smoothToMid() {
currentState = State.AT_MIDDLE
recyclerView!!.layoutParams.height = (height - dragViewLayout!!.height) / 2
if (dragHelper!!.smoothSlideViewTo(
dragViewLayout!!,
width - dragViewLayout!!.width,
(height - dragViewLayout!!.height) / 2
)
) {
ViewCompat.postInvalidateOnAnimation(this)
}
}
private fun smoothToBottom() {
currentState = State.AT_BOTTOM
if (dragHelper!!.smoothSlideViewTo(
dragViewLayout!!,
width - dragViewLayout!!.width,
height - dragViewLayout!!.height
)
) {
ViewCompat.postInvalidateOnAnimation(this)
}
}
}

本文总结了如何在Android中实现bottomSheet的下拉拖拽功能,主要介绍了两种方法:重写父类点击事件和利用ViewDragHelper。文中详细讲解了通过ViewDragHelper的Callback方法来处理拖拽和重新定位views的操作,并提供了相关Demo的核心代码和注释。

921

被折叠的 条评论
为什么被折叠?



