使用CoordinatorLayout打造各類炫酷的效果javascript
自定義Behavior —— 仿知乎,FloatActionButton隱藏與展現java
NestedScrolling 機制深刻解析android
一步步帶你讀懂 CoordinatorLayout 源碼git
自定義 Behavior -仿新浪微博發現頁的實現github
ViewPager,ScrollView 嵌套ViewPager滑動衝突解決segmentfault
自定義 behavior - 完美仿 QQ 瀏覽器首頁,美團商家詳情頁瀏覽器
記得兩年前的時候,曾寫過自定義 behavior 的文章 自定義 Behavior -仿新浪微博發現頁的實現,到如今差很少有一萬多的閱讀量吧。微信
今天,對該 behavior 進行升級,相對於兩年前的 behavior,增長了如下功能app
咱們先來看一下新浪微博發現頁的效果:ide
接下來咱們在來看一下咱們兩年前仿照新浪微博實現的效果
仿 QQ 瀏覽器
仿美團商家詳情頁面的:
有兩種狀態,open 和 close 狀態。
從效果圖,咱們能夠看到 在 open 狀態下,咱們向上滑動 ViewPager 裏面的 RecyclerView 的 時候,RecyclerView 並不會向上移動(RecyclerView 的滑動事件交給 外部的容器處理,被被所有消費掉了),而是整個佈局(指 Header + Tab +ViewPager)會向上偏移。當 Tab 滑動到頂部的時候,咱們向上滑動 ViewPager 裏面的 RecyclerView 的時候,RecyclerView 能夠正常向上滑動,即此時外部容器沒有攔截滑動事件。
同時咱們能夠看到在 open 狀態的時候,咱們是不支持下拉刷新的,這個比較容易實現,監聽頁面的狀態,若是是 open 狀態,咱們設置 SwipeRefreshLayout setEnabled 爲 false,這樣不會 攔截事件,在頁面 close 的時候,設置 SwipeRefreshLayout setEnabled 爲 TRUE,這樣就能夠支持下拉刷新了。
基於上面的分析,咱們這裏能夠把整個效果劃分爲三個部分
第一部分 Header 部分:在 Header 部分尚未滑動到頂部的時候(即 open 的時候),跟隨手指滑動
第二部分 Content 部分:咱們向上滑動的時候,當Header 處於 open 狀態,這時候 Header 向上滑動, content 部分的 recyclerView 不會滑動,當 header 處於 close 狀態,content 部分向上滑動, RecyclerView 向上滑動。當咱們向下滑動的時候,header 並不會隨着滑動,只會滑動 content 部分的 recyclerView
第三部分 search 部分:當咱們向上滑動的時候,Search 部分會隨着滑動,最終停留在固定的位置.
咱們把這三部分的關係定義爲 Content 依賴於 Header。Header 移動的時候,Content 跟着 移動。因此,咱們在處理滑動事件的時候,只須要處理好 Header 部分的 Behavior 就oK了,Content 部分的 Behavior 不須要處理滑動事件,只需依賴於 Header ,跟着作相應的移動便可。Search 部分的 behavior 也不須要處理滑動事件,只需依賴與 Header,跟着作相應的移動。
至於具體怎麼實現的,能夠看自定義 Behavior -仿新浪微博發現頁的實現,核心思想差很少,這裏再也不重複。
這裏咱們已仿 QQ 瀏覽器 demo 進行說明:
咱們一塊兒來看一下怎樣使用:簡單來講,只須要兩步:
第一步:編寫 xml 文件,並指定相應的 behavior
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_blue_light" android:fitsSystemWindows="true"> <!-- Header 部分--> <FrameLayout android:id="@+id/id_uc_news_header_pager" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_behavior="@string/behavior_qq_browser_header_pager"> <com.xj.qqbroswer.behavior.base.NestedLinearLayout android:layout_width="match_parent" android:layout_height="@dimen/header_height" android:orientation="vertical"> <TextView android:id="@+id/news_tv_header_pager" style="@style/TextAppearance.AppCompat.Title" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center_vertical" android:gravity="center" android:text="QQBrowser Header" android:textColor="@android:color/white" /> </com.xj.qqbroswer.behavior.base.NestedLinearLayout> </FrameLayout> <!-- ContentProvide 部分--> <LinearLayout android:id="@+id/behavior_content" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" app:layout_behavior="@string/behavior_contents"> <android.support.design.widget.TabLayout android:id="@+id/id_uc_news_tab" android:layout_width="match_parent" android:layout_height="@dimen/tabs_height" android:background="@color/colorPrimary" app:tabGravity="fill" app:tabIndicatorColor="@color/colorPrimaryLight" app:tabSelectedTextColor="@color/colorPrimaryLight" app:tabTextColor="@color/colorPrimaryIcons" /> <android.support.v4.view.ViewPager android:id="@+id/id_uc_news_content" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#F0F4C3"> </android.support.v4.view.ViewPager> </LinearLayout> <!--search 部分--> <RelativeLayout android:layout_width="match_parent" android:layout_height="@dimen/header_title_height" app:layout_behavior="@string/behavior_search"> <android.support.v7.widget.SearchView android:layout_width="match_parent" android:layout_height="30dp" android:layout_centerVertical="true" android:layout_marginLeft="10dp" android:layout_marginRight="50dp" android:background="@android:color/white" app:defaultQueryHint="搜索" app:queryHint="搜索"> </android.support.v7.widget.SearchView> <android.support.v7.widget.AppCompatImageView android:layout_width="30dp" android:layout_height="30dp" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_marginRight="10dp" android:src="@mipmap/camera" android:tint="@android:color/white" /> </RelativeLayout> </android.support.design.widget.CoordinatorLayout>
第二步:在代碼裏面動態設置一些參數
private void initBehavior() { Resources resources = DemoApplication.getAppContext().getResources(); mHeaderBehavior = (QQBrowserHeaderBehavior) ((CoordinatorLayout.LayoutParams) findViewById(R.id.id_uc_news_header_pager).getLayoutParams()).getBehavior(); mHeaderBehavior.setPagerStateListener(new QQBrowserHeaderBehavior.OnPagerStateListener() { @Override public void onPagerClosed() { if (BuildConfig.DEBUG) { Log.d(TAG, "onPagerClosed: "); } Snackbar.make(mNewsPager, "pager closed", Snackbar.LENGTH_SHORT).show(); setFragmentRefreshEnabled(true); setViewPagerScrollEnable(mNewsPager, true); } @Override public void onScrollChange(boolean isUp, int dy, int type) { } @Override public void onPagerOpened() { Snackbar.make(mNewsPager, "pager opened", Snackbar.LENGTH_SHORT).show(); setFragmentRefreshEnabled(false); } }); // 設置爲 header height 的相反數 mHeaderBehavior.setHeaderOffsetRange(-resources.getDimensionPixelOffset(R.dimen.header_height)); // 設置 header close 的時候是否可以經過滑動打開 mHeaderBehavior.setCouldScroollOpen(false); mContentBehavior = (QQBrowserContentBehavior) ((CoordinatorLayout.LayoutParams) findViewById(R.id.behavior_content).getLayoutParams()).getBehavior(); // 設置依賴於哪個 id,這裏要設置爲 Header layout id mContentBehavior.setDependsLayoutId(R.id.id_uc_news_header_pager); // 設置 content 部分最終停留的位置 mContentBehavior.setFinalY(resources.getDimensionPixelOffset(R.dimen.header_title_height)); }
mHeaderBehavior.setHeaderOffsetRange 設置 Header 部分的偏移量,咱們是經過 translationY 實現的,所以咱們通常設置爲 header 高度的相反數便可。
mHeaderBehavior.setCouldScroollOpen(false) , 設置 header close 的時候是否可以經過滑動打開。
mContentBehavior.setDependsLayoutId(R.id.id_uc_news_header_pager);設置依賴於哪個 id,這裏要設置爲 Header layout id。 mContentBehavior.setFinalY 設置 content 部分最終停留的位置。
咱們來看一下 OnPagerStateListener 的回調
/** * callback for HeaderPager 's state */ public interface OnPagerStateListener { /** * do callback when pager closed */ void onPagerClosed(); /** * when scrooll, it would call back * * @param isUp isScroollUp * @param dy child.getTanslationY * @param type touch or not touch, TYPE_TOUCH, TYPE_NON_TOUCH */ void onScrollChange(boolean isUp, int dy, @ViewCompat.NestedScrollType int type); /** * do callback when pager opened */ void onPagerOpened(); }
主要有三個方法,第一個方法,onPagerClosed 當 header close 的時候,會回調,第二個方法,當 header 滑動距離變化的時候,會回調 onScrollChange 方法。它有三個參數, isUp 表明是不是向上滑動, dy 表明 header 的偏移量, type 表明類型是 touch 或者是非 touch 的(即 fling 滑動的)
若是你想要作一些酷炫的效果的話,你能夠在 onScrollChange 方法中,根據滑動的距離,各個不一樣的 View 作相應的動畫。
步驟跟上面的仿 QQ 瀏覽器的步驟是同樣的,這裏再也不重複相同的步驟,說幾個關鍵點:
第一:在頁面 header close 的時候,咱們能夠經過滑動打開header,這是經過調用 mHeaderBehavior.setCouldScroollOpen(true); 實現的。
第二:滑動 header, fling 的時候,能夠看到 content 部分的 recyclerView 也在滑動,咱們是經過 header 的 fling 事件作到的,在 onFlingStart 的時候手動調用 RecyclerView 的 smoothScrollBy 進行滑動。
mHeaderBehavior.setOnHeaderFlingListener(new HeaderFlingRunnable.OnHeaderFlingListener() { @Override public void onFlingFinish() { } @Override public void onFlingStart(View child, View target, float velocityX, float velocityY) { Log.i(TAG, "onFlingStart: velocityY =" + velocityY); if (velocityY < 0) { mRecyclerView.smoothScrollBy(0, (int) Math.abs(velocityY), new AccelerateDecelerateInterpolator()); } } @Override public void onHeaderClose() { } @Override public void onHeaderOpen() { } });
header 部分沒法響應滑動事件
咱們是經過自定義一個 NestedLinearLayout ,重寫它的 onTouchEvent 事件,經過 NestedScrolling 機制將事件傳遞給 NestedScrollingParent,即 CoordinatorLayout,而 NestedScrollingParent 會交給子 View 的 behavior 進行處理。
@Override public boolean onTouchEvent(MotionEvent event) { mGestureDetector.onTouchEvent(event); final int action = MotionEventCompat.getActionMasked(event); switch (action) { case MotionEvent.ACTION_DOWN: startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL | ViewCompat.SCROLL_AXIS_VERTICAL); break; case MotionEvent.ACTION_MOVE: int dy = (int) (event.getRawY() - lastY); lastY = (int) event.getRawY(); // dy < 0 上滑, dy>0 下拉 if (dy < 0) { // 上滑的時候交給父類去處理 if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) // 若是找到了支持嵌套滾動的父類 && dispatchNestedPreScroll(0, -dy, consumed, offset)) {// // 父類進行了一部分滾動 } } else { if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) // 若是找到了支持嵌套滾動的父類 && dispatchNestedScroll(0, 0, 0, -dy, offset)) {// // 父類進行了一部分滾動 } } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: stopNestedScroll(); break; } return super.onTouchEvent(event); }
當咱們給 header 的子 View 設置點擊事件的時候,沒法滑動 header
對 Android 事件分發機制有必定了解的,都知道,在 Android 中,默認的事件傳遞機制是這樣的,
當TouchEvent發生時,首先Activity將TouchEvent傳遞給最頂層的View,TouchEvent最早到達最頂層 view 的 dispatchTouchEvent ,而後由 dispatchTouchEvent 方法進行分發。
若是dispatchTouchEvent返回 false ,則回傳給父View的onTouchEvent事件處理;
onTouchEvent事件返回true,事件終結,返回false,交給父View的OnTouchEvent方法處理
若是dispatchTouchEvent返回super的話,默認會調用本身的onInterceptTouchEvent方法
默認的狀況下interceptTouchEvent回調用super方法,super方法默認返回false,因此會交給子View的onDispatchTouchEvent方法處理
若是 interceptTouchEvent 返回 true ,也就是攔截掉了,則交給它的 onTouchEvent 來處理
若是 interceptTouchEvent 返回 false ,那麼就傳遞給子 view ,由子 view 的 dispatchTouchEvent 再來開始這個事件的分發。
所以,當咱們給子 View 設置點擊事件的時候,因爲默認的 parent 沒有攔截事件,會走到子 View 的 onToucheEvent 事件中,因爲設置了點擊事件,事件被消費了,因此不會回調父 View onTouchEvent 中的 ACTION_MOVE 事件。
解決辦法: 重寫 NestedLinearLayout 的 onInterceptToucheEvent 事件,當是 ACTION_MOVE 事件的時候,返回 true ,攔截,這樣會調用本身的 onTouchEvent 事件,從而保證能夠滑動。
@Override public boolean onInterceptTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mDownY = (int) event.getRawY(); // 當開始滑動的時候,告訴父view startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL | ViewCompat.SCROLL_AXIS_VERTICAL); break; case MotionEvent.ACTION_MOVE: // 確保不消耗 ACTION_DOWN 事件 if (Math.abs(event.getRawY() - mDownY) > mScaledTouchSlop) { logD("onInterceptTouchEvent: ACTION_MOVE mScaledTouchSlop =" + mScaledTouchSlop); return true; } } return super.onInterceptTouchEvent(event); }
但這裏還有一個坑,正常一個點擊事件,會促發 ACTION_DOWN, ACTION_MOVE, ACTION_UP,若是咱們直接在 ACTION_MOVE 裏面返回 true,將會致使子 View 的 onClick 事件失效。
解決辦法:
final ViewConfiguration configuration = ViewConfiguration.get(getContext()); mScaledTouchSlop = configuration.getScaledTouchSlop(); if (Math.abs(event.getRawY() - mDownY) > mScaledTouchSlop) { return true; }
關於滑動衝突解決的,能夠看我之前的一篇博客:ViewPager,ScrollView 嵌套ViewPager滑動衝突解決
如何判斷 header 是 fling 動做
咱們這裏經過手勢處理器 GestureDetector 作到的,固然你也能夠經過 VelocityTracker 計算,只不過比較繁瑣
public boolean onTouchEvent(MotionEvent event) { mGestureDetector.onTouchEvent(event); } GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() { @Override public boolean onDown(MotionEvent e) { return false; } -----// 省略若干代碼 @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { Log.d(TAG, "onFling: velocityY =" + velocityY); // fling((int) velocityY); getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY); return false; } }; mGestureDetector = new GestureDetector(getContext(), onGestureListener);
有時候,作一些筆記真的挺重要的。
這一次寫這一篇博客,是由於在項目中要作相似的效果。剛開始,真的沒什麼思路。但清楚得記得兩年前寫過相似的文章,具體實現原理早已忘光。我查看了兩年前的博客,整理了一下思路,將代碼搬到項目中,發現了一些坑。修修補補,把坑都填了。
試想一下,若是當初沒有將原理記錄下來,這個效果,真的挺難實現的。若是你對 Coordinatorlayout , behavior,NestedScroll 機制這些不熟悉,你根本就沒法實現。兩年前寫 自定義 Behavior -仿新浪微博發現頁的實現 這篇博客的時候,收到挺多私信的,有一些反饋說他們作這個效果作了兩個多星期仍是沒法實現,挺感謝我寫這篇博客的。所以,從如今起,不妨嘗試一下多作一下筆記。真的,好記性不如爛筆頭。
第二點感觸比較深的是,剛開始,我看了我兩年前寫的代碼,我一開始的反應,我去,這是什麼垃圾代碼。確實,不少地方寫得挺爛的,behavior 耦合業務邏輯,很難複用,也很差維護。所以,這一次,我在空閒的時間將 behavior 抽離出來,之後要實現相似的效果,輕鬆實現, biu biu biu。
說這麼多,總結以下
以爲效果還不錯的,能夠動手掃一掃關注個人微信公衆號,或者到個人 github 上面 star,謝謝