使用NestedScrollView+ViewPager+RecyclerView+SmartRefreshLayout打造酷炫下拉視差效果並解決各類滑動衝突

使用NestedScrollView+ViewPager+RecyclerView+SmartRefreshLayout打造酷炫下拉視差效果並解決各類衝突

若是你還在爲處理滑動衝突而發愁,那麼你須要靜下心來看看這邊文章,若是你能完全理解這篇文章中使用的技術,那麼,一切滑動衝突的問題解決起來就垂手可得了:
先扔一個最終實現的效果圖


先分析下效果圖中實現的功能點
  • 頂部下拉時背景圖造成視差效果
  • 上拉時標題欄透明切換顯示
  • 底部實現TabLayout+ViewPager+Fragment+RecyclerView
  • NestedScrollView+ViewPager的滑動衝突解決
  • NestedScrollView+RecyclerView滑動衝突的解決

複雜在哪裏?整個佈局中使用了SmartRefreshLayout,NestedScrollView,ViewPager,RecyclerView,每個都有滑動事件,咱們平時只是使用ScrollView+RecyclerView都會有滑動衝突,更況且這裏有四個會引發衝突的控件一塊兒使用!

接下來,咱們一步一步實現這個效果
一、佈局設計分析
-FrameLayout(最外層)
    -ImageView(頭部背景圖)
        -SmartRefreshLayout(頭部刷新控件)
            -JudgeNestedScrollView(自定義的NestedScrollView)
                ...省略中間巴拉巴拉布局
                -Tablayout
                    -ViewPager

二、功能點實現說明
2.一、下拉時視差效果的實現
最外層爲FrameLayout,ImageView高度設置超過屏幕頂部,藉助SmartRefreshLayout控件在下拉和鬆開時頭部背景圖作平移處理,背景圖片作了高斯模糊處理

佈局代碼:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <ImageView
        android:id="@+id/iv_header"
        android:layout_width="match_parent"
        android:layout_height="670dp"
        android:layout_marginTop="-300dp"
        android:adjustViewBounds="true"
        android:contentDescription="@string/app_name"
        android:scaleType="centerCrop"
        android:src="@drawable/image_home"
        app:layout_collapseMode="parallax" />

    <com.scwang.smartrefresh.layout.SmartRefreshLayout
        android:id="@+id/refreshLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:srlEnablePreviewInEditMode="false">
        ...
下拉刷新時頭部背景圖片平移代碼:

refreshLayout.setOnMultiPurposeListener(new SimpleMultiPurposeListener() {
            @Override
            public void onHeaderPulling(RefreshHeader header, float percent, int offset, int bottomHeight, int extendHeight) {
                mOffset = offset / 2;
                ivHeader.setTranslationY(mOffset - mScrollY);
            }

            @Override
            public void onHeaderReleasing(RefreshHeader header, float percent, int offset, int bottomHeight, int extendHeight) {
                mOffset = offset / 2;
                ivHeader.setTranslationY(mOffset - mScrollY);
            }
        });

2.二、TabLayout的頂部懸浮效果的實現
此處使用的是最爲簡單笨拙的方法,兩個TabLayout,一個固定爲屏幕頂部ToolBar下面,並隱藏,另外一個正常繪製在佈局中;
計算ToolBar的高度,根據NestedScrollView滑動的高度(這裏的高度指的是跟隨滑動的TabLayout的Y座標)剛好到ToolBar的高度位置時顯示隱藏的ToolBar;

-FrameLayout
    -SmartRefreshLayout
        -Tablayout
        -Viewpager
    -SmartRefreshLayout
    -RelativeLayout
        -Toolbar
        -Tablayout
    -RelativeLayout
-FrameLayout
 
toolbar.post(new Runnable() {
            @Override
            public void run() {
                toolBarPositionY = toolbar.getHeight();
            }
        });
 
scrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
            @Override
            public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                int[] location = new int[2];
                magicIndicator.getLocationOnScreen(location);
                int xPosition = location[0];
                int yPosition = location[1];
                if (yPosition < toolBarPositionY) {
                    toolBarTablayout.setVisibility(View.VISIBLE);
                } else {
                    toolBarTablayout.setVisibility(View.GONE);
                }
            }
        });

2.三、ToolBar的漸變透明度以及按鈕的切換

<android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            style="@style/AppTheme.Toolbar"
            android:layout_marginBottom="0dp"
            android:background="@android:color/transparent"
            app:layout_collapseMode="pin">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_vertical"
                android:orientation="horizontal">

                <ImageView
                    android:id="@+id/iv_back"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:src="@drawable/back_white" />


                <android.support.v7.widget.ButtonBarLayout
                    android:id="@+id/buttonBarLayout"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:layout_weight="1"
                    android:gravity="center">

                    <de.hdodenhof.circleimageview.CircleImageView
                        android:id="@+id/toolbar_avatar"
                        style="@style/UserTitleAvatar"
                        android:src="@drawable/timg" />

                    <TextView
                        android:id="@+id/toolbar_username"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center"
                        android:maxLines="1"
                        android:text="SiberiaDante"
                        android:textColor="@color/mainBlack"
                        android:textSize="@dimen/font_16" />


                </android.support.v7.widget.ButtonBarLayout>

                <ImageView
                    android:id="@+id/iv_menu"
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:layout_gravity="end"
                    android:src="@drawable/icon_menu_white" />
            </LinearLayout>

        </android.support.v7.widget.Toolbar>

ToolBar中間標題默認隱藏,使用的ButtonBarLayout包裹ImageView和TextView設置百分比透明,具體處理有兩點:

buttonBarLayout.setAlpha(0);
toolbar.setBackgroundColor(0);

* 下拉頭部刷新時ToolBar漸變隱藏,一樣利用SmartRefreshLayout處理

refreshLayout.setOnMultiPurposeListener(new SimpleMultiPurposeListener() {
            @Override
            public void onHeaderPulling(RefreshHeader header, float percent, int offset, int bottomHeight, int extendHeight) {
                toolbar.setAlpha(1 - Math.min(percent, 1));
            }

            @Override
            public void onHeaderReleasing(RefreshHeader header, float percent, int offset, int bottomHeight, int extendHeight) {
                toolbar.setAlpha(1 - Math.min(percent, 1));
            }
        });

* 上下滑動時標題欄漸變顯示和隱藏,並切換圖標顏色(這裏其實是根據臨界點直接更換圖片,處理的比較簡單)

scrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
            int lastScrollY = 0;
            int h = DensityUtil.dp2px(170);
            int color = ContextCompat.getColor(getApplicationContext(), R.color.mainWhite) & 0x00ffffff;

            @Override
            public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                int[] location = new int[2];
                magicIndicator.getLocationOnScreen(location);
                int xPosition = location[0];
                int yPosition = location[1];

                if (lastScrollY < h) {
                    scrollY = Math.min(h, scrollY);
                    mScrollY = scrollY > h ? h : scrollY;
                    buttonBarLayout.setAlpha(1f * mScrollY / h);
                    toolbar.setBackgroundColor(((255 * mScrollY / h) << 24) | color);
                    ivHeader.setTranslationY(mOffset - mScrollY);
                }
                if (scrollY == 0) {
                    ivBack.setImageResource(R.drawable.back_white);
                    ivMenu.setImageResource(R.drawable.icon_menu_white);
                } else {
                    ivBack.setImageResource(R.drawable.back_black);
                    ivMenu.setImageResource(R.drawable.icon_menu_black);
                }

                lastScrollY = scrollY;
            }
        });

2.四、NestedScrollView嵌套ViewPager致使ViewPager高度爲0的處理
不少人可能認爲直接自定義ViewPager,測量子View的高度,讓ViewPager去適應高度便可,其實否則,若是這樣處理的話咱們的Viewpager可能就是無限高度,咱們在處理完NestedScrollView後,無限高度的ViewPager和RecyclerView又是一個問題,因此我這裏的處理是計算ViewPager所須要的最大高度,即TabLayout在最頂部顯示時到屏幕底部的最大高度爲ViewPager高度

 toolbar.post(new Runnable() {
            @Override
            public void run() {
                toolBarPositionY = toolbar.getHeight();
                ViewGroup.LayoutParams params = viewPager.getLayoutParams();
                params.height = SDScreenUtil.getScreenHeight() - toolBarPositionY - tablayout.getHeight()+1;
                viewPager.setLayoutParams(params);
            }
        });
這裏爲何要+1,後面會有解釋

2.五、NestedScrollView嵌套RecyclerView滑動衝突
NestedScrollView嵌套RecyclerView滑動衝突咱們使用事件攔截處理,這裏處理的是NestedScrollView的滑動,首先滑動的時候確定是須要NestedScrollView的滑動事件,因此咱們默認不攔截NestedScrollView的滑動事件,直到TabLayout頂部懸浮的時候,咱們攔截NestedScrollView的滑動事件,交給RecyclerView來處理
* 重寫NestedScrollView

public class JudgeNestedScrollView extends NestedScrollView {
    private boolean isNeedScroll = true;
    ...省略構造方法
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_MOVE:
                return isNeedScroll;
        }
        return super.onInterceptTouchEvent(ev);
    }

    /*
    改方法用來處理NestedScrollView是否攔截滑動事件
     */
    public void setNeedScroll(boolean isNeedScroll) {
        this.isNeedScroll = isNeedScroll;
    }
}

這裏默認不攔截NestedScrollView滑動事件,只有當咱們TabLayout滑動到頂部時纔去攔截,也就是TabLayout顯示隱藏的時候

scrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
            @Override
            public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                int[] location = new int[2];
                magicIndicator.getLocationOnScreen(location);
                int xPosition = location[0];
                int yPosition = location[1];
                if (yPosition < toolBarPositionY) {
                    tablayout.setVisibility(View.VISIBLE);
                    scrollView.setNeedScroll(false);
                } else {
                    tablayout.setVisibility(View.GONE);
                    scrollView.setNeedScroll(true);
                }
至於前面測量ViewPager高度的時候,爲何會+1處理,這是由於,若是不+1時,恰好是TabLayout要出現的臨界點,也就是ViewPager剛好的高度,可是這個時候又恰好是咱們NestedScrollView攔截沒有取消的臨界點,因此,在上滑的時候,TabLayout恰好懸浮頂部時,RecyclerView沒有獲取事件,沒法進行滑動,這就是給ViewPager+1處理的理由;

2.五、NestedScrollView嵌套ViewPager滑動衝突2
若是你足夠細心的話,就會發現,當你的TabLayout上滑到一半的時候,再去左右滑動ViewPager是滑動不了的,由於這個時候NestedScrollView依然消費事件,因此咱們還須要對NestedScrollView事件進行處理,判斷若是是左右滑動的時候,咱們不讓NestedScrollView處理,而是交給子View處理,即ViewPager

public class JudgeNestedScrollView extends NestedScrollView {
    private boolean isNeedScroll = true;
    private float xDistance, yDistance, xLast, yLast;
    ...省略構造方法
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                xDistance = yDistance = 0f;
                xLast = ev.getX();
                yLast = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                final float curX = ev.getX();
                final float curY = ev.getY();
                xDistance += Math.abs(curX - xLast);
                yDistance += Math.abs(curY - yLast);
                xLast = curX;
                yLast = curY;
                if (xDistance > yDistance) {
                    return false;
                }
                return isNeedScroll;

        }
        return super.onInterceptTouchEvent(ev);
    }

    /*
    改方法用來處理NestedScrollView是否攔截滑動事件
     */
    public void setNeedScroll(boolean isNeedScroll) {
        this.isNeedScroll = isNeedScroll;
    }
}

至此,完美的解決了全部的問題,當時有些細節這裏並無話費太多的時間去處理,若有任何問題,歡迎各位大佬進行指正

源碼:https://github.com/SiberiaDante/MultiScrollDemo
相關文章
相關標籤/搜索