自定义View事件篇进阶篇(二)-自定义NestedScrolling实战

前言

在上篇文章自定义View事件之进阶篇(一)-NestedScrolling(嵌套滑动)机制中,咱们分析了谷歌对NestedScrolling机制的设计,了解的不一样接口的使用场景。如今就让咱们一块儿结合一个实际的使用例子,来巩固以前学习的知识点吧。java

效果展现

先看咱们须要仿写的实际效果吧。以下图所示:android

Demo展现

上文展现的demo,在项目NestedScrollingDemo有具体实现。git

在上述Demo中,整个界面分为标题栏、展现图片、TabLayout、ViewPage。其中ViewPager中拥有多个Fragment。其中每一个fragment中都对应着一个RecyclerView。整个Demo的实现效果以下所示:github

  • 当产生向上的手势滑动与fling时,若是展现图片没被父控件遮挡,那么父控件先拦截事件并滑动。当图片彻底被遮挡时,子控件(RecyclerView)再接着处理。
  • 当产生向下的手势滑动与fling时,若是展现图片没彻底显示,那么父控件先拦截事件并滑动。当图片彻底显示时,子控件(RecyclerView)再接着处理。
  • 标题栏中的回退键,会随着父控件的滑动,有一个从白色到黑色的渐变效果。
  • 标题栏中的透明度,会随着父控件的滑动,透明度从0到1的变化效果。

如今就让咱们一块儿来实现该效果吧!!markdown

接口使用分析

要实现嵌套滑动,咱们首先想到的是要实现NestedScrollingChild与NestedScrollingParent接口,可是咱们这里的Demo须要父控件处理部分fling,因此咱们这里要使用NestedScrollingChild2与NestedScrollingParent2接口。又由于RecyclerView、NestedScrollView等滚动的View,在谷歌中都实现了NestedScrollingChild2接口,因此咱们不用单独来处理子控件对手势滑动与fling的分发,咱们只用关心父控件的处理就好了。app

又由于总体布局为竖直方向,因此这里咱们采用了继承LinearLayout并实现NestedScrollingParent2接口的方式。同时为了兼容低版本,咱们也要使用NestedScrollingParentHelper这个帮助类。具体类实现类StickyNavLayout代码以下所示;框架

public class StickyNavLayout extends LinearLayout implements NestedScrollingParent2 {

    private NestedScrollingParentHelper mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);

    public StickyNavLayout(Context context) {
        this(context, null);
    }

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

    public StickyNavLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setOrientation(VERTICAL);//设置布局方向为竖直方向。
    }

    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
         return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
    }

    @Override
    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type);
    }

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


    @Override
    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {}

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }

    @Override
    public void onStopNestedScroll(@NonNull View target, int type) {
        mNestedScrollingParentHelper.onStopNestedScroll(target, type);
    }

    @Override
    public int getNestedScrollAxes() {
        return mNestedScrollingParentHelper.getNestedScrollAxes();
    }
}
复制代码

在上述代码中,咱们须要注意如下几点:ide

  • StickyNavLayout实现类默认布局为竖直方向。
  • 为了让父控件处理竖直方向上的事件,咱们须要在onStartNestedScroll方法判断axes & ViewCompat.SCROLL_AXIS_VERTICAL
  • 为了让子控件也处理fling,咱们须要在onNestedPreFling方法中返回false。由于在嵌套滑动机制中,若是该方法返回true,那么子控件就没有机会处理fling了。
  • 为了兼容低版本并得到正确的嵌套滑动状态,咱们须要在onNestedScrollAccepted、onStopNestedScroll、onStopNestedScroll、中调用NestedScrollingParentHelper的相应方法。

布局设置

当咱们把父控件(StickNavaLayout)的基本框架搭好后,如今就准备处理整个界面的布局了。观察Demo效果,咱们发现当标题栏透明的时候,图片是彻底展现的,那么也就说明标题栏布局的层级是在图片之上的。大体布局以下图所示:oop

总体布局.png

继续观察Demo实现效果,咱们能够发现获得以下几点:布局

  • 当产生向上的手势滑动与fling时,若是展现图片没被父控件遮挡,那么父控件先拦截事件并滑动。当图片彻底被遮挡时,子控件再接着处理。
  • 当产生向下的手势滑动与fling时,若是展现图片没彻底显示,那么父控件先拦截事件并滑动。当图片彻底显示时,子控件再接着处理。

那么展现图片遮挡的效果是如何实现的呢?其实很简单,咱们只须要在咱们的父控件中添加一个与展现图片相同高度的透明的View就好了。那么当父控件在滚动的时候,就能够产生一种遮盖的效果啦。具体设计以下图所示:

StickyNavLayout布局.png

那么再对应Android的布局文件,整个界面的布局大概是下面这个样子:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent">

    <!--展现图片-->
    <ImageView android:layout_width="match_parent" android:layout_height="200dp" android:scaleType="fitXY" android:src="@drawable/ic_launcher_background"/>

    <!--标题栏-->
    <include layout="@layout/layout_common_toolbar"/>

    <!--嵌套滑动父控件-->
    <com.jennifer.andy.nestedscrollingdemo.view.StickyNavLayout android:id="@+id/sick_layout" android:layout_width="match_parent" android:layout_height="match_parent">

        <!--透明TopView-->
        <View android:id="@+id/sl_top_view" android:layout_width="match_parent" android:layout_height="200dp"/>
        <!--TabLayout-->
        <android.support.design.widget.TabLayout android:id="@+id/sl_tab" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#fff" app:tabIndicatorColor="@color/colorPrimaryDark"/>
        <!--ViewPager-->
        <android.support.v4.view.ViewPager android:id="@+id/sl_viewpager" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#fff"/>
    </com.jennifer.andy.nestedscrollingdemo.view.StickyNavLayout>

</RelativeLayout>
复制代码

父控件滑动范围

在完成了总体界面的布局后,如今咱们须要处理父控件的滚动。继续观察Demo,咱们能发现父控件滚动的范围为展现图片的高度减去标题栏的高度。为了计算父控件的滚动范围,咱们须要获取父控件内部的TopView(与展现图片高度相同的透明View)的高度。要获取父控件的子控件,咱们能够经过onFinishInflate方法。具体代码以下所示:

private View mTopView;//与展现图片高度相同的透明View

   @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mTopView = findViewById(R.id.sl_top_view);
    }
复制代码

获取了子控件后,咱们能够在onSizeChanged中获得,能够父控件能够滑动的距离(mCanScrollDistance = 展现图片的高度 - 标题栏的高度)。具体代码以下所示:

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        mCanScrollDistance = mTopView.getMeasuredHeight() - getResources().getDimension(R.dimen.normal_title_height);
    }
复制代码

由于标题栏的高度基本都是48dp,因此我这里并没单独去获取标题栏的高度,而是在values/dimens文件中声明了normal_title_height = 48dp。

父控件嵌套滑动实现

处理了父控件的滑动范围,如今到了最关键的嵌套滑动的处理了。当ViewPager中的RecyclerView接受到滑动后,会将滑动先分发给父控件,咱们的父控件(StickyNavaLayout)须要判断是否进行消耗,而判断是否消耗的条件以下:

  • 当向下滑动时,若是RecyclerView不能继续向下滑动且父控件(StickyNavaLayout)已经滑动了移动距离后,父控件(StickyNavaLayout)须要消耗。
  • 当向上滑动时,若是父控件(StickyNavaLayout)已经滑动了部分距离,那么父控件(StickyNavaLayout)须要消耗须要消耗。

具体代码以下所示:

@Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        //若是子view欲向上滑动,则先交给父view滑动
        boolean hideTop = dy > 0 && getScrollY() < mCanScrollDistance;
        //若是子view欲向下滑动,必需要子view不能向下滑动后,才能交给父view滑动
        boolean showTop = dy < 0 && getScrollY() >= 0 && !target.canScrollVertically(-1);
        if (hideTop || showTop) {
            scrollBy(0, dy);
            consumed[1] = dy;// consumed[0] 水平消耗的距离,consumed[1] 垂直消耗的距离
        }
    }
复制代码

在上述代码中,咱们经过调用View的canScrollVertically(int direction)方法来判断是否可以向下滑动,其中当direcation负数时,是检查对应View是否可以向下滑动,能,返回为true,反之返回false。当direcation正数时,是检查对应View是否可以向上滑动,能,返回为true,反之返回false。

须要注意的是在onNestedPreScroll方法中,咱们并无区分是手势滑动仍是fling,也就是区分type为TYPE_TOUCH(0)仍是TYPE_NON_TOUCH(1)。由于无论是手势滑动仍是fling。在Demo效果中父控件都须要处理。因此咱们并无进行判断。

当咱们处理了onNestedPreScroll方法后,咱们还须要处理onNestedScroll方法。由于根据嵌套滑动机制,当父控件预处理后,子控件会再消耗剩余的距离,若是子控件消耗后,还有剩余的距离。那么就又会传递给父控件。也就是会走onNestedScroll方法。在该方法中,咱们只须要单独处理子控件的剩余的向下fling。具体代码以下所示:

@Override
    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
        if (dyUnconsumed < 0 && type == ViewCompat.TYPE_NON_TOUCH) {//表示已经向下滑动到头,且为fling
            scrollBy(0, dyUnconsumed);
        }
    }

复制代码

当子控件产生fling时,若是子控件消耗不完,那么就会传递给父控件。也就是dyConsumed确定是有值的,又由于咱们只关心向下的fling。因此上述代码这样判断。

完成了嵌套滑动的处理后,咱们还须要对父控件(StickyNavaLayout)的滚动范围进行校验,咱们直接重写scrollTo方法。进行判断就行了。

@Override
    public void scrollTo(int x, int y) {
        if (y < 0) {
            y = 0;
        }
        if (y > mCanScrollDistance) {
            y = (int) mCanScrollDistance;
        }
        if (getScrollY() != y) super.scrollTo(x, y);
    }
复制代码

父控件(StickyNavaLayout)的滚动范围为0-mCanScrollDistance。其中mCanScrollDistance = 展现图片的高度 - 标题栏的高度。

ViewPager高度的矫正

到如今你们可能以为基本的嵌套滑动就结束了。可是若是你这样写的话你会发现一个问题:就是当咱们的父控件(StickyNavaLayout)滚动到标题栏下后,咱们会发现咱们的ViewPager并无填充屏幕剩下的距离,而是会有一个空白距离。以下所示:

空白区域.png

是由于咱们的父控件(StickyNavaLayout)继承了LinearLayout且ViewPager的高度为match_parent,那么根据View的测量规则,ViewPager实际的高度为屏幕中剩余的高度。因此父控件(StickyNavaLayout)滚动到标题栏下后,会出现一段空白,那么为了使ViewPager填充整个屏幕,咱们须要从新设置ViewPager的高度。也就是咱们须要重写父控件(StickyNavaLayout)的onMeasure方法。具体代码以下所示:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //先测量一次
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //ViewPager修改后的高度= 总高度-TabLayout的高度
        ViewGroup.LayoutParams lp = mViewPager.getLayoutParams();
        lp.height = getMeasuredHeight() - mNavView.getMeasuredHeight();
        mViewPager.setLayoutParams(lp);
        //由于ViewPager修改了高度,因此须要从新测量
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
复制代码

渐变效果实现

如今咱们就剩下最后两个效果了,回退键渐变与标题栏的透明度的变化了,其实实现也很是简单,由于咱们的父控件(StickyNavaLayout)有一个最大滑动的范围,那么咱们就能够获得当前父控件滑动的距离与最大滑动范围的比例,拿到这个比例后,咱们能够设置标题栏的透明度。也能够经过谷歌提供的ArgbEvaluator获得渐变颜色。具体的实现方式,读者朋友能够自行思考解决。由于篇幅的限制,这里就不在讲解具体的实现方式了。有须要的小伙伴,能够参看项目NestedScrollingDemo中的NestedScrolling2DemoActivity中的具体实现。

最后

整个Demo就讲解完毕了,你们有什么问题,欢迎提出~

相关文章
相关标签/搜索