我的编程空间,编程开发者的网络收藏夹
学习永远不晚

NestScrollView嵌套RecyclerView实现淘宝首页滑动效果

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

NestScrollView嵌套RecyclerView实现淘宝首页滑动效果

一.概述

本文主要实现淘宝首页嵌套滑动,中间tab吸顶效果,以及介绍NestScrollView嵌套RecyclerView处理滑动冲突的方法,淘宝首页的效果图如下:

二.开搞

首先我们通过一张图来分析下页面的布局结构:

先把最基础的页面搭出来,禁用Recycler滑动只需要重写onInterceptTouchEvent、onTouchEvent返回值都设为false即可:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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"
    tools:context=".activiy.ViewPagerActivity"
    android:background="#f2f2f2">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <com.aykj.nestscrolldemo.widget.NoScrollRecyclerView
            android:id="@+id/top_recycler_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <View
                android:layout_width="match_parent"
                android:layout_height="1px"
                android:background="#e0e0e0"/>

            <com.google.android.material.tabs.TabLayout
                android:id="@+id/tab_view"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />

            <View
                android:layout_width="match_parent"
                android:layout_height="1px"
                android:background="#e0e0e0"/>

            <androidx.viewpager.widget.ViewPager
                android:id="@+id/view_pager"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>

        </LinearLayout>

    </LinearLayout>

</ScrollView>

public class ViewPagerActivity extends AppCompatActivity {

    private List<String> topDatas = new ArrayList<>();
    private List<String> tabTitles = new ArrayList<>();
    ActivityViewPagerBinding viewBinding;
    private RecyclerAdapter topAdapter;
    private DividerItemDecoration divider;
    private TabFragmentAdapter pagerAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewBinding = ActivityViewPagerBinding.inflate(LayoutInflater.from(this));
        setContentView(viewBinding.getRoot());

        initDatas();
        initView();
    }

    private void initDatas() {
        topDatas.clear();
        for(int i=0; i<5; i++) {
            topDatas.add("top item " + (i + 1));
        }

        tabTitles.clear();
        tabTitles.add("tab1");
        tabTitles.add("tab2");
        tabTitles.add("tab3");
    }

    private void initView() {
        //init topRecycler
        divider = new DividerItemDecoration(this, LinearLayout.VERTICAL);
        divider.setDrawable(new ColorDrawable(Color.parseColor("#ffe0e0e0")));
        viewBinding.topRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        viewBinding.topRecyclerView.addItemDecoration(divider);
        topAdapter = new RecyclerAdapter(this, topDatas);
        viewBinding.topRecyclerView.setAdapter(topAdapter);

        //initTabs with ViewPager
        pagerAdapter = new TabFragmentAdapter(getSupportFragmentManager(), tabTitles);
        viewBinding.viewPager.setAdapter(pagerAdapter);
        viewBinding.tabView.setupWithViewPager(viewBinding.viewPager);
        viewBinding.tabView.setTabMode(TabLayout.MODE_FIXED);
    }
}

可以看到ViewPager没有正常显示出来,这个时候可以重写ViewPager的onMeasure,重新测量ViewPager的宽高。也可以换用ViewPager2


public class CustomViewPager extends ViewPager {
  	...
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //重写ViewPager的onMeasure
        int width = 0;
        int height = 0;
        for(int i=0; i<getChildCount(); i++) {
            View childView = getChildAt(0);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
            width = Math.max(width, childView.getMeasuredWidth());
            height = Math.max(height, childView.getMeasuredHeight());
        }

        height += getPaddingTop() + getPaddingBottom();
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

从上面的效果图可以看到,ViewPager能正常显示出来了,但是在RecyclerView上滑动的时候发现,RecyclerView滑动完了之后,ScrollView才会滑动,并且ScrollView只滑动了一小段距离,这是因为首先ScrollView是不支持嵌套滑动的

ScrollView内部的第一个子View中所有子View的高度 = 顶部的RecyclerView高度 + TabLayout高度 + 底部RecyclerView中所有可见Item的高度

这个高度只比ScrollView的高度大一点点导致的。为了实现嵌套滑动需要使用NestedScrollView,接下来把ScrollView替换成NestedScrollView:

整个页面可以滑完,看起来就像是两个Scroll被合并成一个了,如果单单只是实现上面的界面效果,我们完全可以使用一个RecyclerView即可,但是Tab没有吸顶,这是因为:

ScrollView内部的第一个子View中所有子View的高度 = 顶部的RecyclerView高度 + TabLayout高度 + 底部RecyclerView中所有Item的高度

要实现Tab吸顶,只需要重写NestedScrollView的onMeasue方法,将TabLayout的高度和ViewPager的高度之和设置为NestedScrollView的高度:


public class StickyScrollLayout extends NestedScrollView {
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        int count = getChildCount();
        if(count == 1) {
            View firstChild = getChildAt(0);
            if(firstChild != null && firstChild instanceof ViewGroup) {
                int childCount = ((ViewGroup) firstChild).getChildCount();
                if(childCount > 1) {
                    topView = ((ViewGroup) firstChild).getChildAt(0);
                    contentView = ((ViewGroup) firstChild).getChildAt(1);
                }
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if(contentView != null) {
            ViewGroup.LayoutParams contentLayoutParams = contentView.getLayoutParams();
            contentLayoutParams.height = getMeasuredHeight();
            contentView.setLayoutParams(contentLayoutParams);
        }
    }
}

此时TabLayout可以吸顶了

三.处理嵌套滑动

从上图中可以看出,当我们在RecyclerView上向上滑动时,需要等RecyclerView滑动完,外部的NestedScrollView才开始滑动,而我们希望NestedScrollView中顶部的RecyclerView滑完之后,底部的RecyclerView才开始滑动,这是为什么呢?

查看NestedScrollView和RecyclerView的源码,可以知道NestedScrollView和RecyclerView分别实现了NestedScrollingParent3,NestedScrollingChild3接口,分别用来表示嵌套滑动的父View、嵌套滑动的子View,当我们的手指在RecyclerView上滑动时,滑动事件会从上往下分发至RecyclerView的onTouchEvent中,RecyclerView会依次响应ACTION_DOWN、ACTION_MOVE、ACTION_UP

RecyclerView在处理ACTION_DOWN时的关键代码如下:


public boolean onTouchEvent(MotionEvent e) {
  switch (action) {
    case MotionEvent.ACTION_DOWN: {
      if (canScrollHorizontally) {
        nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
      }
      if (canScrollVertically) {
        nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
      }
      startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
    } break;
  }
  return true;
}

当手指按下屏幕时会调用其作为NestedScrollingChild的实现方法startNestedScroll,在startNestedScroll的具体实现中,会一级一级的往上查找是否有NestedScrollingParent,如果有,会调用NestedScrollingParent的onStartNestedScroll方法通知它我即将要开始滑动了,然后NestedScrollingParent会调用onNestedScrollAccepted继续传递给上层的NestedScrollingParent,此处的NestedScrollingParent整好由NestedScrollView来充当,而NestedScrollView的上层已经找不到NestedScrollingParent了,时间传给NestedScrollView之后就中断了。

紧接着处理一系列的ACTION_MOVE:


public boolean onTouchEvent(MotionEvent e) {
  switch (action) {
    case MotionEvent.ACTION_MOVE: {
      if (dispatchNestedPreScroll(
        canScrollHorizontally ? dx : 0,
        canScrollVertically ? dy : 0,
        mReusableIntPair, mScrollOffset, TYPE_TOUCH
      )) {
        dx -= mReusableIntPair[0];
        dy -= mReusableIntPair[1];
        // Updated the nested offsets
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += mScrollOffset[1];
        // Scroll has initiated, prevent parents from intercepting
        getParent().requestDisallowInterceptTouchEvent(true);
      }
      
      if (scrollByInternal(
        canScrollHorizontally ? dx : 0,
        canScrollVertically ? dy : 0,
        e)) {
        getParent().requestDisallowInterceptTouchEvent(true);
      }
    } break;
  }
  return true;
}

RecyclerView接收到ACTION_MOVE后,首先会调用其作为NestedScrollingChild的实现方法dispatchNestedPreScroll,在dispatchNestedPreScroll的具体实现中,会一级一级的往上查找是否有NestedScrollingParent,如果有,会调用NestedScrollingParent的dispatchNestedPreScroll,紧接着调用NestedScrollView的onNestedPreScroll,来告诉NestedScrollView我即将要滑动 xxx 距离,你需不需要滑动,在NestedScrollView的onNestedPreScroll方法中并不会去响应滑动,又会把自己作为一个NestedScrollingChild,把事件继续往上传递,而在NestedScrollView的上层已经没有可以处理嵌套滑动的NestedScrollingParent了


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

具体的事件传递流程如下图:

因此我们可以重写NestedScrollView的onNestedPreScroll方法来使NestedScrollView滑动


public class StickyNestedScrollLayout extends NestedScrollView {
  
  	@Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        int count = getChildCount();
        if(count == 1) {
            View firstChild = getChildAt(0);
            if(firstChild != null && firstChild instanceof ViewGroup) {
                int childCount = ((ViewGroup) firstChild).getChildCount();
                if(childCount > 1) {
                    topView = ((ViewGroup) firstChild).getChildAt(0);
                    contentView = ((ViewGroup) firstChild).getChildAt(1);
                }
            }
        }
    }
  
    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        boolean topIsShow = getScrollY() >=0 && getScrollY() < topView.getHeight();
        if(topIsShow) {
            scrollBy(0, dy);
        } else {
          super.onNestedPreScroll(target, dx, dy, consumed, type);
        }
    }
}

此时NestedScrollView能滑动了,但是NestedScrollView滑动的同时,RecyclerView也会跟着滑动,这是为什么呢?

在RecyclerView的dispatchNestedPreScroll方法具体实现中,有这样一段代码


public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type) {
  if (isNestedScrollingEnabled()) {
      consumed[0] = 0;
      consumed[1] = 0;
      ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
    	//consumed[0]、consumed[1]的值仍为0
      return consumed[0] != 0 || consumed[1] != 0;//返回false
    }
  }
  return false;
}

再结合RecyclerView的ACTION_MOVE来看:


public boolean onTouchEvent(MotionEvent e) {
  switch (action) {
    case MotionEvent.ACTION_MOVE: {
      if (dispatchNestedPreScroll(
        canScrollHorizontally ? dx : 0,
        canScrollVertically ? dy : 0,
        mReusableIntPair, mScrollOffset, TYPE_TOUCH
      )) {
        //dispatchNestedPreScroll返回了false,此处的if语句不会执行,因此RecyclerView也会滑动
        dx -= mReusableIntPair[0];
        dy -= mReusableIntPair[1];
        // Updated the nested offsets
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += mScrollOffset[1];
        // Scroll has initiated, prevent parents from intercepting
        getParent().requestDisallowInterceptTouchEvent(true);
      }
      
      if (scrollByInternal(
        canScrollHorizontally ? dx : 0,
        canScrollVertically ? dy : 0,
        e)) {
        getParent().requestDisallowInterceptTouchEvent(true);
      }
    } break;
  }
  return true;
}

因此,我们,在NestedScrollView的onNestedPreScroll方法中,处理完滑动后,通过consumed告诉RecyclerView我滑动了多少,这样

RecyclerView会重新设置dx、dy的值,因此RecyclerView就不会跟着滑动了


public class StickyNestedScrollLayout extends NestedScrollView {
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        int count = getChildCount();
        if(count == 1) {
            View firstChild = getChildAt(0);
            if(firstChild != null && firstChild instanceof ViewGroup) {
                int childCount = ((ViewGroup) firstChild).getChildCount();
                if(childCount > 1) {
                    topView = ((ViewGroup) firstChild).getChildAt(0);
                    contentView = ((ViewGroup) firstChild).getChildAt(1);
                }
            }
        }
    }

    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        boolean topIsShow = getScrollY() >=0 && getScrollY() < topView.getHeight();
        if(topIsShow) {
            scrollBy(0, dy);
            //告诉RecyclerView,我滑动了多少距离
            consumed[1] = dy;
        } else {
            super.onNestedPreScroll(target, dx, dy, consumed, type);
        }
    }
}

四.实现惯性滑动

实现思路:

记录父控件惯性滑动的速度判断NestedScrollView是否滚动到底部,若滚动到底部,判断子控件是否需要继续滚动滚动将惯性滑动的速度转化成距离,计算子控件应滑的距离 = 惯性距离 - 父控件已滑动距离,并将子控件应滑的距离转化成速度交给子控件进行惯性滑动

1.记录父控件惯性滑动的速度


public void fling(int velocityY) {
  super.fling(velocityY);
  if (velocityY <= 0) {
  	mVelocityY = 0;
  } else {
  	mVelocityY = velocityY;
  }
}

2.判断NestedScrollView是否滚动到底部,若滚动到底部,判断子控件是否需要继续滚动


@Override
protected void onScrollChanged(int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
  super.onScrollChanged(scrollX, scrollY, oldScrollX, oldScrollY);
  
  //判断NestedScrollView是否滚动到底部,若滚动到底部,判断子控件是否需要继续滚动
  if (scrollY == getChildAt(0).getMeasuredHeight() - this.getMeasuredHeight()) {
    dispatchChildFling();
  }
  //累计自身滚动的距离
  mConsumedY += scrollY - oldScrollY;
}

3.将惯性滑动的速度转化成距离,计算子控件应滑的距离 = 惯性距离 - 父控件已滑动距离,并将子控件应滑的距离转化成速度交给子控件进行惯性滑动


private void dispatchChildFling() {
    if(mFlingHelper == null) {
      mFlingHelper = new FlingHelper(getContext());
    }
    if (mVelocityY != 0) {
        //将惯性滑动速度转化成距离
        double distance = mFlingHelper.getSplineFlingDistance(mVelocityY);
        //计算子控件应该滑动的距离 = 惯性滑动距离 - 已滑距离
        if (distance > mConsumedY) {
            RecyclerView recyclerView = getChildRecyclerView(mContentView);
            if (recyclerView != null) {
                //将剩余滑动距离转化成速度交给子控件进行惯性滑动
                int velocityY = mFlingHelper.getVelocityByDistance(distance - mConsumedY);
                recyclerView.fling(0, velocityY);
            }
        }
    }

    mConsumedY = 0;
    mVelocityY = 0;
}

//递归获取子控件RecyclerView
private RecyclerView getChildRecyclerView(ViewGroup viewGroup) {
  for (int i = 0; i < viewGroup.getChildCount(); i++) {
    View view = viewGroup.getChildAt(i);
    if (view instanceof RecyclerView && Objects.requireNonNull(((RecyclerView) view).getLayoutManager()).canScrollVertically()) {
      return (RecyclerView) view;
    } else if (viewGroup.getChildAt(i) instanceof ViewGroup) {
      RecyclerView childRecyclerView = getChildRecyclerView((ViewGroup) viewGroup.getChildAt(i));
      if (childRecyclerView != null && Objects.requireNonNull((childRecyclerView).getLayoutManager()).canScrollVertically()) {
        return childRecyclerView;
      }
    }
  }
  return null;
}

到此这篇关于NestScrollView嵌套RecyclerView实现淘宝首页滑动效果的文章就介绍到这了,更多相关NestScrollView嵌套RecyclerView内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

NestScrollView嵌套RecyclerView实现淘宝首页滑动效果

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Android实现微信首页左右滑动切换效果

大家看到微信首页切换效果有没有觉得很炫,滑动切换,点击底部bar瞬间切换,滑动切换渐变效果,线上效果图:之前也在博客上看到别人的实现,再次基础上,我做了些优化。首先说下实现原理,大神略过,o(╯□╰)o 页面上看到的三个页面是三个Fragm
2022-06-06

recycleview如何实现拼多多首页水平滑动效果

这篇文章给大家分享的是有关recycleview如何实现拼多多首页水平滑动效果的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。本文实例为大家分享了recycleview实现拼多多首页水平滑动效果的具体代码,供大家参
2023-06-15

Android进阶NestedScroll嵌套滑动机制实现吸顶效果详解

这篇文章主要为大家介绍了Android进阶NestedScroll嵌套滑动机制实现吸顶效果详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-29

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录