自定義View事件篇進階篇(二)-自定義NestedScrolling實戰

前言

在上篇文章自定義View事件之進階篇(一)-NestedScrolling(嵌套滑動)機制中,咱們分析了谷歌對NestedScrolling機制的設計,瞭解的不一樣接口的使用場景。如今就讓咱們一塊兒結合一個實際的使用例子,來鞏固以前學習的知識點吧。php

效果展現

先看咱們須要仿寫的實際效果吧。以下圖所示:java

Demo展現

上文展現的demo,在項目NestedScrollingDemo有具體實現。android

在上述Demo中,整個界面分爲標題欄、展現圖片、TabLayout、ViewPage。其中ViewPager中擁有多個Fragment。其中每一個fragment中都對應着一個RecyclerView。整個Demo的實現效果以下所示:git

  • 當產生向上的手勢滑動與fling時,若是展現圖片沒被父控件遮擋,那麼父控件先攔截事件並滑動。當圖片徹底被遮擋時,子控件(RecyclerView)再接着處理。
  • 當產生向下的手勢滑動與fling時,若是展現圖片沒徹底顯示,那麼父控件先攔截事件並滑動。當圖片徹底顯示時,子控件(RecyclerView)再接着處理。
  • 標題欄中的回退鍵,會隨着父控件的滑動,有一個從白色到黑色的漸變效果。
  • 標題欄中的透明度,會隨着父控件的滑動,透明度從0到1的變化效果。

如今就讓咱們一塊兒來實現該效果吧!!github

接口使用分析

要實現嵌套滑動,咱們首先想到的是要實現NestedScrollingChild與NestedScrollingParent接口,可是咱們這裏的Demo須要父控件處理部分fling,因此咱們這裏要使用NestedScrollingChild2與NestedScrollingParent2接口。又由於RecyclerView、NestedScrollView等滾動的View,在谷歌中都實現了NestedScrollingChild2接口,因此咱們不用單獨來處理子控件對手勢滑動與fling的分發,咱們只用關心父控件的處理就好了。app

又由於總體佈局爲豎直方向,因此這裏咱們採用了繼承LinearLayout並實現NestedScrollingParent2接口的方式。同時爲了兼容低版本,咱們也要使用NestedScrollingParentHelper這個幫助類。具體類實現類StickyNavLayout代碼以下所示;框架

public class StickyNavLayout extends LinearLayout implements NestedScrollingParent2 {

    private NestedScrollingParentHelper mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);

    public StickyNavLayout(Context context) {
        this(context, null);
    }

    public StickyNavLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public StickyNavLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setOrientation(VERTICAL);//設置佈局方向爲豎直方向。
    }

    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
         return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
    }

    @Override
    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type);
    }

    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {}


    @Override
    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {}

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }

    @Override
    public void onStopNestedScroll(@NonNull View target, int type) {
        mNestedScrollingParentHelper.onStopNestedScroll(target, type);
    }

    @Override
    public int getNestedScrollAxes() {
        return mNestedScrollingParentHelper.getNestedScrollAxes();
    }
}
複製代碼

在上述代碼中,咱們須要注意如下幾點:ide

  • StickyNavLayout實現類默認佈局爲豎直方向。
  • 爲了讓父控件處理豎直方向上的事件,咱們須要在onStartNestedScroll方法判斷axes & ViewCompat.SCROLL_AXIS_VERTICAL
  • 爲了讓子控件也處理fling,咱們須要在onNestedPreFling方法中返回false。由於在嵌套滑動機制中,若是該方法返回true,那麼子控件就沒有機會處理fling了。
  • 爲了兼容低版本並得到正確的嵌套滑動狀態,咱們須要在onNestedScrollAccepted、onStopNestedScroll、onStopNestedScroll、中調用NestedScrollingParentHelper的相應方法。

佈局設置

當咱們把父控件(StickNavaLayout)的基本框架搭好後,如今就準備處理整個界面的佈局了。觀察Demo效果,咱們發現當標題欄透明的時候,圖片是徹底展現的,那麼也就說明標題欄佈局的層級是在圖片之上的。大體佈局以下圖所示:佈局

總體佈局.png

繼續觀察Demo實現效果,咱們能夠發現獲得以下幾點:post

  • 當產生向上的手勢滑動與fling時,若是展現圖片沒被父控件遮擋,那麼父控件先攔截事件並滑動。當圖片徹底被遮擋時,子控件再接着處理。
  • 當產生向下的手勢滑動與fling時,若是展現圖片沒徹底顯示,那麼父控件先攔截事件並滑動。當圖片徹底顯示時,子控件再接着處理。

那麼展現圖片遮擋的效果是如何實現的呢?其實很簡單,咱們只須要在咱們的父控件中添加一個與展現圖片相同高度的透明的View就好了。那麼當父控件在滾動的時候,就能夠產生一種遮蓋的效果啦。具體設計以下圖所示:

StickyNavLayout佈局.png

那麼再對應Android的佈局文件,整個界面的佈局大概是下面這個樣子:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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">

    <!--展現圖片-->
    <ImageView android:layout_width="match_parent" android:layout_height="200dp" android:scaleType="fitXY" android:src="@drawable/ic_launcher_background"/>

    <!--標題欄-->
    <include layout="@layout/layout_common_toolbar"/>

    <!--嵌套滑動父控件-->
    <com.jennifer.andy.nestedscrollingdemo.view.StickyNavLayout android:id="@+id/sick_layout" android:layout_width="match_parent" android:layout_height="match_parent">

        <!--透明TopView-->
        <View android:id="@+id/sl_top_view" android:layout_width="match_parent" android:layout_height="200dp"/>
        <!--TabLayout-->
        <android.support.design.widget.TabLayout android:id="@+id/sl_tab" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#fff" app:tabIndicatorColor="@color/colorPrimaryDark"/>
        <!--ViewPager-->
        <android.support.v4.view.ViewPager android:id="@+id/sl_viewpager" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#fff"/>
    </com.jennifer.andy.nestedscrollingdemo.view.StickyNavLayout>

</RelativeLayout>
複製代碼

父控件滑動範圍

在完成了總體界面的佈局後,如今咱們須要處理父控件的滾動。繼續觀察Demo,咱們能發現父控件滾動的範圍爲展現圖片的高度減去標題欄的高度。爲了計算父控件的滾動範圍,咱們須要獲取父控件內部的TopView(與展現圖片高度相同的透明View)的高度。要獲取父控件的子控件,咱們能夠經過onFinishInflate方法。具體代碼以下所示:

private View mTopView;//與展現圖片高度相同的透明View

   @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mTopView = findViewById(R.id.sl_top_view);
    }
複製代碼

獲取了子控件後,咱們能夠在onSizeChanged中獲得,能夠父控件能夠滑動的距離(mCanScrollDistance = 展現圖片的高度 - 標題欄的高度)。具體代碼以下所示:

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        mCanScrollDistance = mTopView.getMeasuredHeight() - getResources().getDimension(R.dimen.normal_title_height);
    }
複製代碼

由於標題欄的高度基本都是48dp,因此我這裏並沒單獨去獲取標題欄的高度,而是在values/dimens文件中聲明瞭normal_title_height = 48dp。

父控件嵌套滑動實現

處理了父控件的滑動範圍,如今到了最關鍵的嵌套滑動的處理了。當ViewPager中的RecyclerView接受到滑動後,會將滑動先分發給父控件,咱們的父控件(StickyNavaLayout)須要判斷是否進行消耗,而判斷是否消耗的條件以下:

  • 當向下滑動時,若是RecyclerView不能繼續向下滑動且父控件(StickyNavaLayout)已經滑動了移動距離後,父控件(StickyNavaLayout)須要消耗。
  • 當向上滑動時,若是父控件(StickyNavaLayout)已經滑動了部分距離,那麼父控件(StickyNavaLayout)須要消耗須要消耗。

具體代碼以下所示:

@Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        //若是子view欲向上滑動,則先交給父view滑動
        boolean hideTop = dy > 0 && getScrollY() < mCanScrollDistance;
        //若是子view欲向下滑動,必需要子view不能向下滑動後,才能交給父view滑動
        boolean showTop = dy < 0 && getScrollY() >= 0 && !target.canScrollVertically(-1);
        if (hideTop || showTop) {
            scrollBy(0, dy);
            consumed[1] = dy;// consumed[0] 水平消耗的距離,consumed[1] 垂直消耗的距離
        }
    }
複製代碼

在上述代碼中,咱們經過調用View的canScrollVertically(int direction)方法來判斷是否可以向下滑動,其中當direcation負數時,是檢查對應View是否可以向下滑動,能,返回爲true,反之返回false。當direcation正數時,是檢查對應View是否可以向上滑動,能,返回爲true,反之返回false。

須要注意的是在onNestedPreScroll方法中,咱們並無區分是手勢滑動仍是fling,也就是區分type爲TYPE_TOUCH(0)仍是TYPE_NON_TOUCH(1)。由於無論是手勢滑動仍是fling。在Demo效果中父控件都須要處理。因此咱們並無進行判斷。

當咱們處理了onNestedPreScroll方法後,咱們還須要處理onNestedScroll方法。由於根據嵌套滑動機制,當父控件預處理後,子控件會再消耗剩餘的距離,若是子控件消耗後,還有剩餘的距離。那麼就又會傳遞給父控件。也就是會走onNestedScroll方法。在該方法中,咱們只須要單獨處理子控件的剩餘的向下fling。具體代碼以下所示:

@Override
    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
        if (dyUnconsumed < 0 && type == ViewCompat.TYPE_NON_TOUCH) {//表示已經向下滑動到頭,且爲fling
            scrollBy(0, dyUnconsumed);
        }
    }

複製代碼

當子控件產生fling時,若是子控件消耗不完,那麼就會傳遞給父控件。也就是dyConsumed確定是有值的,又由於咱們只關心向下的fling。因此上述代碼這樣判斷。

完成了嵌套滑動的處理後,咱們還須要對父控件(StickyNavaLayout)的滾動範圍進行校驗,咱們直接重寫scrollTo方法。進行判斷就行了。

@Override
    public void scrollTo(int x, int y) {
        if (y < 0) {
            y = 0;
        }
        if (y > mCanScrollDistance) {
            y = (int) mCanScrollDistance;
        }
        if (getScrollY() != y) super.scrollTo(x, y);
    }
複製代碼

父控件(StickyNavaLayout)的滾動範圍爲0-mCanScrollDistance。其中mCanScrollDistance = 展現圖片的高度 - 標題欄的高度。

ViewPager高度的矯正

到如今你們可能以爲基本的嵌套滑動就結束了。可是若是你這樣寫的話你會發現一個問題:就是當咱們的父控件(StickyNavaLayout)滾動到標題欄下後,咱們會發現咱們的ViewPager並無填充屏幕剩下的距離,而是會有一個空白距離。以下所示:

空白區域.png

是由於咱們的父控件(StickyNavaLayout)繼承了LinearLayout且ViewPager的高度爲match_parent,那麼根據View的測量規則,ViewPager實際的高度爲屏幕中剩餘的高度。因此父控件(StickyNavaLayout)滾動到標題欄下後,會出現一段空白,那麼爲了使ViewPager填充整個屏幕,咱們須要從新設置ViewPager的高度。也就是咱們須要重寫父控件(StickyNavaLayout)的onMeasure方法。具體代碼以下所示:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //先測量一次
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //ViewPager修改後的高度= 總高度-TabLayout的高度
        ViewGroup.LayoutParams lp = mViewPager.getLayoutParams();
        lp.height = getMeasuredHeight() - mNavView.getMeasuredHeight();
        mViewPager.setLayoutParams(lp);
        //由於ViewPager修改了高度,因此須要從新測量
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
複製代碼

漸變效果實現

如今咱們就剩下最後兩個效果了,回退鍵漸變與標題欄的透明度的變化了,其實實現也很是簡單,由於咱們的父控件(StickyNavaLayout)有一個最大滑動的範圍,那麼咱們就能夠獲得當前父控件滑動的距離與最大滑動範圍的比例,拿到這個比例後,咱們能夠設置標題欄的透明度。也能夠經過谷歌提供的ArgbEvaluator獲得漸變顏色。具體的實現方式,讀者朋友能夠自行思考解決。由於篇幅的限制,這裏就不在講解具體的實現方式了。有須要的小夥伴,能夠參看項目NestedScrollingDemo中的NestedScrolling2DemoActivity中的具體實現。

最後

整個Demo就講解完畢了,你們有什麼問題,歡迎提出~

相關文章
相關標籤/搜索