笔者在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);    }}

布局代码:

<?xml version="1.0" encoding="utf-8"?><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);    }}

布局代码:

<?xml version="1.0" encoding="utf-8"?><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>

效果图

这里面通过滑动下面的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中该怎么实现,布局如下:
<?xml version="1.0" encoding="utf-8"?><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。

更多相关文章

  1. Android(安卓)Monkey工具
  2. Android的GridView和Gallery结合Demo
  3. Android通过scroller实现缓慢移动
  4. Android(安卓)webView嵌套html页面软键盘遮盖页面问题中级解决方
  5. Android(安卓)自动化测试之Monkey参数介绍及其停止办法
  6. android 三档开关做法
  7. 完全理解android事件分发机制
  8. 点击事件分发机制 关键源码笔记
  9. Android(安卓)实现item可左右滑动移除的GridView

随机推荐

  1. 【Android(安卓)开发教程】理解Intent对
  2. Android(安卓)Banner图片轮播控件+ViewPa
  3. android ListView 与 ScrollView 共存冲
  4. Android的读写文件权限
  5. sss
  6. Android四:sqllite
  7. Android学习_18_使用事务操作SQLite数据
  8. Android TextView(EditView)文字底部或者中
  9. 全屏显示布局随机图片的显示
  10. 记录app端嵌入式H5页面