Android嵌套滑动-Behavior方案实战及细节注意

笔者在2013年就收到Android嵌套滑动的UI效果需求,当时都是直接从监听滑动事件分发做起,至今再次收到这种类似的需求,一直以来想更新下之前的实现方式,相对于Behavior封装过的方案而言毕竟不够优雅,现就介绍前后两种方案。

  • 老方案的思路

    这种方式是相关api直接使用,其他的封装方式(包括behavoir)都是基于此封装而来,直接重写父类(ViewGroup)的事件分发机制:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent等方法,手动事件分发,当属于逻辑外层滑动时候,进行拦截,满足一定条件之后,再重新分发事件给相关子嵌套的滚动View。这里面代码实现就不展示出来,有点历史,思路在此,不过实现中会有些问题。
    例如当重新把move事件分发给子View时,这时子View突然接受到move事件,没有完整的流程经历down事件会导致未初始化而不能响应move事件,就是常见的不能连续滑动的根本原因;其次就是拦截事件不要拦截down事件,会导致某个view点击事件不能响应,滑动都应该只是针对move事件拦截。

  • Behavior方式

    在说Behavior之前先简单提下嵌套滑动在5.0之后新增的Api:NestedScrollingParent、NestedScrollingChild以及相应的Helper类,具体介绍不是重点,分别实现这些接口的父View和子View类就能够实现父View对子View嵌套滑动的监听,同时父View和子View之间不一定是直接的上下层关系,子View可以是父view下任意子View,例如NestedScrollView、RecyclerView、CoordinatorLayout(本文重点,下面再讲)都分别实现这两个接口中一个或两个,当然我们可以自定义ViewGroup实现NestedScrollingParent来监听子View的嵌套滑动,贴下代码:

CustomNestedScrollLinearLayout .class

public class CustomNestedScrollLinearLayout extends LinearLayout implements NestedScrollingParent {
    View mchild, mRecyc, mTitle;

    public CustomNestedScrollLinearLayout(Context context) {
        super(context);
    }

    public CustomNestedScrollLinearLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mchild = findViewById(R.id.move);
        mRecyc = findViewById(R.id.recyclerView);
        mTitle = findViewById(R.id.title);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        ViewGroup.LayoutParams params = mRecyc.getLayoutParams();
        params.height = getMeasuredHeight() - findViewById(R.id.title).getMeasuredHeight();
    }


    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes) {
        Log.i("onLayoutChild", "target=" + target.getHeight());
        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
        int bottom = mchild.getBottom();
        int chileHeight = mchild.getHeight();
        Log.i("onNestedScroll", "onNestedPreScroll dy=" + dy + " bottom=" + bottom);
        if (dy > 0 && bottom > 0) {
            int left = bottom - dy;
            if (left >= 0) {
                consumed[1] = dy;
            } else {
                consumed[1] =bottom;
            }
            mchild.offsetTopAndBottom(-consumed[1]);
            mTitle.offsetTopAndBottom(-consumed[1]);
            target.offsetTopAndBottom(-consumed[1]);
        }     }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
        Log.i("onNestedScroll", "onNestedScroll dyConsumed=" + dyConsumed + " dyUnconsumed=" + dyUnconsumed);
        if (dyUnconsumed > 0) {
            return;
        }
        int bottom = mchild.getBottom();
        int chileHeight = mchild.getHeight();
        if (dyUnconsumed < 0 && bottom < chileHeight) {
            int left = bottom - dyUnconsumed;
            int consumed;
            if (left <= chileHeight) {
                consumed = dyUnconsumed;
            } else {
                consumed = -chileHeight + bottom;
            }
            mchild.offsetTopAndBottom(-consumed);
            mTitle.offsetTopAndBottom(-consumed);
            target.offsetTopAndBottom(-consumed);
        }
    }

    @Override
    public void onStopNestedScroll(View child) {
        Log.i("onNestedScroll", "onStopNestedScroll child=" + child.getClass().getSimpleName());
        super.onStopNestedScroll(child);
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        Log.i("onNestedScroll", "onNestedPreFling target=" + target.getClass().getSimpleName() + " velocityY=" + velocityY);
        return super.onNestedPreFling(target, velocityX, velocityY);
    }
}

布局代码:


<statistics.ymm.com.myapplication.CustomNestedScrollLinearLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">


    <TextView
        android:id="@+id/move"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@color/colorAccent"
        android:gravity="center"
        android:text="Hello World!" />

       <TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="20dp"
        android:background="@android:color/darker_gray"
        android:gravity="center"
        android:text="title" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorPrimary"
        app:layout_behavior="statistics.ymm.com.myapplication.MyBehavior" />


statistics.ymm.com.myapplication.CustomNestedScrollLinearLayout>

原理本文篇幅不够,不想写,之前都是写关于原理篇,可以百度,今天搞个实战篇,第一次贴代码,拿走就用,不谢。

当然这不是本文重点,确实基础,上文提到了CoordinatorLayout,其中Behavior是就是CoordinatorLayout的静态内部类,对其可以简单理解为在CoordinatorLayout实现NestedScrollingParent2之后,接受到子View的滑动通知之后,把直接通过子View的Behavior来通知回调(注意是直接子View,因为Behavior是CoordinatorLayout.LayoutParams的元素,只能解析直接子View的Behavoir配置),Behavior提供了很多回调,包括了嵌套滑动相关的接口方法。废话不多说,上代码:

public class MyBehavior extends CoordinatorLayout.Behavior {

    private WeakReference dependentView;

    public MyBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    private View getDependentView() {
        return dependentView.get();
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        if (dependency != null && dependency.getId() == R.id.move) {
            dependentView = new WeakReference<>(dependency);
            return true;
        }
        return super.layoutDependsOn(parent, child, dependency);
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        child.setTranslationY(dependency.getHeight() + dependency.getTranslationY());
        return true;
    }

    @Override
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {

        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);

        if (dy < 0) {
            return;
        }
        View dependentView = getDependentView();
        float newTranslateY = dependentView.getTranslationY() - dy;
        float minHeaderTranslate = -(dependentView.getHeight());
        Log.i("onLayoutChild", "onNestedPreScroll dy=" + dy + "TranslationY='" + dependentView.getTranslationY());
        if (newTranslateY >= minHeaderTranslate) {
            dependentView.setTranslationY(newTranslateY);
            consumed[1] = dy;
        } else {
            if (dependentView.getTranslationY() >= -minHeaderTranslate) {
                consumed[1] = (int) (dependentView.getTranslationY() - minHeaderTranslate);
            }
            dependentView.setTranslationY(minHeaderTranslate);

        }
    }


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

        if (dyUnconsumed > 0) {
            return;
        }
        View dependentView = getDependentView();
        float currentTranslationY = dependentView.getTranslationY();
        float newTranslateY = currentTranslationY - dyUnconsumed;
        final float maxHeaderTranslate = 0;
        Log.i("onLayoutChild", "onNestedScroll dyUnconsumed=" + dyUnconsumed + "currentTranslationY="+currentTranslationY);
        if (newTranslateY <= maxHeaderTranslate) {
            dependentView.setTranslationY(newTranslateY);
        } else {
            dependentView.setTranslationY(maxHeaderTranslate);
        }


        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
    }

    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {

        return super.onLayoutChild(parent, child, layoutDirection);
    }

    @Override
    public boolean onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {

        return super.onMeasureChild(parent, child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
    }
}

布局代码:


<android.support.design.widget.CoordinatorLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/move"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@color/colorAccent"
        android:gravity="center"
        android:text="Hello World!" />
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        app:layout_behavior="statistics.ymm.com.myapplication.MyBehavior">

        <TextView
            android:id="@+id/title"
            android:layout_width="match_parent"
            android:layout_height="20dp"
            android:background="@android:color/darker_gray"
            android:gravity="center"
            android:text="title"
             />

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/colorPrimary"
  />
    LinearLayout>
android.support.design.widget.CoordinatorLayout>

效果图
Android嵌套滑动-Behavior方案实战及细节注意_第1张图片
Android嵌套滑动-Behavior方案实战及细节注意_第2张图片

这里面通过滑动下面的list,会先让红色区域的Hello World!先移动,直到消失之后,list才滑动,中间连贯,不中断,连续滑动,title停在顶层不动。基本满足个人的需求,直接但是如果上面红色header过长话,希望能通过滑动header(Helll World!区域)也能滑动整页,而不是仅仅通过列表滑动来触发的滑动,这时候嵌套滑动就不够满足。上文也提到了Behavior有好多其他回调接口,要想实现Header滑动导致整页滑动,故此我们必须监听Header上面的滑动事件触发,肯定会想到重写Header的事件分发,这会显得麻烦。Behavior就提供了View滑动事件的拦截监听,直接贴代码。

 @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) {
        int dy = (int) ev.getY();
//        Log.i("chuan", "onInterceptTouchEvent=" + ev.getAction() + "dy=" + dy);
        View dependView = getDependentView();
        if (dependView == null) {
            return super.onInterceptTouchEvent(parent, child, ev);
        }

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downY = (int) ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                lastY = dy;
                if (Math.abs(lastY - downY) > 1 && dy < (dependView.getMeasuredHeight() + dependView.getTranslationY())) {
                    return true;
                }
                break;
            default:
                break;
        }

        return super.onInterceptTouchEvent(parent, child, ev);
    }

    int lastY;

    @Override
    public boolean onTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) {
        acquireVelocityTracker(ev);
        final VelocityTracker verTracker = mVelocityTracker;
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_MOVE:
                int dy = y - lastY;
//                Log.i("chuan", "onTouchEvent=" + ev.getAction() + "dy=" + dy);
                if (dy < 0) {
                    moveUp(-dy, new int[2]);
                } else {
                    movedown(-dy);
                }
                lastY = y;
                break;
            case MotionEvent.ACTION_CANCEL:
                //自动
                verTracker.computeCurrentVelocity(1000, mMaxVelocity);
                autoFlingBySpeedIfNedd(-verTracker.getYVelocity());
                releaseVelocityTracker();
                break;
            default:
                break;
        }
        return super.onTouchEvent(parent, child, ev);
    }

重写Behavior中onInterceptTouchEvent等方法,判断手势启动位置,如果Header没有消失,就拦截Move事件,让header移动,header移动之后其dependVIew子View就会跟着滑动,从而实现整页的滑动。

  • 细节
    -CoordinatorLayout中子View 布局中属性增加MarginBottom或top会导致下面的依赖view之间有重叠覆盖。如上文中的Header若添加margin,会导致其依赖的view之间发生重叠,这个应该是CoordinatorLayout在layout子View时候没有计算上下间距。
    2、多个依赖view之间的布局,第3个view要减去第二个view的高度。例如上列中布局可以看到title和list都在一层父布局中,但是如果希望就是都在CoordinatorLayout中该怎么实现,布局如下:

<android.support.design.widget.CoordinatorLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">


    <TextView
        android:id="@+id/move"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@color/colorAccent"
        android:gravity="center"
        android:text="Hello World!" />


  

        <TextView
            android:id="@+id/title"
            android:layout_width="match_parent"
            android:layout_height="20dp"
            android:background="@android:color/darker_gray"
            android:gravity="center"
            android:text="title"
            app:layout_behavior="statistics.ymm.com.myapplication.MyBehavior" />

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/colorPrimary"
            app:layout_behavior="statistics.ymm.com.myapplication.Titlebehavior" />
  


android.support.design.widget.CoordinatorLayout>

这时候就要增加之后布局的依赖关系了,title移动是依赖Header,设置MyBehavior配置,而list就要跟着title移动继续,新增Titlebehavior,让其依赖title,代码如下:

public class Titlebehavior extends CoordinatorLayout.Behavior {
    public Titlebehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    private WeakReference dependentView;

    private View getDependentView() {
        return dependentView.get();
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {

        if (dependency != null && dependency.getId() == R.id.title) {
            dependentView = new WeakReference<>(dependency);
            return true;
        }
        return super.layoutDependsOn(parent, child, dependency);
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {

        child.setTranslationY(dependency.getTranslationY());

        return true;
    }


    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
        if (lp.height == CoordinatorLayout.LayoutParams.MATCH_PARENT) {
            child.layout(0, (int) TypedValue.applyDimension(1, 20, child.getResources().getDisplayMetrics()), parent.getWidth(), (int) (parent.getHeight()));
            return true;
        }
        return super.onLayoutChild(parent, child, layoutDirection);
    }
}

这个时候要注意onLayoutChild对list控件layOut时候要手动减去依赖VIew的高度,也就是title,否则会导致直接覆盖了title。

你可能感兴趣的:(Android,UI开发)