中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Android中如何實現嵌套滾動

發布時間:2022-04-15 16:31:00 來源:億速云 閱讀:131 作者:iii 欄目:編程語言

這篇文章主要介紹了Android中如何實現嵌套滾動的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Android中如何實現嵌套滾動文章都會有所收獲,下面我們一起來看看吧。

業務需求是:

  1. VT容器可以滾動;

  2. 書籍封面可以滾動,并且有視差;

  3. 當VT容器滾動到頂部時,滾動列表,并且滾動可以銜接。

  4. 當列表滾動到頂部時,可以滾動書籍封面以及VT容器,并且滾動可以銜接

邏輯清楚了,接下來就看如何實現了。在android5以前,對于這種滾動,我們只能選擇自己去攔截事件并處理,但在后面的某個版本,android推出了NestingScroll機制,開發者的日子就好過多了,并且android提供了一個非常好的容器類:CoordinatorLayout,極大的簡化了開發者的工作。當然我們也需要投入精力去學習并運用這些新的Api了。

當然,我們也要知道如果沒有這些API,我們應當如何去實現這些效果。因此本文會用三種方式去實現這個效果:

  1. 純事件攔截與派發方案

  2. 基于NestingScroll機制的實現方案

  3. 基于CoordinatorLayout與Behavior方案的實現

示例代碼放在Github上,可以clone下來結合文章觀看

純事件攔截與派發方案

這是最為原始的方案,當然也靈活性***的了。其它的方案原理上都是系統基于它提供的封裝。使用這種方案時,我們需要解決以下幾個問題:

  1. view的滾動(Scroller);

  2. view的速度追蹤(VelocityTracker);

  3. 當VT容器滾動到頂部時,我們如何將事件傳遞給ListView?

  4. 當ListView滾動到頂部時,VT容器如何攔截到事件?

1、2兩點屬于滾動的基礎知識,這里不會做細致的講解。而第3點為何會出現呢?因為android系統在事件派發時,如果事件被攔截,那么之后的事件都將不會傳遞給子view了。其解決方案也很簡單:在滾動到頂部時主動派發一次Down事件:

if (mTargetCurrentOffset + dy <= mTargetEndOffset) {     moveTargetView(dy);     // 重新dispatch一次down事件,使得列表可以繼續滾動     int oldAction = ev.getAction();     ev.setAction(MotionEvent.ACTION_DOWN);     dispatchTouchEvent(ev);     ev.setAction(oldAction); } else {     moveTargetView(dy); }

那么第4點是什么問題呢?這里就需要清楚一個坑點了:不是所用的事件都會走入onInterceptTouchEvent。有一種情況是子View主動調用parent.requestDisallowInterceptTouchEvent(true)來告訴系統說:這個事件我要了,父View不要攔截了。這就是所謂的內部攔截法。在ListView的某些時刻它會去調用這個方法。因此一旦事件傳遞給了ListView,外部容器就拿不到這個事件了。因此我們要打破它的內部攔截:

@Override public void requestDisallowInterceptTouchEvent(boolean b) {     // 去掉默認行為,使得每個事件都會經過這個Layout }

方法如上,把requestDisallowInterceptTouchEvent的實現干掉就可以了。

主要的技術點已近提出來了。那么下面就看具體實現,首先看使用xml:

<org.cgspine.nestscroll.one.EventDispatchPlanLayout     android:id="@+id/scrollLayout"     android:layout_marginTop="?attr/actionBarSize"     android:layout_width="match_parent"     android:layout_height="match_parent"     app:header_view="@+id/book_header"     app:target_view="@+id/scroll_view"     app:header_init_offset="30dp"     app:target_init_offset="70dp">     <View         android:id="@id/book_header"         android:layout_width="120dp"         android:layout_height="150dp"         android:background="@color/gray"/>     <org.cgspine.nestscroll.one.EventDispatchTargetLayout         android:id="@id/scroll_view"         android:layout_width="match_parent"         android:layout_height="match_parent"         android:orientation="vertical"         android:background="@color/white">         <android.support.design.widget.TabLayout             android:id="@+id/tab_layout"             android:background="@drawable/list_item_bg_with_border_top_bottom"             android:layout_width="match_parent"             android:layout_height="@dimen/tab_layout_height"             android:fillViewport="true"/>         <android.support.v4.view.ViewPager             android:id="@+id/viewpager"             android:layout_width="match_parent"             android:layout_height="0dp"             android:layout_weight="1"/>     </org.cgspine.nestscroll.one.EventDispatchTargetLayout> </org.cgspine.nestscroll.one.EventDispatchPlanLayout>

EventDispatchTargetLayout實現了自定義接口ITargetView:

public interface ITargetView {     boolean canChildScrollUp();     void fling(float vy); }

這是因為與具體業務抽離,我并不清楚內層盒子是怎樣的(有可能就是ListView了,也有可能是ViewPager包裹ListView)

主要的實現在EventDispatchPlanLayout,使用時在xml中指定header_init_offset、target_init_offset等變量就可以了,基本上與業務邏輯獨立。

其重點實現邏輯在onInterceptTouchEvent與onTouchEvent中了。個人不是很建議去動dispatchTouchEvent,雖然所有事件都會經過這里,但是這也明顯會增加代碼處理復雜度:

public boolean onInterceptTouchEvent(MotionEvent ev) {     ensureHeaderViewAndScrollView();     final int action = MotionEventCompat.getActionMasked(ev);     int pointerIndex;      // 不阻斷事件的快路徑:如果目標view可以往上滾動或者`EventDispatchPlanLayout`不是enabled     if (!isEnabled() || mTarget.canChildScrollUp()) {         Log.d(TAG, "fast end onIntercept: isEnabled = " + isEnabled() + "; canChildScrollUp = "                 + mTarget.canChildScrollUp());         return false;     }     switch (action) {         case MotionEvent.ACTION_DOWN:             mActivePointerId = ev.getPointerId(0);             mIsDragging = false;             pointerIndex = ev.findPointerIndex(mActivePointerId);             if (pointerIndex < 0) {                 return false;             }             // 在down的時候記錄初始的y值             mInitialDownY = ev.getY(pointerIndex);             break;          case MotionEvent.ACTION_MOVE:             pointerIndex = ev.findPointerIndex(mActivePointerId);             if (pointerIndex < 0) {                 Log.e(TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");                 return false;             }              final float y = ev.getY(pointerIndex);             // 判斷是否dragging             startDragging(y);             break;          case MotionEventCompat.ACTION_POINTER_UP:             // 雙指邏輯處理             onSecondaryPointerUp(ev);             break;          case MotionEvent.ACTION_UP:         case MotionEvent.ACTION_CANCEL:             mIsDragging = false;             mActivePointerId = INVALID_POINTER;             break;     }      return mIsDragging; }

代碼邏輯很清晰,應該不用多說。接下來看onTouchEvent的處理邏輯。

public boolean onTouchEvent(MotionEvent ev) {     final int action = MotionEventCompat.getActionMasked(ev);     int pointerIndex;      if (!isEnabled() || mTarget.canChildScrollUp()) {         Log.d(TAG, "fast end onTouchEvent: isEnabled = " + isEnabled() + "; canChildScrollUp = "                 + mTarget.canChildScrollUp());         return false;     }    // 速度追蹤    acquireVelocityTracker(ev);      switch (action) {         case MotionEvent.ACTION_DOWN:             mActivePointerId = ev.getPointerId(0);             mIsDragging = false;             break;          case MotionEvent.ACTION_MOVE: {             pointerIndex = ev.findPointerIndex(mActivePointerId);             if (pointerIndex < 0) {                 Log.e(TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");                 return false;             }             final float y = ev.getY(pointerIndex);             startDragging(y);              if (mIsDragging) {                 float dy = y - mLastMotionY;                 if (dy >= 0) {                     moveTargetView(dy);                 } else {                     if (mTargetCurrentOffset + dy <= mTargetEndOffset) {                         moveTargetView(dy);                         // 重新dispatch一次down事件,使得列表可以繼續滾動                         int oldAction = ev.getAction();                         ev.setAction(MotionEvent.ACTION_DOWN);                         dispatchTouchEvent(ev);                         ev.setAction(oldAction);                     } else {                         moveTargetView(dy);                     }                 }                 mLastMotionY = y;             }             break;         }         case MotionEventCompat.ACTION_POINTER_DOWN: {             pointerIndex = MotionEventCompat.getActionIndex(ev);             if (pointerIndex < 0) {                 Log.e(TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index.");                 return false;             }             mActivePointerId = ev.getPointerId(pointerIndex);             break;         }          case MotionEventCompat.ACTION_POINTER_UP:             onSecondaryPointerUp(ev);             break;          case MotionEvent.ACTION_UP: {             pointerIndex = ev.findPointerIndex(mActivePointerId);             if (pointerIndex < 0) {                 Log.e(TAG, "Got ACTION_UP event but don't have an active pointer id.");                 return false;             }              if (mIsDragging) {                 mIsDragging = false;                 // 獲取瞬時速度                 mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);                 final float vy = mVelocityTracker.getYVelocity(mActivePointerId);                 finishDrag((int) vy);             }             mActivePointerId = INVALID_POINTER;             //釋放速度追蹤             releaseVelocityTracker();             return false;         }         case MotionEvent.ACTION_CANCEL:             releaseVelocityTracker();             return false;     }      return mIsDragging; }

或許有人會說:為何與onInterceptTouchEvent與有很多重復代碼?這是因為如果事件不打斷,并且子類不處理,就會走進onTouchEvent邏輯,所以這些重復處理是有意義的(其實是抄SwipeRefreshLayout的)。里面主要的邏輯就是兩個:

  1. 滾動容器

  2. TouchUp時滾動到特定位置以及fling傳遞

滾動容器的邏輯:

private void moveTargetViewTo(int target) {     target = Math.max(target, mTargetEndOffset);     // 用offsetTopAndBottom來偏移view     ViewCompat.offsetTopAndBottom(mTargetView, target - mTargetCurrentOffset);     mTargetCurrentOffset = target;      // 滾動書籍封面view,根據TargetView進行定位     int headerTarget;     if (mTargetCurrentOffset >= mTargetInitOffset) {         headerTarget = mHeaderInitOffset;     } else if (mTargetCurrentOffset <= mTargetEndOffset) {         headerTarget = mHeaderEndOffset;     } else {         float percent = (mTargetCurrentOffset - mTargetEndOffset) * 1.0f / mTargetInitOffset - mTargetEndOffset;         headerTarget = (int) (mHeaderEndOffset + percent * (mHeaderInitOffset - mHeaderEndOffset));     }     ViewCompat.offsetTopAndBottom(mHeaderView, headerTarget - mHeaderCurrentOffset);     mHeaderCurrentOffset = headerTarget; }

TouchUp的滾動邏輯:

private void finishDrag(int vy) {     Log.i(TAG, "TouchUp: vy = " + vy);     if (vy > 0) {         // 向下觸發fling,需要滾動到Init位置         mNeedScrollToInitPos = true;         mScroller.fling(0, mTargetCurrentOffset, 0, vy,                 0, 0, mTargetEndOffset, Integer.MAX_VALUE);         invalidate();     } else if (vy < 0) {        // 向上觸發fling,需要滾動到End位置         mNeedScrollToEndPos = true;         mScroller.fling(0, mTargetCurrentOffset, 0, vy,                 0, 0, mTargetEndOffset, Integer.MAX_VALUE);         invalidate();     } else {         // 沒有觸發fling,就近原則         if (mTargetCurrentOffset <= (mTargetEndOffset + mTargetInitOffset) / 2) {             mNeedScrollToEndPos = true;         } else {             mNeedScrollToInitPos = true;         }         invalidate();     } }

當然這里會打上一些標志位,具體實現是在computeScroll中,這屬于Scroller的功能,這里就不展開了。

這樣大體邏輯就講述清楚了,其它細節就請看官直接看源碼了。

基于NestingScroll機制的實現方案

NestingScroll機制是在某個版本support包加入的,不過外界極少有文章介紹,所以應該大多數人并不知道這個機制。NestingScroll主要有兩個接口:

  • NestedScrollingParent

  • NestedScrollingChild

當我們需要使用NestingScroll特性時,我們去實現這兩個接口就好了。NestingScroll本質是內部攔截發然后將相應的接口開給外界。因此實現NestedScrollingChild接口是有難度的,不過像RecyclerView這些控件,官方已經幫我們實現好了NestedScrollingChild,要完成我們的需求,我們直接拿來用就好了(ListView就沒辦法使用了,當然你也可以去實現NestedScrollingChild接口)。并且NestedScrollingChild與NestedScrollingParent只要有嵌套關系就行了,并不一定NestedScrollingChild是直接的子View。

我們來來看看NestedScrollingParent的定義:

public interface NestedScrollingParent {     // 是否接受NestingScroll     public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);     // 接受NestingScroll的Hook鉤子     public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);     // NestingScroll結束     public void onStopNestedScroll(View target);     // NestingScroll進行中。重要參數dxUnconsumed, dyUnconsumed: 用于表示沒有被消耗的滾動量,一般是列表滾動到頭了,就會產生未消耗量     public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);     // NestingScroll滾動之前。重要參數consumed: 是用于告訴子View我消耗了多少。如果位全部消耗dy,那么子view就可以消耗了。     public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);     // fling時     public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);     // fling之前:可以由父元素消耗這次fling事件     public boolean onNestedPreFling(View target, float velocityX, float velocityY);    // 獲取滾動軸: x軸或y軸    public int getNestedScrollAxes(); }

關于“Android中如何實現嵌套滾動”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“Android中如何實現嵌套滾動”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

福清市| 廉江市| 资溪县| 乃东县| 黑水县| 稷山县| 冕宁县| 清徐县| 南宁市| 泰来县| 荣成市| 宣威市| 呼图壁县| 上林县| 南江县| 延庆县| 新巴尔虎左旗| 栾川县| 衡阳县| 汝城县| 临沭县| 潜江市| 利津县| 定安县| 奇台县| 左贡县| 清流县| 凉山| 聂荣县| 雷波县| 滨海县| 时尚| 河池市| 密山市| 三穗县| 昌图县| 华宁县| 晋中市| 横峰县| 白水县| 新绛县|