使用NestedScrollView+ViewPager+RecyclerView+SmartRefreshLayout打造酷炫下拉视差效果并解决各种滑动冲突

 
如果你还在为处理滑动冲突而发愁,那么你需要静下心来看看这边文章,如果你能彻底理解这篇文章中使用的技术,那么,一切滑动冲突的问题解决起来就轻而易举了:
先扔一个最终实现的效果图


先分析下效果图中实现的功能点
  • 顶部下拉时背景图形成视差效果
  • 上拉时标题栏透明切换显示
  • 底部实现TabLayout+ViewPager+Fragment+RecyclerView
  • NestedScrollView+ViewPager的滑动冲突解决
  • NestedScrollView+RecyclerView滑动冲突的解决

复杂在哪里?整个布局中使用了SmartRefreshLayout,NestedScrollView,ViewPager,RecyclerView,每一个都有滑动事件,我们平时只是使用ScrollView+RecyclerView都会有滑动冲突,更何况这里有四个会引起冲突的控件一起使用!

接下来,我们一步一步实现这个效果
1、布局设计分析
1
2
3
4
5
6
7
-FrameLayout(最外层)
     -ImageView(头部背景图)
         -SmartRefreshLayout(头部刷新控件)
             -JudgeNestedScrollView(自定义的NestedScrollView)
                 ...省略中间巴拉巴拉布局
                 -Tablayout
                     -ViewPager

2、功能点实现说明
2.1、下拉时视差效果的实现
最外层为FrameLayout,ImageView高度设置超过屏幕顶部,借助SmartRefreshLayout控件在下拉和松开时头部背景图做平移处理,背景图片做了高斯模糊处理

布局代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<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.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.3、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.4、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.5、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.5、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


SiberiaDante的博客:http://www.cnblogs.com/shen-hua/
SiberiaDante的github地址:https://github.com/SiberiaDante
好的,下面是一个使用TabLayout+ViewPager+Fragmet+RecyclerView结合的小demo的示例代码。 1. 首先,在xml文件中添加TabLayoutViewPager组件。 ```xml <android.support.design.widget.TabLayout android:id="@+id/tab_layout" android:layout_width="match_parent" android:layout_height="wrap_content" /> <android.support.v4.view.ViewPager android:id="@+id/view_pager" android:layout_width="match_parent" android:layout_height="match_parent" /> ``` 2. 创建Fragment类,用于展示RecyclerView数据。 ```java public class SimpleFragment extends Fragment { private RecyclerView recyclerView; private SimpleAdapter adapter; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_simple, container, false); recyclerView = view.findViewById(R.id.recycler_view); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); adapter = new SimpleAdapter(); recyclerView.setAdapter(adapter); return view; } } ``` 3. 创建FragmentPagerAdapter类,用于管理Fragment。 ```java public class SimplePagerAdapter extends FragmentPagerAdapter { private String[] titles; public SimplePagerAdapter(FragmentManager fm, String[] titles) { super(fm); this.titles = titles; } @Override public Fragment getItem(int position) { return new SimpleFragment(); } @Override public int getCount() { return titles.length; } @Nullable @Override public CharSequence getPageTitle(int position) { return titles[position]; } } ``` 4. 创建RecyclerView Adapter类,用于展示数据。 ```java public class SimpleAdapter extends RecyclerView.Adapter<SimpleViewHolder> { private List<String> data; public SimpleAdapter() { data = new ArrayList<>(); for (int i = 0; i < 20; i++) { data.add("Item " + i); } } @NonNull @Override public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_simple, parent, false); return new SimpleViewHolder(view); } @Override public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position) { holder.textView.setText(data.get(position)); } @Override public int getItemCount() { return data.size(); } } ``` 5. 创建RecyclerView ViewHolder类,用于展示每个item。 ```java public class SimpleViewHolder extends RecyclerView.ViewHolder { public TextView textView; public SimpleViewHolder(View itemView) { super(itemView); textView = itemView.findViewById(R.id.text_view); } } ``` 6. 最后,在Activity中进行初始化和设置。 ```java public class MainActivity extends AppCompatActivity { private TabLayout tabLayout; private ViewPager viewPager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tabLayout = findViewById(R.id.tab_layout); viewPager = findViewById(R.id.view_pager); String[] titles = {"Tab 1", "Tab 2", "Tab 3"}; SimplePagerAdapter adapter = new SimplePagerAdapter(getSupportFragmentManager(), titles); viewPager.setAdapter(adapter); tabLayout.setupWithViewPager(viewPager); } } ``` 这样就完成了TabLayout+ViewPager+Fragmet+RecyclerView结合的小demo。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值