自定義Behavior —— 仿知乎,FloatActionButton隱藏與展示
前段時間寫了一篇博客 使用CoordinatorLayout打造各種炫酷的效果 ,主要介紹了APPBarLayout和CollapsingToolbarLayout的基本用法,AppBarLayout主要用來實現在滾動的時候ToolBar的 隱藏于展示,CollapsingToolbarLayout主要用來實現parallax和pin等效果。如果你對CoordinatorLayout還不了解的話,請先閱讀這篇文章。
寫作思路
- CoordinatorLayout Behavior 簡介
- 怎樣自定義 Behavior
- 仿知乎效果 自定義 Behavior 的實現
- 自定義 Behavior 兩種方法的 對比
- FloatActionButton 自定義 Behavior 效果的實現
- 題外話
今天就來講解怎樣通過自定義behavior來實現各種炫酷的效果 ,效果圖如下
下面讓我們一起來看一下怎樣實現仿知乎的效果
老規矩,先看代碼
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
android:id="@+id/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.support.design.widget.AppBarLayout
android:id="@+id/index_app_bar"
theme="@style/AppTheme.AppBarOverlay"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways">
<ImageView
android:id="@+id/search"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:src="@drawable/search"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:layout_toRightOf="@id/search"
android:text="搜索話題、問題或人"
android:textSize="16sp"/>
</RelativeLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
</android.support.v7.widget.RecyclerView>
<!--使用RadioGroup來實現tab的切換-->
<RadioGroup
android:id="@+id/rg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@color/bg_tab"
android:orientation="horizontal"
app:layout_behavior="@string/behavior_footer"
>
<RadioButton
android:id="@+id/rb_home"
style="@style/bottom_tab"
android:drawableTop="@drawable/sel_home"
android:text="Home"/>
<RadioButton
android:id="@+id/rb_course"
style="@style/bottom_tab"
android:drawableTop="@drawable/sel_course"
android:text="course"/>
<RadioButton
android:id="@+id/rb_direct_seeding"
style="@style/bottom_tab"
android:drawableTop="@drawable/sel_direct_seeding"
android:text="direct"/>
<RadioButton
android:id="@+id/rb_me"
style="@style/bottom_tab"
android:drawableTop="@drawable/sel_me"
android:text="me"/>
</RadioGroup>
</android.support.design.widget.CoordinatorLayout>
<style name="bottom_tab">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">60dp</item>
<item name="android:layout_weight">1</item>
<item name="android:text">0dp</item>
<item name="android:gravity">center</item>
<item name="android:textColor">@drawable/sel_bottom_tab_text</item>
<item name="android:padding">10dp</item>
<item name="android:button">@null</item>
</style>
<style name="bottom_tab">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">60dp</item>
<item name="android:layout_weight">1</item>
<item name="android:text">0dp</item>
<item name="android:gravity">center</item>
<item name="android:textColor">@drawable/sel_bottom_tab_text</item>
<item name="android:padding">10dp</item>
<item name="android:button">@null</item>
</style>
思路分析
根據動態如可以看到,主要有兩個效果
- 上面的AppBarLayout 向上滑動的時候會隱藏,向下滑動的時候會展示,說白了就是給APPLayout的子View Relativelayout 設置 app:layout_scrollFlags="scroll|enterAlways",核心代碼如下
<android.support.design.widget.AppBarLayout
android:id="@+id/index_app_bar"
theme="@style/AppTheme.AppBarOverlay"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways">
----
</RelativeLayout>
</android.support.design.widget.AppBarLayout>
- 下面的 RadioGroup ,我們可以看到,向上 滑動的時候會隱藏,向下滑動的時候會顯示,其實我們只是給其設置了 behavior 而已 app:layout_behavior="@string/behavior_footer",那這個behavior_footer是什么東西,別急 ,下面就是介紹了
<string name="behavior_footer">com.xujun.contralayout.behavior.FooterBehavior</string>
Behavior簡介
Behavior是CoordinatorLayout里面的一個內部類,通過它我們可以與 CoordinatorLayout的一個或者多個子View進行交互,包括 drag,swipes, flings等手勢動作。
今天 我們主要著重介紹里面的幾個方法
方法 | 解釋 |
---|---|
boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) | 確定child View 是否有一個特定的兄弟View作為布局的依賴(即dependency) |
boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) | 當child View 的 dependent view 發生變化的時候,這個方法會調用 |
boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes) | 當CoordinatorLayout 的直接或者非直接子View開始準備嵌套滑動的時候會調用 |
void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) | 當嵌套滑動的 時候,target嘗試滑動或者正在滑動的 時候會調用 |
關于更多方法,請參考官 網文檔說明
怎樣自定義Behavior
前面已經說到,今天主要介紹四個方法,這里我們把它分為兩組。
第一組
// 決定child 依賴于把一個 dependency
boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency)
// 當 dependency View 改變的時候 child 要做出怎樣的響應
boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency)
第二組
// 當CoordinatorLayout的直接或者非直接子View開始嵌套滑動的時候,會調用這個方法
boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes)
// 當嵌套滑動的時候,target 嘗試滑動或者正在滑動會調用這個方法
onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
首先我們先看第一組是怎樣實現的?
/**
* 知乎效果底部behavior 依賴于 AppBarLayout
*
* @author xujun on 2016/11/30.
* @email gdutxiaoxu@163.com
*/
public class FooterBehaviorDependAppBar extends CoordinatorLayout.Behavior<View> {
public static final String TAG = "xujun";
public FooterBehaviorDependAppBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
//當 dependency instanceof AppBarLayout 返回TRUE,將會調用onDependentViewChanged()方法
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof AppBarLayout;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
//根據dependency top值的變化改變 child 的 translationY
float translationY = Math.abs(dependency.getTop());
child.setTranslationY(translationY);
Log.i(TAG, "onDependentViewChanged: " + translationY);
return true;
}
}
思路分析
這里我們要分清兩個概念,child 和 dependency ,child 是我們要改變的坐標的view,而 dependency 是child 的 附屬 ,即child 會隨著 dependency 坐標的改變而改變。
比如上面的例子:當我們把 app:layout_behavior="com.xujun.contralayout.behavior.FooterBehaviorDependAppBar" 設置給 RadioGroup 的時候,這時候 child 就是 RadioGroup ,而 dependency 就是 APPBarLayout ,因為我們在 layoutDependsOn 方法里面 ,返回 dependency instanceof AppBarLayout ,即當 dependency 是 AppBarLayout 或者 AppBarLayout的子類的時候返回TRUE。
//當 dependency instanceof AppBarLayout 返回TRUE,將會調用onDependentViewChanged()方法
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof AppBarLayout;
}
而之所以 RadioGroup 在向上滑動的時候會隱藏,向下滑動的時候會顯示,是因為我們在 onDependentViewChanged 方法的時候 動態地根據 dependency 的 top 值改變 RadioGroup 的 translationY 值,核心 代碼如下
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
//根據dependency top值的變化改變 child 的 translationY
float translationY = Math.abs(dependency.getTop());
child.setTranslationY(translationY);
Log.i(TAG, "onDependentViewChanged: " + translationY);
return true;
}
到此第一種思路分析為止
第二種思路
主要是根據 onStartNestedScroll() 和 onNestedPreScroll()方法 來實現的,
- 當我們開始滑動的時候,我們判斷是否是垂直滑動,如果是返回TRUE,否則返回 FALSE,返回TRUE,會接著調用onNestedPreScroll()等一系列方法。
//1.判斷滑動的方向 我們需要垂直滑動
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child,
View directTargetChild, View target, int nestedScrollAxes) {
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
- 在 onNestedPreScroll() 方法里面,我們根據我們的邏輯來決定是否顯示 target , 在這里我們是向上上滑動的時候,如果我們滑動的距離超過 target 的高度 并且 當前是可見的狀態下,我們執行動畫,隱藏 target,當我們向下滑動的時候,并且 View 是不可見的情況下,我們執行動畫 ,顯示target
//2.根據滑動的距離顯示和隱藏footer view
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child,
View target, int dx, int dy, int[] consumed) {
if (dy > 0 && sinceDirectionChange < 0 || dy < 0 && sinceDirectionChange > 0) {
child.animate().cancel();
sinceDirectionChange = 0;
}
sinceDirectionChange += dy;
int visibility = child.getVisibility();
if (sinceDirectionChange > child.getHeight() && visibility == View.VISIBLE) {
hide(child);
} else {
if (sinceDirectionChange < 0 && (visibility == View.GONE || visibility == View
.INVISIBLE)) {
show(child);
}
}
}
全部代碼如下
/**
* 知乎效果底部 behavior
*
* @author xujun on 2016/11/30.
* @email gdutxiaoxu@163.com
*/
public class FooterBehavior extends CoordinatorLayout.Behavior<View> {
private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
private int sinceDirectionChange;
public FooterBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
//1.判斷滑動的方向 我們需要垂直滑動
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child,
View directTargetChild, View target, int nestedScrollAxes) {
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
//2.根據滑動的距離顯示和隱藏footer view
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child,
View target, int dx, int dy, int[] consumed) {
if (dy > 0 && sinceDirectionChange < 0 || dy < 0 && sinceDirectionChange > 0) {
child.animate().cancel();
sinceDirectionChange = 0;
}
sinceDirectionChange += dy;
int visibility = child.getVisibility();
if (sinceDirectionChange > child.getHeight() && visibility == View.VISIBLE) {
hide(child);
} else {
if (sinceDirectionChange < 0 && (visibility == View.GONE || visibility == View
.INVISIBLE)) {
show(child);
}
}
}
private void hide(final View view) {
ViewPropertyAnimator animator = view.animate().translationY(view.getHeight()).
setInterpolator(INTERPOLATOR).setDuration(200);
animator.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
view.setVisibility(View.GONE);
}
@Override
public void onAnimationCancel(Animator animator) {
show(view);
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
animator.start();
}
private void show(final View view) {
ViewPropertyAnimator animator = view.animate().translationY(0).
setInterpolator(INTERPOLATOR).
setDuration(200);
animator.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
view.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationCancel(Animator animator) {
hide(view);
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
animator.start();
}
}
兩種實現方法的對比和總結
-
我們知道第一種方法我們主要是重寫layoutDependsOn 和 onDependentViewChanged 這兩個方法,這個方法在 layoutDependsOn 判斷 dependency 是否是 APpBarLayout 的實現類,所以 會導致 child 依賴于 AppBarLayout,靈活性不是太強
-
而第二種方法,我們主要是重寫 onStartNestedScroll 和 onNestedPreScroll 這兩個方法,判斷是否是垂直滑動,是的話就進行處理,靈活性大大增強,推薦使用這一種方法
-
需要注意的是不管是第一種方法,還是第二種方法,我們都需要重寫帶兩個構造方法的函數,因為底層機制會采用反射的形式獲得該對象
public FooterBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
自定義 Behavior 實現 FloatingActionButton 的顯示與隱藏
效果圖如下
縮放隱藏的
向上向下隱藏的
布局代碼
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
android:id="@+id/activity_floating_action_button"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="com.xujun.contralayout.UI.FloatingActionButtonActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/index_app_bar"
theme="@style/AppTheme.AppBarOverlay"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways">
<ImageView
android:id="@+id/search"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:src="@drawable/search"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:layout_toRightOf="@id/search"
android:text="搜索話題、問題或人"
android:textSize="16sp"/>
</RelativeLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
</android.support.v7.widget.RecyclerView>
<android.support.design.widget.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right|end"
android:layout_marginBottom="40dp"
android:layout_marginRight="25dp"
android:background="@android:color/holo_green_light"
android:src="@drawable/add"
app:layout_behavior="@string/behavior_my_fab_scale"/>
</android.support.design.widget.CoordinatorLayout>
如果想使用不同的效果,只需要給 FloatingActionButton 制定不同的 bevaior 即可
app:layout_behavior="com.xujun.contralayout.behavior.MyFabBehavior"
自定義behavior 代碼
/**
* FloatingActionButton behavior 向上向下隱藏的
* @author xujun on 2016/12/1.
* @email gdutxiaoxu@163.com
*/
public class MyFabBehavior extends CoordinatorLayout.Behavior<View> {
private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
private float viewY;//控件距離coordinatorLayout底部距離
private boolean isAnimate;//動畫是否在進行
public MyFabBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
//在嵌套滑動開始前回調
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
if(child.getVisibility() == View.VISIBLE&&viewY==0){
//獲取控件距離父布局(coordinatorLayout)底部距離
viewY=coordinatorLayout.getHeight()-child.getY();
}
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;//判斷是否豎直滾動
}
//在嵌套滑動進行時,對象消費滾動距離前回調
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
//dy大于0是向上滾動 小于0是向下滾動
if (dy >=0&&!isAnimate&&child.getVisibility()==View.VISIBLE) {
hide(child);
} else if (dy <0&&!isAnimate&&child.getVisibility()==View.GONE) {
show(child);
}
}
//隱藏時的動畫
private void hide(final View view) {
ViewPropertyAnimator animator = view.animate().translationY(viewY).setInterpolator(INTERPOLATOR).setDuration(200);
animator.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
isAnimate=true;
}
@Override
public void onAnimationEnd(Animator animator) {
view.setVisibility(View.GONE);
isAnimate=false;
}
@Override
public void onAnimationCancel(Animator animator) {
show(view);
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
animator.start();
}
//顯示時的動畫
private void show(final View view) {
ViewPropertyAnimator animator = view.animate().translationY(0).setInterpolator(INTERPOLATOR).setDuration(200);
animator.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
view.setVisibility(View.VISIBLE);
isAnimate=true;
}
@Override
public void onAnimationEnd(Animator animator) {
isAnimate=false;
}
@Override
public void onAnimationCancel(Animator animator) {
hide(view);
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
animator.start();
}
}
/**
* <p>下拉時顯示FAB,上拉隱藏,留出更多位置給用戶。</p>
* Created on 2016/12/1.
*
* @author xujun
*/
public class ScaleDownShowBehavior extends FloatingActionButton.Behavior {
/**
* 退出動畫是否正在執行。
*/
private boolean isAnimatingOut = false;
private OnStateChangedListener mOnStateChangedListener;
public ScaleDownShowBehavior(Context context, AttributeSet attrs) {
super();
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
if ((dyConsumed > 0 || dyUnconsumed > 0) && !isAnimatingOut && child.getVisibility() == View.VISIBLE) {//往下滑
AnimatorUtil.scaleHide(child, viewPropertyAnimatorListener);
if (mOnStateChangedListener != null) {
mOnStateChangedListener.onChanged(false);
}
} else if ((dyConsumed < 0 || dyUnconsumed < 0) && child.getVisibility() != View.VISIBLE) {
AnimatorUtil.scaleShow(child, null);
if (mOnStateChangedListener != null) {
mOnStateChangedListener.onChanged(true);
}
}
}
public void setOnStateChangedListener(OnStateChangedListener mOnStateChangedListener) {
this.mOnStateChangedListener = mOnStateChangedListener;
}
// 外部監聽顯示和隱藏。
public interface OnStateChangedListener {
void onChanged(boolean isShow);
}
public static <V extends View> ScaleDownShowBehavior from(V view) {
ViewGroup.LayoutParams params = view.getLayoutParams();
if (!(params instanceof CoordinatorLayout.LayoutParams)) {
throw new IllegalArgumentException("The view is not a child of CoordinatorLayout");
}
CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params).getBehavior();
if (!(behavior instanceof ScaleDownShowBehavior)) {
throw new IllegalArgumentException("The view is not associated with ScaleDownShowBehavior");
}
return (ScaleDownShowBehavior) behavior;
}
private ViewPropertyAnimatorListener viewPropertyAnimatorListener = new ViewPropertyAnimatorListener() {
@Override
public void onAnimationStart(View view) {
isAnimatingOut = true;
}
@Override
public void onAnimationEnd(View view) {
isAnimatingOut = false;
view.setVisibility(View.GONE);
}
@Override
public void onAnimationCancel(View arg0) {
isAnimatingOut = false;
}
};
}
思路這里就不詳細展開了,因為前面在講解 仿知乎效果的時候已經講過了,大概就是根據不同的滑動行為執行不同的動畫 而已
題外話
- 通過這篇文章,熟悉 CoordinatorLayout 的 各種用法,同時也初步理解了自定義Behavior的思路
- 同時復習了動畫的相關知識
來自:http://www.jianshu.com/p/c174edcce58d