嵌套滑动
嵌套滑动的场景
- 不同向嵌套
- onInterceptTouchEvent 父 View 拦截
- requestDisallowInterceptTouchEvent() 子 View 阻止父 View 拦截
- 同向嵌套
- 父 View 会彻底卡住子 View
- 原因:抢夺条件一致,但 父 View 的 onInterceptTouchEvent() 早于子View 的 dispatchTouchEvent()
- 本质上是策略问题:嵌套状态下用户手指滑动,他是想滑谁?
- 场景一:NestedScrollView
- 子 View 能滑动的时候,滑子 View;滑不动的时候,滑父 View
- 场景二:Google 的样例
- 上滑:优先父 View
- 下滑:优先子 View
- 场景一:NestedScrollView
- 解决方案:实现策略——父 View、子 View 谁来消费事件可以实时协商
- 实现?
- 其实大多数场景,SDK 就解决了
- 案例 1: ScrollView 嵌套:没法滑动
- 换成 NestedScrollView:可以滑动
- 其实大多数场景,SDK 就解决了
- 父 View 会彻底卡住子 View
嵌套滑动ImageView实现
- 图片可以放大
- 当图片滑动到底部时,可以向上滑动
- 当图片滑动到顶部时,可以向下滑动
private val IMAGE_SIZE = 300.dp.toInt()
private const val EXTRA_SCALE_FACTOR = 1.5f
class NestedScalableImageView(context: Context?, attrs: AttributeSet?) : View(context, attrs),
NestedScrollingChild3 {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val bitmap = getAvatar(resources, IMAGE_SIZE)
private var originalOffsetX = 0f
private var originalOffsetY = 0f
private var offsetX = 0f
private var offsetY = 0f
private var smallScale = 0f
private var bigScale = 0f
private val dshGestureListener = DshGestureListener()
private val dshScaleGestureListener = DshScaleGestureListener()
private val dshFlingRunner = DshFlingRunner()
private val gestureDetector = GestureDetectorCompat(context, dshGestureListener)
private val scaleGestureDetector = ScaleGestureDetector(context, dshScaleGestureListener)
private var big = false
private var currentScale = 0f
set(value) {
field = value
invalidate()
}
private val scaleAnimator = ObjectAnimator.ofFloat(this, "currentScale", smallScale, bigScale)
private val scroller = OverScroller(context)
private var childHelper = NestedScrollingChildHelper(this).apply {
isNestedScrollingEnabled = true
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
originalOffsetX = (width - IMAGE_SIZE) / 2f
originalOffsetY = (height - IMAGE_SIZE) / 2f
if (bitmap.width / bitmap.height.toFloat() > width / height.toFloat()) {
smallScale = width / bitmap.width.toFloat()
bigScale = height / bitmap.height.toFloat() * EXTRA_SCALE_FACTOR
} else {
smallScale = height / bitmap.height.toFloat()
bigScale = width / bitmap.width.toFloat() * EXTRA_SCALE_FACTOR
}
currentScale = smallScale
scaleAnimator.setFloatValues(smallScale, bigScale)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
scaleGestureDetector.onTouchEvent(event)
if (!scaleGestureDetector.isInProgress) {
gestureDetector.onTouchEvent(event)
childHelper.startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
}
return true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val scaleFraction = (currentScale - smallScale) / (bigScale - smallScale)
canvas.translate(offsetX * scaleFraction, offsetY * scaleFraction)
canvas.scale(currentScale, currentScale, width / 2f, height / 2f)
canvas.drawBitmap(bitmap, originalOffsetX, originalOffsetY, paint)
}
private fun fixOffsets(): Int {
val rawOffsetY = offsetY
val maxOffsetY = (bitmap.height * bigScale - height) / 2
offsetX = min(offsetX, (bitmap.width * bigScale - width) / 2)
offsetX = max(offsetX, -(bitmap.width * bigScale - width) / 2)
offsetY = min(offsetY, maxOffsetY)
offsetY = max(offsetY, -maxOffsetY)
return when {
rawOffsetY < -maxOffsetY -> (-maxOffsetY - rawOffsetY).toInt()
rawOffsetY > maxOffsetY -> (maxOffsetY - rawOffsetY).toInt()
else -> 0
}
}
override fun startNestedScroll(axes: Int, type: Int): Boolean {
return childHelper.startNestedScroll(axes, type)
}
override fun stopNestedScroll(type: Int) {
childHelper.stopNestedScroll(type)
}
override fun hasNestedScrollingParent(type: Int): Boolean {
return childHelper.hasNestedScrollingParent(type)
}
override fun dispatchNestedScroll(
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
offsetInWindow: IntArray?,
type: Int,
consumed: IntArray
) {
return childHelper.dispatchNestedScroll(
dxConsumed,
dyConsumed,
dxUnconsumed,
dyUnconsumed,
offsetInWindow,
type, consumed
)
}
override fun dispatchNestedScroll(
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
offsetInWindow: IntArray?,
type: Int
): Boolean {
return childHelper.dispatchNestedScroll(
dxConsumed,
dyConsumed,
dxUnconsumed,
dyUnconsumed,
offsetInWindow,
type
)
}
override fun dispatchNestedPreScroll(
dx: Int,
dy: Int,
consumed: IntArray?,
offsetInWindow: IntArray?,
type: Int
): Boolean {
return childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
}
inner class DshGestureListener : GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent): Boolean {
return true
}
override fun onFling(
downEvent: MotionEvent?,
currentEvent: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
if (big) {
scroller.fling(
offsetX.toInt(), offsetY.toInt(), velocityX.toInt(), velocityY.toInt(),
(-(bitmap.width * bigScale - width) / 2).toInt(),
((bitmap.width * bigScale - width) / 2).toInt(),
(-(bitmap.height * bigScale - height) / 2).toInt(),
((bitmap.height * bigScale - height) / 2).toInt()
)
ViewCompat.postOnAnimation(this@NestedScalableImageView, dshFlingRunner)
}
return false
}
override fun onScroll(
downEvent: MotionEvent?,
currentEvent: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {
if (big) {
offsetX -= distanceX
offsetY -= distanceY
val unconsumed = fixOffsets()
if (unconsumed != 0) {
childHelper.dispatchNestedScroll(0, 0, 0, unconsumed, null)
} else {
invalidate()
}
}
return false
}
override fun onDoubleTap(e: MotionEvent): Boolean {
big = !big
if (big) {
offsetX = (e.x - width / 2f) * (1 - bigScale / smallScale)
offsetY = (e.y - height / 2f) * (1 - bigScale / smallScale)
fixOffsets()
scaleAnimator.start()
} else {
scaleAnimator.reverse()
}
return true
}
}
inner class DshScaleGestureListener : ScaleGestureDetector.OnScaleGestureListener {
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
offsetX = (detector.focusX - width / 2f) * (1 - bigScale / smallScale)
offsetY = (detector.focusY - height / 2f) * (1 - bigScale / smallScale)
return true
}
override fun onScaleEnd(detector: ScaleGestureDetector?) {
}
override fun onScale(detector: ScaleGestureDetector): Boolean {
val tempCurrentScale = currentScale * detector.scaleFactor
if (tempCurrentScale < smallScale || tempCurrentScale > bigScale) {
return false
} else {
currentScale *= detector.scaleFactor // 0 1; 0 无穷
return true
}
}
}
inner class DshFlingRunner : Runnable {
override fun run() {
if (scroller.computeScrollOffset()) {
offsetX = scroller.currX.toFloat()
offsetY = scroller.currY.toFloat()
invalidate()
ViewCompat.postOnAnimation(this@NestedScalableImageView, this)
}
}
}
}
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="400dp"
android:background="@color/colorAccent" />
<com.dsh.nestedscroll.view.NestedScalableImageView
android:layout_width="match_parent"
android:layout_height="400dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>