前言:通过NestedScrollView嵌套RecyclerView可以轻松实现嵌套滑动,但我们会发现RecyclerView懒加载失效了。
- <?xml version="1.0" encoding="utf-8"?>
- <androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- xmlns:app="http://schemas.android.com/apk/res-auto">
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:descendantFocusability="blocksDescendants">
- <TextView
- android:id="@+id/tv_title"
- android:layout_width="match_parent"
- android:layout_height="100dp"
- android:background="@color/colorAccent"
- android:gravity="center"
- android:text="这是头部" />
- <androidx.recyclerview.widget.RecyclerView
- android:id="@+id/rv_content"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
- android:orientation="vertical"/>
- </LinearLayout>
- </androidx.core.widget.NestedScrollView>
原因:NestedScrollView高度虽然为屏幕高度,但其对子布局会进行wrap_content方式测量,这里LinearLayout即使是match_parent也不是屏幕高度,因此传递给其子RecyclerView也wrap_content方式得到的高度,导致RecyclerView一次加载出全部数据。(NestedScrollView嵌套滑动原理是还是平面式的滑动,RecyclerView由于加载了全部数据,本身不再滑动而是随着将LinearLayout移动实现的)
方案:要使RecyclerView懒加载则不能使其高度包裹所有item,需要指定最大高度(本例中最大高度为屏幕高度,先是NestedScrollView响应滑动当LinearLayout至最底部时,此时RecyclerView正好显示全,接下来再滑动就由RecyclerView来完成,这才是真正的嵌套滑动)
1、自定义RecyclerView使其支持设置最大高度
- public class MaxHeightRecyclerView extends RecyclerView {
- private int mMaxHeight;
-
- public MaxHeightRecyclerView(@NonNull Context context) {
- super(context);
- }
-
- public MaxHeightRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onMeasure(int widthSpec, int heightSpec) {
- if (mMaxHeight != 0){
- super.onMeasure(widthSpec, View.MeasureSpec.makeMeasureSpec(mMaxHeight, View.MeasureSpec.AT_MOST));
- }else {
- super.onMeasure(widthSpec, heightSpec);
- }
- }
-
- public void setMaxHeight(int maxHeight) {
- this.mMaxHeight = maxHeight;
- }
- }
2、自定义NestedScrollView,使其给RecyclerView设置最大高度
- public class LazyNestedScrollView extends androidx.core.widget.NestedScrollView{
- private int mRecyclerViewId;
- private MaxHeightRecyclerView mRecyclerView;
-
- public LazyNestedScrollView(@NonNull Context context) {
- super(context);
- }
-
- public LazyNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LazyNestedScrollView);
- //获取嵌套RecyclerView控件的id
- mRecyclerViewId = typedArray.getResourceId(R.styleable.LazyNestedScrollView_recyclerview_id, 0);
- typedArray.recycle();
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- //在super前给RecyclerView设置最大值,然后通过super进行测量即可
- int height = MeasureSpec.getSize(heightMeasureSpec);
- if (mRecyclerView != null){
- mRecyclerView.setMaxHeight(height);
- }else {
- findViewById(this);
- if (mRecyclerView != null){
- mRecyclerView.setMaxHeight(height);
- }
- }
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
- //根据id递归查询RecyclerView
- private void findViewById(View view){
- if (view instanceof ViewGroup && !(view instanceof RecyclerView)){
- ViewGroup viewGroup = (ViewGroup) view;
- int childCount = viewGroup.getChildCount();
- for (int i = 0; i < childCount; i++){
- View childView = viewGroup.getChildAt(i);
- findViewById(childView);
- }
- }else {
- if (view.getId() == mRecyclerViewId && view instanceof MaxHeightRecyclerView){
- mRecyclerView = (MaxHeightRecyclerView) view;
- }
- }
- }
-
- @Override
- public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
- if (mRecyclerView != null){
- View child = getChildAt(0);
- MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
- //获取自己能够滑动的距离
- int topHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin - getMeasuredHeight();
- int scrollY = getScrollY();
- boolean topIsShow = scrollY >=0 && scrollY < topHeight;
- if(topIsShow) {
- //由自己响应滑动
- int remainScrollY= topHeight - scrollY;
- int selfScrollY = remainScrollY > dy ? dy : remainScrollY;
- scrollBy(0, selfScrollY);
- //告诉RecyclerView,自己滑动了多少距离
- consumed[1] = selfScrollY;
- } else {
- super.onNestedPreScroll(target, dx, dy, consumed, type);
- }
- }else {
- super.onNestedPreScroll(target, dx, dy, consumed, type);
- }
- }
- }
注:
(1)RecyclerView的setNestedScrollingEnabled()应为true
(2)嵌套滑动是通过NestedScrollingParent3和NestedScrollingChild3来实现的,这里RecyclerView已经继承NestedScrollingChild3而NestedScrollView也已继承NestedScrollingParent3,RecyclerView先接收滑动事件然后先询问NestedScrollView来滑动(即onNestedPreScroll方法)然后将其滑动距离告诉RecyclerView,RecyclerView再对剩下的距离进行滑动
3、惯性滑动(NestedScrollView滑动到底部,将其滑动速度转化成惯性距离,计算子控件应滑距离=父惯性距离-父已滑距离,将子控件应滑距离转化成速度交给子控件进行惯性滑动)
(1)记录NestedScrollView惯性速度
- @Override
- public void fling(int velocityY) {
- super.fling(velocityY);
- mVelocityY = velocityY;
- }
(2)将剩余的惯性速度传递给RecyclerView
- @Override
- protected void onScrollChanged(int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
- super.onScrollChanged(scrollX, scrollY, oldScrollX, oldScrollY);
- if (mRecyclerView != null){
- View child = getChildAt(0);
- MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
- //判断是否滑动到底部
- if (scrollY == child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin - getMeasuredHeight()){
- if (mVelocityY > 0){
- //将惯性速度转化为滑动距离
- double distance = FlingHelper.getInstance(getContext()).getSplineFlingDistance(mVelocityY);
- if (distance > scrollY){
- //将剩余滑动距离转化为惯性速度
- int velocityY = FlingHelper.getInstance(getContext()).getVelocityByDistance(distance - scrollY);
- //将剩余惯性速度传递给RecyclerView
- mRecyclerView.fling(0, velocityY);
- //重置惯性速度
- mVelocityY = 0;
- }
- }
- }
- }
- }
(3)惯性速度和滑动距离转化工具类
- public class FlingHelper {
- private static FlingHelper mFlingHelper;
- private static final double DECELERATION_RATE = Math.log(0.78) / Math.log(0.9);
- private float mFlingFriction = ViewConfiguration.getScrollFriction();
- private float mPhysicalCoeff;
-
- private FlingHelper(Context context){
- mPhysicalCoeff = context.getResources().getDisplayMetrics().density * 160.0f * 386.0878f * 0.84f;
- }
-
- public static FlingHelper getInstance(Context context){
- if (mFlingHelper == null){
- mFlingHelper = new FlingHelper(context);
- }
- return mFlingHelper;
- }
-
- public double getSplineFlingDistance(int i){
- return Math.exp(getSplineDeceleration(i) * (DECELERATION_RATE / (DECELERATION_RATE - 1.0))) * (mFlingFriction * mPhysicalCoeff);
- }
-
- private double getSplineDeceleration(int i){
- return Math.log(0.35f * Math.abs(i) / (mFlingFriction * mPhysicalCoeff));
- }
-
- public int getVelocityByDistance(double d){
- return (int) Math.abs((Math.exp(getSplineDecelerationByDistance(d)) * mFlingFriction * mPhysicalCoeff / 0.3499999940395355));
- }
-
- private double getSplineDecelerationByDistance(double d){
- return (DECELERATION_RATE - 1.0) * Math.log(d / (mFlingFriction * mPhysicalCoeff)) / DECELERATION_RATE;
- }
- }