一.概述

本文主要实现淘宝首页嵌套滑动,中间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);
  /*
         * scrollY == 0 即还未滚动
         * scrollY == getChildAt(0).getMeasuredHeight() - v.getMeasuredHeight()即滚动到底部了
         */
  //判断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内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

NestScrollView嵌套RecyclerView实现淘宝首页滑动效果的更多相关文章

  1. android – AnimateLayoutChanges不适用于RecyclerView

    XMLJAVA解决方法过了一会儿我得到了解决方案.我创建了一个函数来设置recyclelerView高度的动画.JAVA}

  2. android – 在RecyclerView中更新ProgressBar

    我有一个RecyclerView.其中,项目具有标准布局–一个TextView和一个ProgressBar.项目在运行时添加到recyclerview.每当添加一个Item时,都会启动一个更新ProgressBar的AsyncTask.AsynTask保存了RecyclerViewAdapter对ProgressBar对象的引用.当回收器视图中的项目太多时,会发生此问题.我知道RecyclerVi

  3. android – WearableRecyclerView .scrollToPosition不适合穿?

    我有一组应该在列表中填充的项目,因为我使用了WearableRecyclerView.在某些情况下,我希望特定的项目成为焦点以供选择.我正在使用Wearablelinearlayoutmanager的scrollToPosition()方法,但监视列表不会滚动到所需的位置,但是在使用RecyclerView和scrollLooutManager的scrollToPosition()的手机上也是如此

  4. android – 在RecyclerView中更改单个drawable的颜色将更改所有drawable

    我只是试图根据一个值改变我的行内部drawable的颜色,但是不是一个drawable,适配器改变了所有这些颜色.这是我的适配器:知道怎么做?谢谢您的帮助!!!

  5. android – 我可以像这样创建一个nestedScroll布局吗?

    我认为它可以通过nestedScrollingChildnestedScrollingParent来实现.但我无法理解他们.谁可以帮助我!产品经理坚持设计.ScrollView包含LinearLayout,“TabLayout”和ViewPager.ViewPager包含2个包含RecyclerView的片段或仅包含2个RecyclerView.当ScrollView滚动到Bottom时,Recy

  6. android – 在NestedScrollView onBindViewHolder中的RecyclerView调用所有getItemCount大小

    当我将RecyclerView放入nestedScrollView时,然后onBindViewHolder调用所有行,比如说我有一个大小为30的列表,那么即使没有滚动,也会同时为所有30行调用onBindViewHolder我的xml是但如果我删除nestedScrollView它正常工作.解决方法高度问题引起的问题.1)编辑nestedScrollView&RecyclerView如下:2)确保

  7. android – RecyclerView滚动条在第一个项目后跳回到顶部

    我创建了一个非常基本的RecyclerView示例.布局:活动:现在,当我滚动到第一个项目之外时,滚动条会跳回到顶部然后继续正常.第二个问题是当我滚动到底部时,滚动条会提前停止.这是支持库中的错误还是我自己的错?解决方法如here所述,此错误已在新版本的支持库v21.0.2中得到修复.

  8. android – 将项目添加到recyclerview的顶部

    我的活动中有一个Recyclerview.当我下拉它将加载新项目回收视图.现在我需要实现pull来将概念刷新到我的recyclerview.我做到了.但是当我调用pull来刷新时,我正在获取新项目并添加到回收视图底部.我需要在循环视图的顶部添加新项目.如何将新加载的项目添加到回收站视图的顶部位置.解决方法我会坚持要在第0个位置添加项目,这个位置来自拉动刷新,如下所示,

  9. android – 如何在另一个recycleview适配器下的recycleview适配器?

    我有一个RecyclerView.它有一个自定义布局,在自定义布局内是另一个RecyclerView.当我通知回收站视图项目已被删除时,我的主回收站视图已更新,但我的自定义视图回收视图未收到通知.这是要删除的RecyclerView滑动的代码.在我的购物车适配器中我采取了另一个回收视图适应.任何想法如何通知自适应如果从循环视图中删除任何数据.???我的onBindViewHolder类家长回收视图儿童recycleview解决方法首先,为什么你甚至需要另一个回收者视图?

  10. android – Animate Recycler在列数更改时查看网格

    我正在使用带有GridLayoutManager的RecyclerView.用户可以在2到4之间切换跨度计数,这将导致动画将每个单元格的内置翻译动画运行到其新位置.我到目前为止使用的代码是:这对我来说一直很好,但现在我需要为每个跨度计数设置不同的布局.为了支持这一点,我在RecyclerView中有2种视图类型,并且由于在移动到新的跨度计数时视图类型已更改,因此RecyclerView无法看到它是

随机推荐

  1. Flutter 网络请求框架封装详解

    这篇文章主要介绍了Flutter 网络请求框架封装详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Android单选按钮RadioButton的使用详解

    今天小编就为大家分享一篇关于Android单选按钮RadioButton的使用详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧

  3. 解决android studio 打包发现generate signed apk 消失不见问题

    这篇文章主要介绍了解决android studio 打包发现generate signed apk 消失不见问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

  4. Android 实现自定义圆形listview功能的实例代码

    这篇文章主要介绍了Android 实现自定义圆形listview功能的实例代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  5. 详解Android studio 动态fragment的用法

    这篇文章主要介绍了Android studio 动态fragment的用法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  6. Android用RecyclerView实现图标拖拽排序以及增删管理

    这篇文章主要介绍了Android用RecyclerView实现图标拖拽排序以及增删管理的方法,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下

  7. Android notifyDataSetChanged() 动态更新ListView案例详解

    这篇文章主要介绍了Android notifyDataSetChanged() 动态更新ListView案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下

  8. Android自定义View实现弹幕效果

    这篇文章主要为大家详细介绍了Android自定义View实现弹幕效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  9. Android自定义View实现跟随手指移动

    这篇文章主要为大家详细介绍了Android自定义View实现跟随手指移动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Android实现多点触摸操作

    这篇文章主要介绍了Android实现多点触摸操作,实现图片的放大、缩小和旋转等处理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部