自定義Behavior —— 仿知乎,FloatActionButton隱藏與展示

rsxd4147 8年前發布 | 20K 次閱讀 安卓開發 Android開發 移動開發

前段時間寫了一篇博客 使用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

 

 本文由用戶 rsxd4147 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!