类似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中使用: