实现ScrollView底部和顶部阻尼拖拽

先上效果图
在这里插入图片描述 在这里插入图片描述

实现原理:通过继承NestedScrollView,并重写onTouchEvent方法,滚动其子view来实现
完整代码

package com.example.myapplication

import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.core.widget.NestedScrollView
import kotlin.math.abs

class PullEdgeScrollView : NestedScrollView {
    companion object {
        private const val INVALID_POINTER = -1
        private const val RESET_DURATION = 300L
        private const val SCROLL_RATIO = 0.5f
    }

    private lateinit var mDragView: View
    private var mLastMotionY: Int = 0
    private var mActivePointerId = INVALID_POINTER
    private var mIsBeingDragged = false
    private var mIsReset = false
    private var mDragStartY = 0
    private var mResetAnimation: ValueAnimator? = null
    private var mOnScrollListener: OnScrollListener? = null

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
        context,
        attrs,
        defStyle
    )

    override fun onFinishInflate() {
        initView()
        super.onFinishInflate()
    }

    private fun initView() {
        overScrollMode = OVER_SCROLL_NEVER
        if (getChildAt(0) != null) {
            mDragView = getChildAt(0)
        }
        setOnScrollChangeListener { view: NestedScrollView, scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int ->
            run {
                mOnScrollListener?.onScrollY(scrollY + mDragView.scrollY)
            }
        }
    }

    fun setOnScrollListener(l: OnScrollListener) {
        mOnScrollListener = l
    }

    override fun onTouchEvent(ev: MotionEvent): Boolean {
        val actionMasked = ev.actionMasked
        when (actionMasked) {
            MotionEvent.ACTION_DOWN -> {
                mLastMotionY = ev.y.toInt()
                mActivePointerId = ev.getPointerId(0)
                if (mIsReset) {
                    mResetAnimation?.cancel()
                    mIsReset = false
                    mIsBeingDragged = true
                }
            }
            MotionEvent.ACTION_MOVE -> {
                val activePointerIndex = ev.findPointerIndex(mActivePointerId)
                if (activePointerIndex != INVALID_POINTER) {
                    val y = ev.getY(activePointerIndex).toInt()
                    val deltaY = (mLastMotionY - y)
                    val canDragUp = scrollY >= mDragView.height - height
                    val canDragDown = scrollY == 0
                    if (!mIsBeingDragged) {
                        if ((canDragUp && deltaY > 0) || (canDragDown && deltaY < 0)) {
                            if (abs(deltaY) > 0) {
                                mIsBeingDragged = true
                                mDragStartY = y
                            }
                        } else {
                            mLastMotionY = y
                        }
                    }
                    if (mIsBeingDragged) {
                        val dragScrollY = mDragView.scrollY
                        val obstacle =
                            (dragScrollY > 0 && deltaY > 0) || (dragScrollY < 0 && deltaY < 0)
                        //如果滑动是阻力方向,则滚动为滑动距离的1/2,如果不是则滚动为滑动距离
                        var needScrollY = if (obstacle) (deltaY * SCROLL_RATIO).toInt() else deltaY
                        //这里判断防止一步跨过边界,导致没有从拖拽模式(子view scrollY不等于0)转为正常模式
                        if ((dragScrollY > 0 && (dragScrollY < -needScrollY)) || (dragScrollY < 0 && (dragScrollY > -needScrollY))) {
                            needScrollY = -dragScrollY
                        }
                        mDragView.scrollBy(0, needScrollY)
                        mOnScrollListener?.onScrollY(mDragView.scrollY + scrollY)
                        //子View的scrollY为0退出拖拽模式
                        if (mDragView.scrollY == 0) {
                            mIsBeingDragged = false
                        }
                        //当滚动距离不为0才重置滑动的起点,防止滑动动作慢,距离很短,导致滚动不流畅
                        if (needScrollY != 0) {
                            mLastMotionY = y
                        }
                    }
                }
            }
            MotionEvent.ACTION_CANCEL,
            MotionEvent.ACTION_UP -> {
                mActivePointerId = INVALID_POINTER
                mLastMotionY = 0
                mIsBeingDragged = false
                //当最后一根手指抬起时,如果子view不在原位置则通过属性动画让它回到原位置
                if (mDragView.scrollY != 0) {
                    resetDragView()
                }
            }
            MotionEvent.ACTION_POINTER_DOWN -> {
                val index = ev.actionIndex
                mLastMotionY = ev.getY(index).toInt()
                mActivePointerId = ev.getPointerId(index)
            }
            MotionEvent.ACTION_POINTER_UP -> {
                val pointerIndex = ev.actionIndex
                val pointerId = ev.getPointerId(pointerIndex)
                if (pointerId == mActivePointerId) {
                    val newPointerIndex = if (pointerIndex == 0) 1 else 0
                    mActivePointerId = ev.getPointerId(newPointerIndex)
                }
                mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId)).toInt()
            }
        }
        return super.onTouchEvent(ev)
    }

    override fun onOverScrolled(scrollX: Int, scrollY: Int, clampedX: Boolean, clampedY: Boolean) {
        //如果正在拖拽或正在执行重置动画,则拦截ScrollView滚动,防止子View和ScrollView一起滚动,速度变快
        if (mIsBeingDragged || mIsReset) {
            return
        }
        super.onOverScrolled(scrollX, scrollY, clampedX, clampedY)
    }

    private fun resetDragView() {
        mIsReset = true
        mResetAnimation = ValueAnimator.ofInt(mDragView.scrollY, 0)
        mResetAnimation?.duration = RESET_DURATION
        mResetAnimation?.addUpdateListener {
            mDragView.scrollTo(mDragView.scrollX, it.animatedValue as Int)
            mOnScrollListener?.onScrollY(mDragView.scrollY + scrollY)
            if (mDragView.scrollY == 0) {
                mIsReset = false
            }
        }
        mResetAnimation?.start()
    }

    interface OnScrollListener {
        fun onScrollY(scrollY: Int)
    }
}

github: https://github.com/ttooyy/PullEdgeScrollView

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值