Android开发——自定义view之京东淘宝首页二级联动

1、布局

Android开发——自定义view之京东淘宝首页二级联动

 

2、解决问题

问题1:嵌套滑动冲突

我们知道,通过上面的简单布局,在执行代码的时候,会有滑动冲突产生,当我们滑动recyclerView的时候,headerView不会跟着滑动。

解决办法:嵌套滑动是需要两个角色的,一个是父亲(ScrollView),一个是孩子(recyclerView),我们的recyclerView实现了NestedScrollingChild,而ScrollView并没有实现NestedScrollingParent,因此ScrollView不能作为父亲,而我们的界面用到的是嵌套滑动,嵌套滑动又必须要有父亲,因此我们将ScrollView改成NestedScrollView即可,NestedScrollView实现了NestedScrollingParent接口。此时事件已经被传递,但是没有吸顶。

原因:事件分发原理

 

问题2:TabLayout吸顶效果

处理方式:

  • 固定位置
  • 事件拦截
  • 备胎(弄两个tabLayout,进行高度计算隐藏和显示)

我们这里用固定位置来做。思路如下:

先看图

Android开发——自定义view之京东淘宝首页二级联动

我们将NestedScrollView的高度固定,高度等于headerview+屏幕的高度,而屏幕的高度=tablayout的高度+viewpager的高度

当NestedScrollView滑动到底部的时候,也就是headerview全部隐藏的时候,此时tabLayout在屏幕最顶部,NestedScrollView已经滑动到了底部,我们继续滑动屏幕的时候,实际上是在滑动recyclerView,这样tabLayout就会有一个吸顶的效果。

<?xml version="1.0" encoding="utf-8"?>
<layout>

    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:id="@+id/swipe_refresh_layout"
        android:layout_height="match_parent">

        <com.nestedscroll.e_perfect_nestedscroll.NestedScrollLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

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

                <com.common.views.xxrecyclerview.FixedDataScrollDisabledRecyclerView
                    android:id="@+id/combo_top_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">

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

                    <androidx.viewpager2.widget.ViewPager2
                        android:id="@+id/viewpager_view"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent" />
                </LinearLayout>
            </LinearLayout>
        </com.nestedscroll.e_perfect_nestedscroll.NestedScrollLayout>
    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

</layout>

 

我们需要自定义一个NestedScrollView,然后重写里面的两个方法

  Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        contentView = (ViewGroup) ((ViewGroup) getChildAt(0)).getChildAt(1);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 调整contentView的高度为父容器高度,使之填充布局,避免父容器滚动后出现空白
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        ViewGroup.LayoutParams lp = contentView.getLayoutParams();
        lp.height = getMeasuredHeight();
        contentView.setLayoutParams(lp);
    }

当加载完毕之后,获取到NestedScrollView下第0个元素的第1个元素,也就是这段

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

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

                    <androidx.viewpager2.widget.ViewPager2
                        android:id="@+id/viewpager_view"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent" />
                </LinearLayout>    

然后通过onMeasure计算高度。

此时吸顶效果完成。吸顶其实是假象,实际上是NestedScrollView已经到头了,继续滑动屏幕的话实际上滑动的是recyclerView。

 

问题3:NestedScrollView和recyclerView的滑动没有连接起来

还是回到前面,嵌套滑动是由父亲和孩子两个元素来完成的,所以我们这里需要用子view来主动触发,也就是recyclerView主动触发,NestedScrollView跟随滑动。

因此当NestedScrollView还未处于底部的时候,也就是说NestedScrollView还可以继续滑动的时候,我们需要对滑动进行拦截,告诉recyclerView,父亲滑动就行了,你不用动。

所以我们重写拦截方法onNestedPreScroll,如果不拦截,那么会交给NestedScrollView的父亲去执行,并不是我们想要的

  @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        topView = ((ViewGroup) getChildAt(0)).getChildAt(0);
        contentView = (ViewGroup) ((ViewGroup) getChildAt(0)).getChildAt(1);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 调整contentView的高度为父容器高度,使之填充布局,避免父容器滚动后出现空白
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        ViewGroup.LayoutParams lp = contentView.getLayoutParams();
        lp.height = getMeasuredHeight();
        contentView.setLayoutParams(lp);
    }

    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        Log.i("NestedScrollLayout", getScrollY()+"::onNestedPreScroll::"+topView.getMeasuredHeight());
        // 向上滑动。若当前topview可见,需要将topview滑动至不可见
        boolean hideTop = dy > 0 && getScrollY() < topView.getMeasuredHeight();
        if (hideTop) {
            scrollBy(0, dy);
            consumed[1] = dy;
        }
    }

 

问题4:惯性滑动

思路

  • 记下速度
  • 转成距离
  • 父view滑动了多少距离
  • 计算子view该滑动的距离(根据速度转换后的距离-父view滑动了的额距离=子view该滑的距离)
  • 子view该滑的距离转成速度(因为recyclerView只有一个fling方法,该方法接收的是一个速度值,因此我们要把剩余的距离转成速度传进去,而不是直接传距离)

下面直接贴代码,代码有注释,配合上面的思路应该就清楚了。

public class NestedScrollLayout extends NestedScrollView {
    private View topView;
    private ViewGroup contentView;
    private static final String TAG = "NestedScrollLayout";

    public NestedScrollLayout(Context context) {
        this(context, null);
        init();
    }

    public NestedScrollLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
        init();
    }

    public NestedScrollLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
        init();
    }

    public NestedScrollLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private FlingHelper mFlingHelper;

    int totalDy = 0;
    /**
     * 用于判断RecyclerView是否在fling
     */
    boolean isStartFling = false;
    /**
     * 记录当前滑动的y轴加速度
     */
    private int velocityY = 0;

    private void init() {
        mFlingHelper = new FlingHelper(getContext());
        setOnScrollChangeListener(new View.OnScrollChangeListener() {
            @Override
            public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                if (isStartFling) {
                    totalDy = 0;
                    isStartFling = false;
                }
                if (scrollY == 0) {
                    Log.i(TAG, "TOP SCROLL");
                   // refreshLayout.setEnabled(true);
                }
                if (scrollY == (getChildAt(0).getMeasuredHeight() - v.getMeasuredHeight())) {
                    Log.i(TAG, "BOTTOM SCROLL");
                    dispatchChildFling();
                }
                //在RecyclerView fling情况下,记录当前RecyclerView在y轴的偏移
                totalDy += scrollY - oldScrollY;
            }
        });
    }

  //获取剩余距离 private void dispatchChildFling() { if (velocityY != 0) { Double splineFlingDistance = mFlingHelper.getSplineFlingDistance(velocityY); if (splineFlingDistance > totalDy) { childFling(mFlingHelper.getVelocityByDistance(splineFlingDistance - Double.valueOf(totalDy))); } } totalDy = 0; velocityY = 0; }
  //计算子view滑动的距离 private void childFling(int velY) { RecyclerView childRecyclerView = getChildRecyclerView(contentView); if (childRecyclerView != null) {
        //RecyclerView只接收速度参数 childRecyclerView.fling(0, velY); } }
  //通过速度计算距离 @Override public void fling(int velocityY) { super.fling(velocityY); if (velocityY <= 0) { this.velocityY = 0; } else { isStartFling = true; this.velocityY = velocityY; } } @Override protected void onFinishInflate() { super.onFinishInflate(); topView = ((ViewGroup) getChildAt(0)).getChildAt(0); contentView = (ViewGroup) ((ViewGroup) getChildAt(0)).getChildAt(1); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 调整contentView的高度为父容器高度,使之填充布局,避免父容器滚动后出现空白 super.onMeasure(widthMeasureSpec, heightMeasureSpec); ViewGroup.LayoutParams lp = contentView.getLayoutParams(); lp.height = getMeasuredHeight(); contentView.setLayoutParams(lp); } @Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { Log.i("NestedScrollLayout", getScrollY()+"::onNestedPreScroll::"+topView.getMeasuredHeight()); // 向上滑动。若当前topview可见,需要将topview滑动至不可见 boolean hideTop = dy > 0 && getScrollY() < topView.getMeasuredHeight(); if (hideTop) { scrollBy(0, dy); consumed[1] = dy; } } private RecyclerView getChildRecyclerView(ViewGroup viewGroup) { for (int i = 0; i < viewGroup.getChildCount(); i++) { View view = viewGroup.getChildAt(i); if (view instanceof RecyclerView && view.getClass() == NestedLogRecyclerView.class) { return (RecyclerView) viewGroup.getChildAt(i); } else if (viewGroup.getChildAt(i) instanceof ViewGroup) { ViewGroup childRecyclerView = getChildRecyclerView((ViewGroup) viewGroup.getChildAt(i)); if (childRecyclerView instanceof RecyclerView) { return (RecyclerView) childRecyclerView; } } continue; } return null; } }

 

计算距离的工具类,google自己写的,我直接抄的源码,我专业不是物理,所以这玩意直接抄的。。。。。。

public class FlingHelper {
    private static float DECELERATION_RATE = ((float) (Math.log(0.78d) / Math.log(0.9d)));
    private static float mFlingFriction = ViewConfiguration.getScrollFriction();
    private static float mPhysicalCoeff;

    public FlingHelper(Context context) {
        mPhysicalCoeff = context.getResources().getDisplayMetrics().density * 160.0f * 386.0878f * 0.84f;
    }

    private double getSplineDeceleration(int i) {
        return Math.log((double) ((0.35f * ((float) Math.abs(i))) / (mFlingFriction * mPhysicalCoeff)));
    }

    private double getSplineDecelerationByDistance(double d) {
        return ((((double) DECELERATION_RATE) - 1.0d) * Math.log(d / ((double) (mFlingFriction * mPhysicalCoeff)))) / ((double) DECELERATION_RATE);
    }

    public double getSplineFlingDistance(int i) {
        return Math.exp(getSplineDeceleration(i) * (((double) DECELERATION_RATE) / (((double) DECELERATION_RATE) - 1.0d))) * ((double) (mFlingFriction * mPhysicalCoeff));
    }

    public int getVelocityByDistance(double d) {
        return Math.abs((int) (((Math.exp(getSplineDecelerationByDistance(d)) * ((double) mFlingFriction)) * ((double) mPhysicalCoeff)) / 0.3499999940395355d));
    }
}

至此,功能完成!

上一篇:Android ScrollView嵌套RecyclerView


下一篇:NestedScrollView嵌套RecyclerView滑动无惯性,有点停顿的解决办法