类似IOS的over-scrolling效果

类似IOS的over-scrolling效果,即对于滑动到顶部或底部的View继续滑动时会超出,松手后自动还原到原始位置

分两块内容:容器+滑动控件

1、容器:OverScrollDecor.kt

package xxx

import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.widget.FrameLayout
import androidx.core.view.ViewCompat
import androidx.customview.widget.ViewDragHelper
import kotlin.math.abs

/**
 * 类似IOS的over-scrolling效果,即对于滑动到顶部或底部的View继续滑动时会超出,松手后自动还原到原始位置
 * Decor容器
 * @author jocerly
 * @date 2022-12-07
 */
class OverScrollDecor @JvmOverloads constructor(
    context: Context?,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(
    context!!, attrs, defStyleAttr) {

    private val mDragHelper by lazy {
        ViewDragHelper.create(this, 1.0f, OverScrollCallBack())
    }

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        var shouldIntercept = false
        try {
            shouldIntercept = mDragHelper.shouldInterceptTouchEvent(ev)
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return shouldIntercept
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        try {
            mDragHelper.processTouchEvent(event)
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return true
    }

    override fun computeScroll() {
        if (mDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this)
        }
    }

    private inner class OverScrollCallBack : ViewDragHelper.Callback() {
        override fun tryCaptureView(child: View, pointerId: Int): Boolean {
            return true
        }

        override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
            (releasedChild.layoutParams as MarginLayoutParams).apply {
                mDragHelper.smoothSlideViewTo(releasedChild, leftMargin, topMargin)
            }
            ViewCompat.postInvalidateOnAnimation(this@OverScrollDecor)
        }

        override fun getViewVerticalDragRange(child: View): Int = abs(child.height)

        override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int = child.left

        override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int = child.top + dy / 2
    }
}

2、滑动控件:VerticalRecyclerView.kt,VerticalScrollView.kt等

package xxx

import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager

/**
 * 类似IOS的over-scrolling效果,即对于滑动到顶部或底部的View继续滑动时会超出,松手后自动还原到原始位置
 * 支持的RecyclerView
 * @author jocerly
 * @date 2022-12-07
 */
class VerticalRecyclerView @JvmOverloads constructor(
    context: Context?,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : RecyclerView(
    context!!, attrs, defStyle), ObservableView {
    private var downX = 0f
    private var downY = 0f

    /** 第一个可见的item的位置  */
    private var firstVisibleItemPosition = 0

    /** 第一个的位置  */
    private var firstPositions: IntArray? = null

    /** 最后一个可见的item的位置  */
    private var lastVisibleItemPosition = 0

    /** 最后一个的位置  */
    private var lastPositions: IntArray? = null
    override var isTop = false
        private set
    override var isBottom = false
        private set

    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        val layoutManager = layoutManager
        if (layoutManager != null) {
            when (layoutManager) {
                is GridLayoutManager -> {
                    lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
                    firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
                }
                is LinearLayoutManager -> {
                    lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
                    firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
                }
                is StaggeredGridLayoutManager -> {
                    if (lastPositions == null) {
                        lastPositions = IntArray(layoutManager.spanCount)
                        firstPositions = IntArray(layoutManager.spanCount)
                    }
                    layoutManager.findLastVisibleItemPositions(lastPositions)
                    layoutManager.findFirstVisibleItemPositions(firstPositions)
                    lastVisibleItemPosition = findMax(lastPositions!!)
                    firstVisibleItemPosition = findMin(firstPositions!!)
                }
            }
        } else {
            throw RuntimeException("Unsupported LayoutManager used. Valid ones are LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager")
        }
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                downX = ev.x
                downY = ev.y
                //如果滑动到了最底部,就允许继续向上滑动加载下一页,否者不允许
                parent.requestDisallowInterceptTouchEvent(true)
            }
            MotionEvent.ACTION_MOVE -> {
                val dx = ev.x - downX
                val dy = ev.y - downY
                val allowParentTouchEvent: Boolean
                if (Math.abs(dy) > Math.abs(dx)) {
                    if (dy > 0) {
                        //位于顶部时下拉,让父View消费事件
                        isTop = firstVisibleItemPosition == 0 && getChildAt(0).top >= 0
                        allowParentTouchEvent = isTop
                    } else {
                        //位于底部时上拉,让父View消费事件
                        val visibleItemCount = layoutManager.childCount
                        val totalItemCount = layoutManager.itemCount
                        isBottom =
                            visibleItemCount > 0 && lastVisibleItemPosition >= totalItemCount - 1 && getChildAt(
                                childCount - 1).bottom <= height
                        allowParentTouchEvent = isBottom
                    }
                } else {
                    //水平方向滑动
                    allowParentTouchEvent = true
                }
                parent.requestDisallowInterceptTouchEvent(!allowParentTouchEvent)
            }
        }
        return super.dispatchTouchEvent(ev)
    }

    private fun findMax(lastPositions: IntArray): Int {
        var max = lastPositions[0]
        for (value in lastPositions) {
            if (value >= max) {
                max = value
            }
        }
        return max
    }

    private fun findMin(firstPositions: IntArray): Int {
        var min = firstPositions[0]
        for (value in firstPositions) {
            if (value < min) {
                min = value
            }
        }
        return min
    }
}
package xxx

import android.R
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.ScrollView
import kotlin.math.abs

/**
 * 类似IOS的over-scrolling效果,即对于滑动到顶部或底部的View继续滑动时会超出,松手后自动还原到原始位置
 * 支持的ScrollView
 * @author jocerly
 * @date 2022-12-07
 */
class VerticalScrollView @JvmOverloads constructor(
    context: Context?,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = R.attr.scrollViewStyle
) : ScrollView(context, attrs, defStyleAttr), ObservableView {

    private var downX = 0f
    private var downY = 0f

    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                downX = ev.x
                downY = ev.y
                //如果滑动到了最底部,就允许继续向上滑动加载下一页,否者不允许
                parent.requestDisallowInterceptTouchEvent(true)
            }
            MotionEvent.ACTION_MOVE -> {
                val dx = ev.x - downX
                val dy = ev.y - downY
                val allowParentTouchEvent: Boolean = if (abs(dy) > abs(dx)) {
                    if (dy > 0) {
                        //位于顶部时下拉,让父View消费事件
                        isTop
                    } else {
                        //位于底部时上拉,让父View消费事件
                        isBottom
                    }
                } else {
                    //水平方向滑动
                    true
                }
                parent.requestDisallowInterceptTouchEvent(!allowParentTouchEvent)
            }
        }
        return super.dispatchTouchEvent(ev)
    }

    @get:SuppressLint("NewApi")
    override val isTop: Boolean
        get() = !canScrollVertically(-1)
    override val isBottom: Boolean
        get() = !canScrollVertically(1)
}

3、ObservableView.kt

package xxx

interface ObservableView {
    val isTop: Boolean
    val isBottom: Boolean
}

4、直接xml中使用:

<xxx.OverScrollDecor
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <xxx.VerticalRecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent">


    </xxx.VerticalRecyclerView>   
</xxx.OverScrollDecor>    
                

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值