日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > Android >内容正文

Android

Android 实现嵌套滑动

發布時間:2023/12/14 Android 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Android 实现嵌套滑动 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前言

Android實現簡易版滑動

上次文章中實現了簡易的ScrollerView滑動,但實際使用中許多場景都會涉及到嵌套滑動,在今天的博文中我們基于上次的ScrollLayout來進一步實現嵌套滑動。

嵌套滑動預備知識:https://juejin.cn/post/6844904184911773709

整體頁面結構

<?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個TextView</com.example.nestedscroll.ScrollChildLayout></com.example.nestedscroll.ScrollParentLayout>

嵌套結構中父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實現)以提供滑動功能
  • ScrollParentLayout實現了NestedScrollingParent3接口,作為嵌套滑動的父控件
  • ScrollChildLayout實現了NestedScrollingChild3接口,作為嵌套滑動的子控件

頁面滑不動

運行后發現頁面滑不動,查看NestedScrollLayout的onInterceptTouchEvent()實現,為簡單實現滑動效果,上節中簡單將NestedScrollLayout設置為攔截所有觸摸事件。

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {return true }

這直接導致了頁面滑不動,因為ScrollParentLayout其子View的高度經過onMeasure后都是固定的了,所以ScrollParentLayout的控件高度和內容高度相等,ScrollParentLayout不可滑動。同時由于ScrollParentLayout在外層攔截了觸摸事件,ScrollChildLayout無法接收到觸摸事件,因此也無法響應,所以頁面無法滑動。

結合嵌套滑動的機制(NestedScrollingParent,NestedScrollingChild機制),滑動時間需由子控件來接收,然后通過嵌套滑動機制來確定父控件是否消費部分滑動距離,因此ScrollParentLayout需要保證不攔截觸摸事件,同時ScrollChildLayout需要接收到觸摸事件。

//ScrollParentLayout.kt override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {return false } //ScrollChildLayout.kt //實現參考了NestedScrollView //實現參考了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//開始嵌套滑動,注意不是startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH)}MotionEvent.ACTION_CANCEL,MotionEvent.ACTION_UP,-> {//結束嵌套滑動isBeingDragged = falsestopNestedScroll()}}return isBeingDragged }

重寫onInterceptTouchEvent()中,我們默認不攔截觸摸事件,只有當View表現為正在滑動時才進行攔截,以處理滑動,并在開始滑動時調用startNestedScroll(),手指抬起時調用stopNestedScroll(),由于一個事件序列中會有多個ACTION_MOVE事件,而startNestedScroll()僅僅只在第一次判定為滑動時調用,所以引入了isBeingDragged變量,用以判斷當前是否已經在嵌套滑動了,如果是則直接返回true,對應的邏輯為下邊的代碼。

if (action == MotionEvent.ACTION_MOVE && isBeingDragged){return true}

經過處理后子View可以正常滑動了。

嵌套Scroll

ScrollChildLayout實現NestedScrollChild3接口

嵌套滑動機制中為我們提供了NestedScrollingChildHelper工具類,封裝了基本的子ScrollView向父ScrollView傳遞滑動事件的操作,我們只需要NestedScrollingChildHelper對應的方法即可。注意NestedScrollingChildHelper要手動設置isNestedScrollingEnabled為ture。

private val childHelper = NestedScrollingChildHelper(this).apply {//注意要手動設置isNestedScrollingEnabled為ture,只有開啟此開關,嵌套滑動才有效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實現NestedScrollParent3接口

嵌套滑動機制中也提供了NestedScrollingParentHelper工具類,我們可以使用此工具類來實現onNestedScrollAccepted()和onStopNestedScroll(),其他很多接口需要我們自行根據業務需要實現。

private val parentHelper = NestedScrollingParentHelper(this)override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {//判斷是否處理嵌套滑動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()中我們通過在TOUCH_DOWN事件中調用了startNestedScroll()方法,開啟了嵌套滑動,此方法主要用于確定嵌套滑動的NestedScrollingParent是誰。

接下來就需要由ScrollChildLayout來在滑動時將事件分發給ScrollParentLayout。滑動事件在onTouchEvent()的ACTION_MOVE事件中處理,這里將其抽離出來單獨放在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)} }

上面代碼中,利用嵌套滑動機制,首先dispatchNestedPreScroll()將滑動距離交由ScrollParentLayout來處理,ScrollParentLayout來先消費一部分距離,將剩下未消費的距離交由ScrollChildLayout繼續處理,

ScrollChildLayout在判斷了是否滑出邊界后,調用scrollBy()方法處理剩下的滑動距離。

然后ScrollParentLayout也需要配合完成相應的滑動操作,ScrollParentLayout在onNestedPreScroll()方法中接收到對應的嵌套滑動距離,判斷自身是否要消費。

回顧下目前布局結構是:

-ScrollParentLayout

-TopView

-ScrollChildLayout

ScrollParentLayout有兩種常見處理方式:

  • TopView和ScrollChildLayout同步調用layout(left,top,right,bottom)方法,TopView更新top和bottom,ScrollChildLayout更新top。(實現時遇到些問題,暫未采用)
  • ScrollParentLayout調用scrollBy方法()整體滑動。看起來比較簡單,下邊代碼即采用此方案。但需要先改造onMeasure()方法,讓其能計算出其內容的高度(包括所有可滑動的子View內容的高度)作為其view的height屬性
  • override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {super.onMeasure(widthMeasureSpec, heightMeasureSpec)//visibleHeight為控件可見高度visibleHeight = measuredHeightif (orientation == VERTICAL) {var totalLength = paddingTop + paddingBottomfor (child in children) {totalLength += child.marginTop + child.measuredHeight + child.marginBottom}totalHeight = totalLength}//將measureHeight設置為內容的高度setMeasuredDimension(measuredWidth, totalHeight) }

    在onNestedPreScroll中,我們需要計算出ScrollParentLayout需要消費的滑動距離,主要要保證最后交由ScrollParentLayout處理的滑動的最終位置在[0, topViewHeight]范圍內(即保證TopView可見或剛好不可見的部分才交由ScrollParentLayout處理)。

    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {var consumedY = 0//scrollY以向下為正向,整體相對于初始位置的偏移 -topViewHeight <= scrollY <= 0if (target == scrollChildLayout) {//下滑 && TopView還能再下滑(在初始位置之上)if (dy > 0 && scrollY > 0 && !scrollChildLayout.canScrollVertically(-1)) {consumedY = Math.min(scrollY, dy)//上滑 && TopView還能向上滑(TopView還可見)} else if (dy < 0 && scrollY < topViewHeight) {consumedY = Math.max(-topViewHeight + scrollY, dy)}}if (consumedY != 0) {scrollBy(0, -consumedY)consumed[1] = consumedY} }

    問題1:嵌套滑動距離小于手指滑動距離,滑動抖動

    這個問題由于MotionEvent所對應的View(ScrollChildLayout)移動了所導致的,正常的跟手滑動為ScrollChildLayout不動,則每次滑動的deltaY = currY - lastY。currY和lastY都是通過event.getY()獲取到的,event.getY()獲取到的y值是相對于當前View(ScrollChildLayout)的Y值。由于當前View也朝相同方向滑動了,這導致計算出來的deltaY偏小,從而導致嵌套滑動距離小于手指滑動距離。(TODO滑動抖動)

    解決辦法(參考NestedScrollView):

    我們需要獲取到在ScrollParentLayout滑動時ScrollChildLayout的偏移量,查看dispatchNestedPreScroll()方法,可以使用offsetInWindow這個參數來獲取ScrollParentLayout此次嵌套滑動的偏移量,然后在最后賦值lastY = currY - offsetInWindow[1]來校準偏移量。

    /*** 在滑動之前,將滑動值分發給NestedScrollingParent* @param dx 水平方向消費的距離* @param dy 垂直方向消費的距離* @param consumed 輸出坐標數組,consumed[0]為NestedScrollingParent消耗的水平距離、* consumed[1]為NestedScrollingParent消耗的垂直距離,此參數可空。* @param offsetInWindow 含有View從此方法調用之前到調用完成后的屏幕坐標偏移量,* 可以使用這個偏移量來調整預期的輸入坐標(即上面4個消費、剩余的距離)跟蹤,此參數可空。* @return 返回NestedScrollingParent是否消費部分或全部滑動值*/ boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,@NestedScrollType int type);

    至此就可以流暢的嵌套Scroll了~。

    嵌套Fling

    回顧前文中非嵌套的fling,通過OverScroller來實現滑動。OverScroller需配合computeScroll()方法一起處理fling動作。

    NestedScrollChild接口提供了對應的dispatchNestedFling()和dispatchNestedPreFling()方法,NestedScrollParent接口也提供了對應的onNestedFlin()和onNestedPreFling()方法。由于目前還沒想到使用的時機,暫時不知道咋用。。所以暫不使用這兩個。通過scroll相關的接口也可以實現嵌套fling的效果。

    fling事件一般在ACTION_UP事件中處理,先通過overScroller開始fling,然后開啟嵌套滑動,注意嵌套滑動的類型是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)} }

    同時computeScroll()方法中也要配合實現嵌套滑動,在子View調用scrollBy()方法的之前先通過dispatchNestedPreScroll()詢問父View是否需要處理嵌套滑動事件,然后子View再消耗剩下的滑動距離,實現方法類似處理ACTION_MOVE事件中的嵌套滑動處理。但要注意滑動的類型是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()}

    之所以能通過startNestedScroll()的方式來處理嵌套fling,是因為嵌套scroll本質上是在調用scrollBy()方法之前詢問父View是否要消費滑動距離,而ACTION_MOVE中的跟手滑動和fling中的慣性滑動,都是調用的scrollBy()方法,所以都可以通過startNestedScroll()來處理嵌套滑動。

    問題1:嵌套fling的滑動距離明顯不夠,比預期的要短

    這個問題的原因類似于嵌套Scroll中的嵌套滑動距離過短,它們都是由于當前View(ScrollChildLayout)的位置也發生了變化,導致了計算的手指移動距離過短而導致的。由于fling事件需要通過velocityTracker.addMovement(event)事先添加該次觸摸事件序列中的所有事件,然后根據所有的event來計算出速度,由于event不加處理的情況下,會由于View(ScrollChildLayout)的滑動導致event的位置不準確,這樣計算出的速度也是不準確的。我們可以使用類似上邊處理嵌套滑動的手段計算出當前View(ScrollChildLayout)滑動的偏差。然后將event加上對應的偏差值,然后再添加到velocityTracker中即可校準速度。

    //ScrollChildLayout.ktoverride fun handleScroll(currX: Float, currY: Float) {...if (dispatchNestedPreScroll(0,unconsumed,scrollConsumed,scrollOffset,ViewCompat.TYPE_TOUCH)) {unconsumed -= scrollConsumed[1]//計算此次滑動事件序列的總偏差值,用于校正fling的速度nestedYOffset += scrollOffset[1]lastY -= scrollOffset[1]}... }override fun onTouchEvent(event: MotionEvent?): Boolean {...val offsetEvent = MotionEvent.obtain(event)//根據總的嵌套滑動偏移量,校正速度offsetEvent.offsetLocation(0f, nestedYOffset.toFloat())velocityTracker.addMovement(offsetEvent)offsetEvent.recycle()... }

    未解決的問題:

  • 嵌套滑動時,可滑動邊界判斷不準,滑動到底部還多出一段空白(高度等于TopView)。
  • 原因:ScrollChildLayout的可滑動范圍=totalHeight - visibleHeight,初始時visibleHeight= ScrollParentLayout.visibleHeight - TopViewHeight,而隨著ScrollChildLayout的向上滑動,其visibleHeight會慢慢增加,直到等于ScrollParentLayout.visibleHeight。目前ScrollChildLayout.visibleHeight未動態修改。

    總結

    以上是生活随笔為你收集整理的Android 实现嵌套滑动的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。