CoordinatorLayout------協(xié)調(diào)者布局.
首先來看一下CoordinatorLayout的繼承結(jié)構(gòu):
CoordinatorLayout其本質(zhì)是一個超級 FrameLayout,其功能主要有2個:
1)作為頂層布局
2)協(xié)調(diào)其直接子View進行交互
官方文檔
使用前添加依賴:
compile 'com.android.support:design:25.3.1'
一、Behavior
CoordinatorLayout的神奇之處就在于Behavior對象,CoordinatorLayout通過Behavior對象來處理其子View的下列事件:
1)布局事件
2)觸摸事件
3)View狀態(tài)變化事件
4)嵌套滑動事件
Behavior就是CoordinatorLayout處理事件的媒介,在Behavior中定義了 CoordinatorLayout 中直接子 View 的行為規(guī)范,決定了當(dāng)收到不同事件時,應(yīng)該做怎樣的處理。
Behavior繼承結(jié)構(gòu)
Behavior是一個定義在CoordinatorLayout中的抽象內(nèi)部類.
public static abstract class Behavior<V extends View> {
public Behavior() {
}
public Behavior(Context context, AttributeSet attrs) {
}
......
}
下面我們通過自定義Behavior來簡單了解:
3)View狀態(tài)變化事件
某個View監(jiān)聽另一個View的狀態(tài)變化,例如大小、位置、顯示狀態(tài)等
此時,我們需要關(guān)心Behavior的下面兩個方法:
layoutDependsOn()
onDependentViewChanged()
4)嵌套滑動事件
某個View監(jiān)聽另一個View(實現(xiàn)NestedScrollingChild接口)的滑動狀態(tài)
此時,我們需要關(guān)心Behavior中與NestedScrolling相關(guān)的方法.
二、自定義Behavior之一
某個View監(jiān)聽另一個View的狀態(tài)變化
我們首先來看一下效果圖:
1)水平拖動dependency時,child朝著與dependency相反方向移動
2)豎直拖動dependency時,child在相同方向上同步移動
這里我們要理解兩個概念:child、dependency.
1) child是CoordinatorLayout的直接子View,也就是要執(zhí)行動作的View
2) dependency是指child依賴的View,也就是child要監(jiān)聽的View
在上面的效果圖中:child的動作依賴于dependency,當(dāng)dependency這個View發(fā)生了變化,那么child這個View就發(fā)生相應(yīng)變化。
child具體變化的動作就定義在Behavior中:
我們定義一個類,繼承CoordinatorLayout.Behavior<T>,其中,泛型參數(shù)T是我們要執(zhí)行動作的View類,也就是Child,然后實現(xiàn)Behavior的兩個方法:
layoutDependsOn()
onDependentViewChanged()
public class DependencyBehavior extends CoordinatorLayout.Behavior<Button> {
private int width;
/**
* 這個構(gòu)造方法必須重載,因為在CoordinatorLayout里利用反射去獲取Behavior的時候就是拿的這個構(gòu)造
*/
public DependencyBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
width = context.getResources().getDisplayMetrics().widthPixels;
}
/**
* 確定依賴關(guān)系
* @param child 要執(zhí)行動作的View
* @param dependency child要依賴的View,也就是child要監(jiān)聽的View
* @return 根據(jù)邏輯確定依賴關(guān)系
*/
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, Button child, View dependency) {
return dependency instanceof com.my.DragTextView;
}
/**
* dependency狀態(tài)(大小、位置、顯示與否等)發(fā)生變化時該方法執(zhí)行
* 在這里我們定義child要執(zhí)行的具體動作
* @return child是否要執(zhí)行相應(yīng)動作
*/
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, Button child, View dependency) {
int top = dependency.getTop();
int left = dependency.getLeft();
int x = width - left - child.getWidth();
int y = top;
setPosition(child, x, y);
return true;
}
private void setPosition(View child, int x, int y) {
CoordinatorLayout.MarginLayoutParams layoutParams = (CoordinatorLayout.MarginLayoutParams) child.getLayoutParams();
layoutParams.leftMargin = x;
layoutParams.topMargin = y;
child.setLayoutParams(layoutParams);
}
}
這里的DragTextView是一個自定義的TextView
/**
* 隨著手指移動的TextView
*/
public class DragTextView extends TextView {
private int lastX;
private int lastY;
public DragTextView(Context context) {
super(context);
}
public DragTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getRawX();
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
break;
}
case MotionEvent.ACTION_MOVE: {
CoordinatorLayout.MarginLayoutParams layoutParams = (CoordinatorLayout.MarginLayoutParams) getLayoutParams();
int left = layoutParams.leftMargin + x - lastX;
int top = layoutParams.topMargin + y - lastY;
layoutParams.leftMargin = left;
layoutParams.topMargin = top;
setLayoutParams(layoutParams);
requestLayout();
break;
}
case MotionEvent.ACTION_UP: {
break;
}
}
lastX = x;
lastY = y;
return true;
}
}
Behavior的使用方式:
- 在布局文件中通過app:layout_behavior=" "引用
- 使用注解添加,系統(tǒng)的控件一般使用這種方式,例AppBarLayout:
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
......
public static class Behavior extends HeaderBehavior<AppBarLayout> {
......
}
......
}
- 在代碼中添加
DependencyBehavior mBehavior = new DependencyBehavior(this,null);
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) childView.getLayoutParams();
params.setBehavior(mBehavior);
這里我們使用第一種方式:
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.my.DragTextView
android:layout_width="88dp"
android:layout_height="88dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="dependency"
android:textColor="@android:color/white" />
<Button
app:layout_behavior="com.my.DependencyBehavior"
android:layout_width="88dp"
android:layout_height="88dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="child"
android:textAllCaps="false"
android:textColor="@android:color/white"/>
</android.support.design.widget.CoordinatorLayout>
這里需要注意的是:
1)一個child可以同時依賴多個dependency
2)dependency也有可能依賴一個或者多個另外的dependency
3)如果你添加了一個依賴,不管child的順序如何,你的child將總是在所依賴的View放置之后才會被放置
ok,簡單的自定義Behavior已經(jīng)完成了,你對于CoordinatorLayout使用Behavior協(xié)調(diào)子View之間的交互是否有所了解了?
三、自定義Behavior之二
某個View監(jiān)聽另一個View(實現(xiàn)NestedScrollingChild接口)的滑動狀態(tài)
我們首先來看一下效果圖:
1)向上滑動時,右下角FloatingActionButton隱藏
2)向下滑動時,右下角FloatingActionButton顯示
3)點擊FloatingActionButton時,彈出Snackbar,同時FloatingActionButton自動上移.
其中效果(3)是FloatingActionButton中自帶的Behavior的效果,相信看了上面你也大概了解這個Behavior中FloatingActionButton一定是依賴Snackbar的了吧。
所以這里我們是直接繼承FloatingActionButton.Behavior:
public class ScrollBehavior extends FloatingActionButton.Behavior {
public ScrollBehavior(Context context, AttributeSet attrs) {
super(context,attrs);
}
/**
* 嵌套滑動事件開始
*
* @return 根據(jù)返回值確定我們關(guān)心哪個方向的滑動(x軸/y軸),這里我們關(guān)心的是y軸方向
*/
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
/**
* 嵌套滑動正在進行中
* 參數(shù)有點多,由于這里我們只關(guān)心y軸方向的滑動,所以簡單測試了dyConsumed、dyUnconsumed
* dyConsumed > 0 && dyUnconsumed == 0 上滑中
* dyConsumed == 0 && dyUnconsumed > 0 到邊界了還在上滑
*
* dyConsumed < 0 && dyUnconsumed == 0 下滑中
* dyConsumed == 0 && dyUnconsumed < 0 到邊界了還在下滑
*/
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
if (((dyConsumed > 0 && dyUnconsumed == 0)
|| (dyConsumed == 0 && dyUnconsumed > 0))
&& child.getVisibility() != View.INVISIBLE) {// 上滑隱藏
child.setVisibility(View.INVISIBLE);
} else if (((dyConsumed < 0 && dyUnconsumed == 0)
|| (dyConsumed == 0 && dyUnconsumed < 0))
&& child.getVisibility() != View.VISIBLE ) {//下滑顯示
child.setVisibility(View.VISIBLE);
}
}
}
這里我們采用一種高大上的方式來使用Behavior ,也是我們使用系統(tǒng)定義好Behavior 的常用方式:
在res/values/strings.xml中:
<resources>
<string name="app_name">CoordinatorLayout</string>
<string name="my_scroll_behavior">com.my.ScrollBehavior</string>
<!--看這里-->
<string name="text">\n
從前現(xiàn)在過去了再不來\n\n
紅紅落葉長埋塵土內(nèi)\n\n
開始終結(jié)總是沒變改\n\n
天邊的你飄泊白云外\n\n
苦海翻起愛恨\n\n
在世間難逃避命運\n\n
相親竟不可接近\n\n
或我應(yīng)該相信是緣份\n\n
情人別后永遠(yuǎn)再不來(消散的情緣)\n\n
無言獨坐放眼塵世外(愿來日再續(xù))\n\n
鮮花雖會凋謝(只愿)\n\n
但會再開(為你)\n\n
一生所愛隱約(守候)\n\n
在白云外(期待)\n\n
苦海翻起愛恨\n\n
在世間難逃避命運\n\n
相親竟不可接近\n\n
或我應(yīng)該相信是緣份\n\n
苦海翻起愛恨\n\n
在世間難逃避命運\n\n
相親竟不可接近\n\n
或我應(yīng)該相信是緣份\n
----------------\n
從前現(xiàn)在過去了再不來\n\n
紅紅落葉長埋塵土內(nèi)\n\n
開始終結(jié)總是沒變改\n\n
天邊的你飄泊白云外\n\n
苦海翻起愛恨\n\n
在世間難逃避命運\n\n
相親竟不可接近\n\n
或我應(yīng)該相信是緣份\n\n
情人別后永遠(yuǎn)再不來(消散的情緣)\n\n
無言獨坐放眼塵世外(愿來日再續(xù))\n\n
鮮花雖會凋謝(只愿)\n\n
但會再開(為你)\n\n
一生所愛隱約(守候)\n\n
在白云外(期待)\n\n
苦海翻起愛恨\n\n
在世間難逃避命運\n\n
相親竟不可接近\n\n
或我應(yīng)該相信是緣份\n\n
苦海翻起愛恨\n\n
在世間難逃避命運\n\n
相親竟不可接近\n\n
或我應(yīng)該相信是緣份\n
</string>
</resources>
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|top">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/text" />
</android.support.v4.widget.NestedScrollView>
<android.support.design.widget.FloatingActionButton
app:layout_behavior="@string/my_scroll_behavior"
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="16dp"
android:src="@mipmap/add"
app:backgroundTint="@color/colorPrimary"
app:borderWidth="0dp"
app:elevation="8dp"
app:fabSize="normal"
app:pressedTranslationZ="16dp" />
</android.support.design.widget.CoordinatorLayout>
Activiy中:
public class MainActivity extends AppCompatActivity {
private FloatingActionButton mFAB;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scroll);
mFAB = (FloatingActionButton) findViewById(R.id.fab);
mFAB.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Snackbar.make(mFAB,"Snackbar",Snackbar.LENGTH_SHORT).show();
}
});
}
}
這里需要注意的是:
1)你不需要在嵌套滑動的Behavior中定義依賴,CoordinatorLayout的每個child都有機會接收到嵌套滑動事件,這里繼承的FloatingActionButton.Behavior中存在依賴是因為要和Snackbar實現(xiàn)聯(lián)動.
2)雖然我叫它嵌套滑動,但其實它包含滾動(scrolling)和滑動(flinging)兩種
3)監(jiān)聽的滑動View必須實現(xiàn)NestedScrollingChild的接口,這是因為CoordinatorLayout中一個View想向外界傳遞滑動事件,即通知 NestedScrollingParent(CoordinatorLayout實現(xiàn)了此接口),就必須實現(xiàn)此接口.而 Child 與 Parent 的具體交互邏輯, NestedScrollingChildHelper 輔助類基本已經(jīng)幫我們封裝好了,所以我們只需要調(diào)用對應(yīng)的方法即可。
這就可以解釋為什么不能用ScrollView、ListView而用NestScrollView來滑動了,當(dāng)然,如果你要自己自定義一個View實現(xiàn)NestedScrollingChild接口也是可以的,不過那樣太麻煩了。像NestScrollView、RecyclerView、SwipeRefreshLayout中都實現(xiàn)了NestedScrollingChild接口.
如果你想了解關(guān)于嵌套滑動機制更多的詳情,你可以去看一看下面幾個類:
NestedScrollingChild
NestedScrollingParent
NestedScrollingChildHelper
NestedScrollingParentHelper
本文參考:
CoordinatorLayout的使用如此簡單
Intercepting everything with CoordinatorLayout Behaviors