文章目錄
- 前言
- 分析
- 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 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"
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();
}
}
推薦閱讀
Android 11新特性,Scoped Storage又有了新花樣
About
本文Demo
UI系列文章一覽
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/265949.html
標籤:其他
