Android 实现嵌套滑动
前言
Android實(shí)現(xiàn)簡(jiǎn)易版滑動(dòng)
上次文章中實(shí)現(xiàn)了簡(jiǎn)易的ScrollerView滑動(dòng),但實(shí)際使用中許多場(chǎng)景都會(huì)涉及到嵌套滑動(dòng),在今天的博文中我們基于上次的ScrollLayout來(lái)進(jìn)一步實(shí)現(xiàn)嵌套滑動(dòng)。
嵌套滑動(dòng)預(yù)備知識(shí):https://juejin.cn/post/6844904184911773709
整體頁(yè)面結(jié)構(gòu)
<?xml version="1.0" encoding="utf-8"?> <com.example.nestedscroll.ScrollParentLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><TextViewandroid:layout_width="match_parent"android:layout_height="300dp"android:text="I'm TOP!"android:gravity="center"android:textSize="24sp"android:background="@color/teal_700"/><com.example.nestedscroll.ScrollChildLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><TextViewandroid:layout_width="match_parent"android:layout_height="100dp"android:text="I'm 1"android:gravity="center"android:textSize="24sp"android:background="@color/red1"/><TextViewandroid:layout_width="match_parent"android:layout_height="100dp"android:text="I'm 2"android:gravity="center"android:textSize="24sp"android:background="@color/red2"/><TextViewandroid:layout_width="match_parent"android:layout_height="100dp"android:text="I'm 3"android:gravity="center"android:textSize="24sp"android:background="@color/red1"/>... 后邊還有n個(gè)TextView</com.example.nestedscroll.ScrollChildLayout></com.example.nestedscroll.ScrollParentLayout>嵌套結(jié)構(gòu)中父ViewGroup為ScrollParentLayout,子ViewGroup為ScrollChildLayout。
class ScrollParentLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, ) : NestedScrollLayout(context, attrs), NestedScrollingParent3 class ScrollChildLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, ) : NestedScrollLayout(context, attrs), NestedScrollingChild3- ScrollParentLayout和ScrollChildLayout均繼承自NestedScrollLayout(NestedScrollLayout為ScrollLayout的copy,以不修改ScrollLayout實(shí)現(xiàn))以提供滑動(dòng)功能
- ScrollParentLayout實(shí)現(xiàn)了NestedScrollingParent3接口,作為嵌套滑動(dòng)的父控件
- ScrollChildLayout實(shí)現(xiàn)了NestedScrollingChild3接口,作為嵌套滑動(dòng)的子控件
頁(yè)面滑不動(dòng)
運(yùn)行后發(fā)現(xiàn)頁(yè)面滑不動(dòng),查看NestedScrollLayout的onInterceptTouchEvent()實(shí)現(xiàn),為簡(jiǎn)單實(shí)現(xiàn)滑動(dòng)效果,上節(jié)中簡(jiǎn)單將NestedScrollLayout設(shè)置為攔截所有觸摸事件。
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {return true }這直接導(dǎo)致了頁(yè)面滑不動(dòng),因?yàn)镾crollParentLayout其子View的高度經(jīng)過(guò)onMeasure后都是固定的了,所以ScrollParentLayout的控件高度和內(nèi)容高度相等,ScrollParentLayout不可滑動(dòng)。同時(shí)由于ScrollParentLayout在外層攔截了觸摸事件,ScrollChildLayout無(wú)法接收到觸摸事件,因此也無(wú)法響應(yīng),所以頁(yè)面無(wú)法滑動(dòng)。
結(jié)合嵌套滑動(dòng)的機(jī)制(NestedScrollingParent,NestedScrollingChild機(jī)制),滑動(dòng)時(shí)間需由子控件來(lái)接收,然后通過(guò)嵌套滑動(dòng)機(jī)制來(lái)確定父控件是否消費(fèi)部分滑動(dòng)距離,因此ScrollParentLayout需要保證不攔截觸摸事件,同時(shí)ScrollChildLayout需要接收到觸摸事件。
//ScrollParentLayout.kt override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {return false } //ScrollChildLayout.kt //實(shí)現(xiàn)參考了NestedScrollView //實(shí)現(xiàn)參考了NestedScrollView override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {if (ev == null) return falseval action = ev.actionif (action == MotionEvent.ACTION_MOVE && isBeingDragged) {return true}var currY = ev.ywhen (action) {MotionEvent.ACTION_MOVE -> {if (abs(currY - lastY) >= touchSlop) {isBeingDragged = trueval parent = parentparent?.requestDisallowInterceptTouchEvent(true)}}MotionEvent.ACTION_DOWN -> {isBeingDragged = false//開(kāi)始嵌套滑動(dòng),注意不是startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH)}MotionEvent.ACTION_CANCEL,MotionEvent.ACTION_UP,-> {//結(jié)束嵌套滑動(dòng)isBeingDragged = falsestopNestedScroll()}}return isBeingDragged }重寫onInterceptTouchEvent()中,我們默認(rèn)不攔截觸摸事件,只有當(dāng)View表現(xiàn)為正在滑動(dòng)時(shí)才進(jìn)行攔截,以處理滑動(dòng),并在開(kāi)始滑動(dòng)時(shí)調(diào)用startNestedScroll(),手指抬起時(shí)調(diào)用stopNestedScroll(),由于一個(gè)事件序列中會(huì)有多個(gè)ACTION_MOVE事件,而startNestedScroll()僅僅只在第一次判定為滑動(dòng)時(shí)調(diào)用,所以引入了isBeingDragged變量,用以判斷當(dāng)前是否已經(jīng)在嵌套滑動(dòng)了,如果是則直接返回true,對(duì)應(yīng)的邏輯為下邊的代碼。
if (action == MotionEvent.ACTION_MOVE && isBeingDragged){return true}經(jīng)過(guò)處理后子View可以正常滑動(dòng)了。
嵌套Scroll
ScrollChildLayout實(shí)現(xiàn)NestedScrollChild3接口
嵌套滑動(dòng)機(jī)制中為我們提供了NestedScrollingChildHelper工具類,封裝了基本的子ScrollView向父ScrollView傳遞滑動(dòng)事件的操作,我們只需要NestedScrollingChildHelper對(duì)應(yīng)的方法即可。注意NestedScrollingChildHelper要手動(dòng)設(shè)置isNestedScrollingEnabled為ture。
private val childHelper = NestedScrollingChildHelper(this).apply {//注意要手動(dòng)設(shè)置isNestedScrollingEnabled為ture,只有開(kāi)啟此開(kāi)關(guān),嵌套滑動(dòng)才有效isNestedScrollingEnabled = true }override fun startNestedScroll(axes: Int, type: Int): Boolean {return childHelper.startNestedScroll(axes, type) }override fun stopNestedScroll(type: Int) {return 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, ) {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) }override fun dispatchNestedFling(velocityX: Float,velocityY: Float,consumed: Boolean, ): Boolean {return childHelper.dispatchNestedFling(velocityX, velocityY, consumed) }override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {return childHelper.dispatchNestedPreFling(velocityX, velocityY) }ScrollParentLayout實(shí)現(xiàn)NestedScrollParent3接口
嵌套滑動(dòng)機(jī)制中也提供了NestedScrollingParentHelper工具類,我們可以使用此工具類來(lái)實(shí)現(xiàn)onNestedScrollAccepted()和onStopNestedScroll(),其他很多接口需要我們自行根據(jù)業(yè)務(wù)需要實(shí)現(xiàn)。
private val parentHelper = NestedScrollingParentHelper(this)override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {//判斷是否處理嵌套滑動(dòng)return axes and ViewCompat.SCROLL_AXIS_VERTICAL != 0 }override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {parentHelper.onNestedScrollAccepted(child, target, axes, type) }override fun onStopNestedScroll(target: View, type: Int) {parentHelper.onStopNestedScroll(target, type) }override fun onNestedScroll(target: View,dxConsumed: Int,dyConsumed: Int,dxUnconsumed: Int,dyUnconsumed: Int,type: Int,consumed: IntArray, ) {}override fun onNestedScroll(target: View,dxConsumed: Int,dyConsumed: Int,dxUnconsumed: Int,dyUnconsumed: Int,type: Int, ) {}override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {//TODO }override fun onNestedFling(target: View,velocityX: Float,velocityY: Float,consumed: Boolean ): Boolean {//TODO }override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {//TODO }讓嵌套Scroll生效
上邊的onInterceptTouchEvent()中我們通過(guò)在TOUCH_DOWN事件中調(diào)用了startNestedScroll()方法,開(kāi)啟了嵌套滑動(dòng),此方法主要用于確定嵌套滑動(dòng)的NestedScrollingParent是誰(shuí)。
接下來(lái)就需要由ScrollChildLayout來(lái)在滑動(dòng)時(shí)將事件分發(fā)給ScrollParentLayout。滑動(dòng)事件在onTouchEvent()的ACTION_MOVE事件中處理,這里將其抽離出來(lái)單獨(dú)放在handleScroll()方法中。
override fun handleScroll(currX: Float, currY: Float) {val deltaX = currX - lastXval deltaY = currY - lastYvar realDeltaY = deltaY.toInt()if (dispatchNestedPreScroll(0,realDeltaY,scrollConsumed,scrollOffset,ViewCompat.TYPE_TOUCH)) {realDeltaY -= scrollConsumed[1]}if (canScrollVertically(1) || canScrollVertically(-1)) {//防止滑出邊界realDeltaY = limitRange(realDeltaY, scrollY, -getScrollRange() + scrollY)scrollBy(0, -realDeltaY)} }上面代碼中,利用嵌套滑動(dòng)機(jī)制,首先dispatchNestedPreScroll()將滑動(dòng)距離交由ScrollParentLayout來(lái)處理,ScrollParentLayout來(lái)先消費(fèi)一部分距離,將剩下未消費(fèi)的距離交由ScrollChildLayout繼續(xù)處理,
ScrollChildLayout在判斷了是否滑出邊界后,調(diào)用scrollBy()方法處理剩下的滑動(dòng)距離。
然后ScrollParentLayout也需要配合完成相應(yīng)的滑動(dòng)操作,ScrollParentLayout在onNestedPreScroll()方法中接收到對(duì)應(yīng)的嵌套滑動(dòng)距離,判斷自身是否要消費(fèi)。
回顧下目前布局結(jié)構(gòu)是:
-ScrollParentLayout
-TopView
-ScrollChildLayout
ScrollParentLayout有兩種常見(jiàn)處理方式:
在onNestedPreScroll中,我們需要計(jì)算出ScrollParentLayout需要消費(fèi)的滑動(dòng)距離,主要要保證最后交由ScrollParentLayout處理的滑動(dòng)的最終位置在[0, topViewHeight]范圍內(nèi)(即保證TopView可見(jiàn)或剛好不可見(jiàn)的部分才交由ScrollParentLayout處理)。
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {var consumedY = 0//scrollY以向下為正向,整體相對(duì)于初始位置的偏移 -topViewHeight <= scrollY <= 0if (target == scrollChildLayout) {//下滑 && TopView還能再下滑(在初始位置之上)if (dy > 0 && scrollY > 0 && !scrollChildLayout.canScrollVertically(-1)) {consumedY = Math.min(scrollY, dy)//上滑 && TopView還能向上滑(TopView還可見(jiàn))} else if (dy < 0 && scrollY < topViewHeight) {consumedY = Math.max(-topViewHeight + scrollY, dy)}}if (consumedY != 0) {scrollBy(0, -consumedY)consumed[1] = consumedY} }問(wèn)題1:嵌套滑動(dòng)距離小于手指滑動(dòng)距離,滑動(dòng)抖動(dòng)
這個(gè)問(wèn)題由于MotionEvent所對(duì)應(yīng)的View(ScrollChildLayout)移動(dòng)了所導(dǎo)致的,正常的跟手滑動(dòng)為ScrollChildLayout不動(dòng),則每次滑動(dòng)的deltaY = currY - lastY。currY和lastY都是通過(guò)event.getY()獲取到的,event.getY()獲取到的y值是相對(duì)于當(dāng)前View(ScrollChildLayout)的Y值。由于當(dāng)前View也朝相同方向滑動(dòng)了,這導(dǎo)致計(jì)算出來(lái)的deltaY偏小,從而導(dǎo)致嵌套滑動(dòng)距離小于手指滑動(dòng)距離。(TODO滑動(dòng)抖動(dòng))
解決辦法(參考NestedScrollView):
我們需要獲取到在ScrollParentLayout滑動(dòng)時(shí)ScrollChildLayout的偏移量,查看dispatchNestedPreScroll()方法,可以使用offsetInWindow這個(gè)參數(shù)來(lái)獲取ScrollParentLayout此次嵌套滑動(dòng)的偏移量,然后在最后賦值lastY = currY - offsetInWindow[1]來(lái)校準(zhǔn)偏移量。
/*** 在滑動(dòng)之前,將滑動(dòng)值分發(fā)給NestedScrollingParent* @param dx 水平方向消費(fèi)的距離* @param dy 垂直方向消費(fèi)的距離* @param consumed 輸出坐標(biāo)數(shù)組,consumed[0]為NestedScrollingParent消耗的水平距離、* consumed[1]為NestedScrollingParent消耗的垂直距離,此參數(shù)可空。* @param offsetInWindow 含有View從此方法調(diào)用之前到調(diào)用完成后的屏幕坐標(biāo)偏移量,* 可以使用這個(gè)偏移量來(lái)調(diào)整預(yù)期的輸入坐標(biāo)(即上面4個(gè)消費(fèi)、剩余的距離)跟蹤,此參數(shù)可空。* @return 返回NestedScrollingParent是否消費(fèi)部分或全部滑動(dòng)值*/ boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,@NestedScrollType int type);至此就可以流暢的嵌套Scroll了~。
嵌套Fling
回顧前文中非嵌套的fling,通過(guò)OverScroller來(lái)實(shí)現(xiàn)滑動(dòng)。OverScroller需配合computeScroll()方法一起處理fling動(dòng)作。
NestedScrollChild接口提供了對(duì)應(yīng)的dispatchNestedFling()和dispatchNestedPreFling()方法,NestedScrollParent接口也提供了對(duì)應(yīng)的onNestedFlin()和onNestedPreFling()方法。由于目前還沒(méi)想到使用的時(shí)機(jī),暫時(shí)不知道咋用。。所以暫不使用這兩個(gè)。通過(guò)scroll相關(guān)的接口也可以實(shí)現(xiàn)嵌套fling的效果。
fling事件一般在ACTION_UP事件中處理,先通過(guò)overScroller開(kāi)始fling,然后開(kāi)啟嵌套滑動(dòng),注意嵌套滑動(dòng)的類型是ViewCompat.TYPE_NON_TOUCH,代表的就是fling類型。
//ScrollChildLayout.kt override fun touchUp() {velocityTracker.computeCurrentVelocity(1000, maxFlingVelocity.toFloat())val yVelocity = velocityTracker.yVelocityif (abs(yVelocity) >= minFlingVelocity) {flingWithOverScroller(-yVelocity)startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH)lastScrollY = scrollYViewCompat.postInvalidateOnAnimation(this)} }同時(shí)computeScroll()方法中也要配合實(shí)現(xiàn)嵌套滑動(dòng),在子View調(diào)用scrollBy()方法的之前先通過(guò)dispatchNestedPreScroll()詢問(wèn)父View是否需要處理嵌套滑動(dòng)事件,然后子View再消耗剩下的滑動(dòng)距離,實(shí)現(xiàn)方法類似處理ACTION_MOVE事件中的嵌套滑動(dòng)處理。但要注意滑動(dòng)的類型是ViewCompat.TYPE_NON_TOUCH。
//ScrollChildLayout.kt override fun computeScroll() {if (overScroller.computeScrollOffset()) {val deltaY = overScroller.currY - lastScrollYvar unconsumed = deltaYlastScrollY = overScroller.currYif (dispatchNestedPreScroll(0,unconsumed,scrollConsumed,null,ViewCompat.TYPE_NON_TOUCH)) {unconsumed -= scrollConsumed[1]totalParentConsumeScrollY += scrollConsumed[1]}if (unconsumed != 0 && canScrollVertically(1) || canScrollVertically(-1)) {//防止滑出邊界val selfConsume = getRealScrollDistance(unconsumed)scrollBy(0, -selfConsume)}}if (!overScroller.isFinished) {ViewCompat.postInvalidateOnAnimation(this)} else {stopNestedScroll(ViewCompat.TYPE_NON_TOUCH)}awakenScrollBars()}之所以能通過(guò)startNestedScroll()的方式來(lái)處理嵌套fling,是因?yàn)榍短譻croll本質(zhì)上是在調(diào)用scrollBy()方法之前詢問(wèn)父View是否要消費(fèi)滑動(dòng)距離,而ACTION_MOVE中的跟手滑動(dòng)和fling中的慣性滑動(dòng),都是調(diào)用的scrollBy()方法,所以都可以通過(guò)startNestedScroll()來(lái)處理嵌套滑動(dòng)。
問(wèn)題1:嵌套fling的滑動(dòng)距離明顯不夠,比預(yù)期的要短
這個(gè)問(wèn)題的原因類似于嵌套Scroll中的嵌套滑動(dòng)距離過(guò)短,它們都是由于當(dāng)前View(ScrollChildLayout)的位置也發(fā)生了變化,導(dǎo)致了計(jì)算的手指移動(dòng)距離過(guò)短而導(dǎo)致的。由于fling事件需要通過(guò)velocityTracker.addMovement(event)事先添加該次觸摸事件序列中的所有事件,然后根據(jù)所有的event來(lái)計(jì)算出速度,由于event不加處理的情況下,會(huì)由于View(ScrollChildLayout)的滑動(dòng)導(dǎo)致event的位置不準(zhǔn)確,這樣計(jì)算出的速度也是不準(zhǔn)確的。我們可以使用類似上邊處理嵌套滑動(dòng)的手段計(jì)算出當(dāng)前View(ScrollChildLayout)滑動(dòng)的偏差。然后將event加上對(duì)應(yīng)的偏差值,然后再添加到velocityTracker中即可校準(zhǔn)速度。
//ScrollChildLayout.ktoverride fun handleScroll(currX: Float, currY: Float) {...if (dispatchNestedPreScroll(0,unconsumed,scrollConsumed,scrollOffset,ViewCompat.TYPE_TOUCH)) {unconsumed -= scrollConsumed[1]//計(jì)算此次滑動(dòng)事件序列的總偏差值,用于校正fling的速度nestedYOffset += scrollOffset[1]lastY -= scrollOffset[1]}... }override fun onTouchEvent(event: MotionEvent?): Boolean {...val offsetEvent = MotionEvent.obtain(event)//根據(jù)總的嵌套滑動(dòng)偏移量,校正速度offsetEvent.offsetLocation(0f, nestedYOffset.toFloat())velocityTracker.addMovement(offsetEvent)offsetEvent.recycle()... }未解決的問(wèn)題:
原因:ScrollChildLayout的可滑動(dòng)范圍=totalHeight - visibleHeight,初始時(shí)visibleHeight= ScrollParentLayout.visibleHeight - TopViewHeight,而隨著ScrollChildLayout的向上滑動(dòng),其visibleHeight會(huì)慢慢增加,直到等于ScrollParentLayout.visibleHeight。目前ScrollChildLayout.visibleHeight未動(dòng)態(tài)修改。
總結(jié)
以上是生活随笔為你收集整理的Android 实现嵌套滑动的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 计算机动画关键技术,计算机动画关键技术综
- 下一篇: Android 4.0模拟器弹出---“