文章目录
- 前言
- 分析
- 1、父容器子容器
- 2、如何形成关联,谁是发起者
- 3、NestedScrollingParent和NestedScrollingChild对应
- 4、响应者
- 示例
前言
嵌套滑动,顾名思义,嵌套嵌套就一定有父容器和子容器。如何能让子容器滑动能带动父容器(或父容器包含的其他子容器)滑动?什么样的子容器有这种能力?这种关联如何形成,以及被关联的容器如何响应,这种响应的逻辑在哪里定义?
相信你在对本文的阅读之后会有一定的了解
提示:以下是本篇文章正文内容
分析
1、父容器子容器
了解安卓开发的同学对这个概念再熟悉不过了,父容器是容器布局,子容器(控件)则是被这个父容器包含的容器布局(控件)。如:
<FrameLayout>
<RelativeLayout>
...
</RelativeLayout>
<View/>
</FrameLayout>
这里的FrameLayout
就是父容器,这里的RelativeLayout
和View
就是子容器(控件)。
2、如何形成关联,谁是发起者
父容器实现NestedScrollingParent
接口,
子容器实现NestedScrollingChild
接口。
查看代码示例
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.AppBarLayout/>
<FrameLayout
app:layout_behavior="android.support.design.widget.AppBarLayout$ScrollingViewBehavior">
<android.support.v7.widget.RecyclerView/>
<!--悬浮条-->
<RelativeLayout>
...
</RelativeLayout>
</FrameLayout
app:layout_behavior=".ScaleBehavior">
<android.support.design.widget.FloatingActionButton/>
</android.support.design.widget.CoordinatorLayout>
示例中的CoordinatorLayout
默认实现了NestedScrollingParent
接口,而RecyclerView
控件默认实现了NestedScrollingChild
接口。
所以在RecyclerView
滑动的时候,CoordinatorLayout
一直能收到相应的回调。比如说这时候这个控件不是RecyclerView
而是ListView
的话,那这个回调自然是没有的,理由就是ListView
并没有默认实现NestedScrollingChild
接口。
实现了NestedScrollingChild
接口的控件,其实也就是整个事件的发起者,而父容器便是接受的一方。
3、NestedScrollingParent和NestedScrollingChild对应
这两个接口的回调方法api,如下:
SCROLL_STATE_IDLE 0, 最后是RecyclerView滚动停止状态。
SCROLL_STATE_DRAGGING 1, 先是手指拖拽的状态
SCROLL_STATE_SETTLING 2,再是手指松开但是RecyclerView还在滑动
/**
*父容器实现的接口
*/
public interface NestedScrollingParent {
/**
* 开始滑动回调
* @param child 该父View 的子View
* @param target 支持嵌套滑动的 VIew
* @param nestedScrollAxes 滑动方向
* @return 是否支持 嵌套滑动
*/
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int nestedScrollAxes);
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int nestedScrollAxes);
void onStopNestedScroll(@NonNull View target);
/**
* 这里 传来了 x y 方向上的滑动距离
* 并且 先与 子VIew 处理滑动, 并且 consumed 中可以设置相应的 除了的距离
* 然后 子View 需要更具这感觉, 来处理自己滑动
*
* @param target
* @param dx
* @param dy
* @param consumed
*/
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);
/**
* 这里 主要处理 dyUnconsumed dxUnconsumed 这两个值对应的数据
* @param target
* @param dxConsumed
* @param dyConsumed
* @param dxUnconsumed
* @param dyUnconsumed
*/
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);
boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);
int getNestedScrollAxes();
}
/**
*子容器(控件)实现的接口
*/
public interface NestedScrollingChild {
//设置允许嵌套滑动 true表示允许
void setNestedScrollingEnabled(boolean enable);
boolean isNestedScrollingEnabled();
//开始嵌套滑动 这里需要返回true 否在后续事件不会再触发
boolean startNestedScroll(int axes);//坐标轴
//结束嵌套滑动
void stopNestedScroll();
//判断NestedParent的onStartNestedScroll是否返回true 只有为true后续的事件才能继续一系列的嵌套滑动
boolean hasNestedScrollingParent();
//子view消费了拖动事件之前通知父view,dx dy是将要消费的距离,如果父view要消费可通过
//设置consumed[0]=x .consumed[1]=y来分别消费x,y。然后子view继续处理剩下的位移(即dx-x,dy-y)
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow);
//子View消费滑动事件后通知父View
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);
//子view消费了滑动事件之前通知父view
boolean dispatchNestedPreFling(float var1, float var2);
//子view消费了滑动事件之后通知父view
boolean dispatchNestedFling(float var1, float var2, boolean var3);
}
父接口的回调和子接口的回调,两者的方法有明显的对应关系。
这样实现了子接口就可以在需要的时候调用接口的方法,如stopNestedScroll
,这样对应在父接口onStopNestedScroll
也就会被回调。
4、响应者
上文说了父容器是接受的一方,但它并不是真正意义上的响应者,响应者是谁取决于Behavior
的定义。
本例中的父容器是CoordinatorLayout
,我们就可以自定义一个Behavior
继承CoordinatorLayout.Behavior
,然后在对应的父容器回调方法中加入自己想要的逻辑。
值得一提的是,你即可以指定设置了Behavior
的控件本身响应,也可以指定该父容器下的其他子容器(控件)响应,无论这个Behavior
设置给哪个子容器(控件)。
示例如下:
public class ScaleBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
//只有返回true 后续的动作才会触发
return axes == ViewCompat.SCROLL_AXIS_VERTICAL;//垂直滚动
}
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
...
}
}
当然Android本身也有很多定义好的Behavior
可以直接使用,这里就不赘述了。
最后将这个Behavior
设置到布局中,就可以正常使用了。
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.AppBarLayout/>
<FrameLayout
app:layout_behavior="android.support.design.widget.AppBarLayout$ScrollingViewBehavior">
<android.support.v7.widget.RecyclerView/>
<!--悬浮条-->
<RelativeLayout>
...
</RelativeLayout>
</FrameLayout
app:layout_behavior=".ScaleBehavior">
<android.support.design.widget.FloatingActionButton/>
</android.support.design.widget.CoordinatorLayout>
示例
本例主要
- 使用
RecyclerView
做示范。(将项目启动页改为MainActivity
查看) - 自定义了一个实现了
NestedScrollingChild
接口的ListView
做示范。 - 自定义
Behavior
示范。
效果如下
总布局代码如下
<?xml versinotallow="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"
tools:context=".SecondActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:layout_scrollFlags="scroll|enterAlways"
app:title="ToolBar" />
</android.support.design.widget.AppBarLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="android.support.design.widget.AppBarLayout$ScrollingViewBehavior">
<pers.owen.recyclerview.NestedListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
tools:listitem="@layout/item_feed" />
<!--悬浮条-->
<RelativeLayout
android:id="@+id/suspension_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white">
<pers.owen.recyclerview.CircleImageView
android:id="@+id/iv_avatar"
android:layout_width="44dp"
android:layout_height="44dp"
android:padding="8dp"
android:src="@drawable/avatar1" />
<TextView
android:id="@+id/tv_nickname"
android:layout_width="wrap_content"
android:layout_height="44dp"
android:layout_marginLeft="8dp"
android:layout_toRightOf="@id/iv_avatar"
android:gravity="center_vertical"
android:text="粥可温"
android:textSize="12sp" />
<View
android:id="@+id/top_divider"
android:layout_width="match_parent"
android:layout_height="0.2dp"
android:layout_below="@id/tv_nickname"
android:background="#33000000" />
</RelativeLayout>
</FrameLayout>
<android.support.design.widget.FloatingActionButton
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="bottom|right"
android:layout_margin="16dp"
app:layout_behavior=".ScaleBehavior" />
</android.support.design.widget.CoordinatorLayout>
NestedListView代码如下
public class NestedListView extends ListView implements NestedScrollingChild {
//1初始化获取ChildHelper
private NestedScrollingChildHelper mChildHelper;
private int mLastY;
private final int[] mScrollOffset = new int[2];//滑动偏移
private final int[] mScrollConsumed = new int[2];//滑动消费
private int mNestedOffsetY;//嵌套偏移
public NestedListView(Context context) {
super(context);
init();
}
public NestedListView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public NestedListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return mChildHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
mChildHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return mChildHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) {
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
int y = (int) ev.getY();
ev.offsetLocation(0, mNestedOffsetY);
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastY = y;
mNestedOffsetY = 0;
this.startNestedScroll((ViewCompat.SCROLL_AXIS_VERTICAL));//开始嵌套滑动
break;
case MotionEvent.ACTION_MOVE:
int dy = mLastY - y;//Y的拖动距离
int oldY = getScrollY();//注意一般一直为0
//在自己消费前先分发给父容器
if (dispatchNestedPreScroll(0, dy, mScrollConsumed, mScrollOffset)) {
dy -= mScrollConsumed[1];//剩余
ev.offsetLocation(0, -mScrollOffset[1]);
mNestedOffsetY += mScrollOffset[1];
}
mLastY = y - mScrollOffset[1];
int newScrollY = oldY + dy;
dy -= newScrollY - oldY;//全部消费完
//自己消费
if (dispatchNestedScroll(0, newScrollY - dy, 0, dy, mScrollOffset)) {
ev.offsetLocation(0, mScrollOffset[1]);
mNestedOffsetY += mScrollOffset[1];
mLastY -= mScrollOffset[1];
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
stopNestedScroll();
break;
}
return super.onTouchEvent(ev);
}
}
ScaleBehavior代码如下
public class ScaleBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
private Interpolator interpolator;
private boolean isRunning;
public ScaleBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
interpolator = new AccelerateDecelerateInterpolator();
}
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
//只有返回true 后续的动作才会触发
return axes == ViewCompat.SCROLL_AXIS_VERTICAL;//垂直滚动
}
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
Log.e("test", dyConsumed + " " + dyUnconsumed);
if (dyConsumed > 0 && !isRunning && child.getVisibility() == View.VISIBLE) {
//上滑 缩小隐藏 动画
scaleHide(child);
} else if (dyConsumed < 0 && !isRunning && child.getVisibility() == View.INVISIBLE) {
//下滑 放大显示
scaleShow(child);
}
}
private void scaleShow(final V child) {
child.setVisibility(View.VISIBLE);
ViewCompat.animate(child).alpha(1).scaleX(1).scaleY(1).setInterpolator(interpolator)
.setListener(new ViewPropertyAnimatorListener() {
@Override
public void onAnimationStart(View view) {
isRunning = true;
}
@Override
public void onAnimationEnd(View view) {
isRunning = false;
}
@Override
public void onAnimationCancel(View view) {
isRunning = false;
}
}).setDuration(500).start();
}
private void scaleHide(final V child) {
ViewCompat.animate(child).alpha(0).scaleX(0).scaleY(0).setInterpolator(interpolator)
.setListener(new ViewPropertyAnimatorListener() {
@Override
public void onAnimationStart(View view) {
isRunning = true;
}
@Override
public void onAnimationEnd(View view) {
isRunning = false;
child.setVisibility(View.INVISIBLE);
}
@Override
public void onAnimationCancel(View view) {
isRunning = false;
}
}).setDuration(500).start();
}
}