Android:bottomSheet效果的下拉拖拽功能总结

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

       效果如动图所示,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)
        }
    }
}

 

 

Demo地址:https://github.com/crystalyf/DemoX 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值