Material Design 之 Behavior的使用和自定義Behavior

寫在前面

Material 系列文章:
Material Design 之 Toolbar 開發(fā)實踐總結(jié)
Material Design之 AppbarLayout 開發(fā)實踐總結(jié)

前面兩篇文章講了Toolbar 和 AppbarLayout 相關(guān)的東西嗜诀,還沒看過的同學(xué)可以去看看却特。前面我們說過琉挖,CoordinatorLayout很強大凌受,它可以協(xié)調(diào)子View的交互動作店雅,那么CoordinatorLayout它是怎么協(xié)調(diào)子View的呢政基?其實核心就是Behavior。那么今天講的就是這個很重要的東西-Behavior,在上面篇文章中闹啦,我們其實已經(jīng)看到過Behavior這個東西了沮明,在AppbarLayout 與NestedScrollView 聯(lián)動的時候,我們?yōu)镹estedScrollView設(shè)置了一個Behavior, 通過app:layout_behavior="@string/appbar_scrolling_view_behavior"窍奋,它的值是一個類的全路徑荐健,這個Behavior 是Google已經(jīng)為我們提供的,AppbarLayout的內(nèi)部類琳袄,專門用于處理可滾動View(如:ScrollView江场、RecyclerView) 與AppbarLayout 聯(lián)動的。那么這篇文章我們通過介紹Google提供的一些Behavior 的使用場景窖逗、使用方式和自定義Behavior 來熟悉和掌握 Behavior扛稽。

本文目錄:

  • Behavior 介紹
  • BottomSheetBehavior/BottomSheetDialog 的使用
  • SwipeDissmissBehavior 的使用
  • 自定義 Behavior

正文

1,Behavior 介紹

看一下官方的介紹:Interaction behavior plugin for child views of CoordinatorLayout. 作用于CoordinatorLayout的子View的交互行為插件。一個Behavior 實現(xiàn)了用戶的一個或者多個交互行為滑负,它們可能包括拖拽在张、滑動用含、快滑或者其他一些手勢。

Behavior 是一個頂層抽象類帮匾,其他的一些具體行為的Behavior 都是繼承自這個類啄骇。它提供了幾個重要的方法:

  • layoutDependsOn
  • onDependentViewChanged
  • onStartNestedScroll
  • onNestedPreScroll
  • onNestedScroll
  • onStopNestedScroll
  • onNestedScrollAccepted
  • onNestedPreFling
  • onStartNestedScroll
  • onLayoutChild

解釋一下上面幾個方法和它們的調(diào)用時機(jī):

 /**
     * 表示是否給應(yīng)用了Behavior 的View 指定一個依賴的布局,通常瘟斜,當(dāng)依賴的View 布局發(fā)生變化時
     * 不管被被依賴View 的順序怎樣缸夹,被依賴的View也會重新布局
     * @param parent
     * @param child 綁定behavior 的View
     * @param dependency   依賴的view
     * @return 如果child 是依賴的指定的View 返回true,否則返回false
     */
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return super.layoutDependsOn(parent, child, dependency);
    }

    /**
     * 當(dāng)被依賴的View 狀態(tài)(如:位置、大新菥洹)發(fā)生變化時虽惭,這個方法被調(diào)用
     * @param parent
     * @param child
     * @param dependency
     * @return
     */
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        return super.onDependentViewChanged(parent, child, dependency);
    }

    /**
     *  當(dāng)coordinatorLayout 的子View試圖開始嵌套滑動的時候被調(diào)用。當(dāng)返回值為true的時候表明
     *  coordinatorLayout 充當(dāng)nested scroll parent 處理這次滑動蛇尚,需要注意的是只有當(dāng)返回值為true
     *  的時候芽唇,Behavior 才能收到后面的一些nested scroll 事件回調(diào)(如:onNestedPreScroll、onNestedScroll等)
     *  這個方法有個重要的參數(shù)nestedScrollAxes取劫,表明處理的滑動的方向匆笤。
     *
     * @param coordinatorLayout 和Behavior 綁定的View的父CoordinatorLayout
     * @param child  和Behavior 綁定的View
     * @param directTargetChild
     * @param target
     * @param nestedScrollAxes 嵌套滑動 應(yīng)用的滑動方向,看 {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
     *                         {@link ViewCompat#SCROLL_AXIS_VERTICAL}
     * @return
     */
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

    /**
     * 嵌套滾動發(fā)生之前被調(diào)用
     * 在nested scroll child 消費掉自己的滾動距離之前谱邪,嵌套滾動每次被nested scroll child
     * 更新都會調(diào)用onNestedPreScroll炮捧。注意有個重要的參數(shù)consumed,可以修改這個數(shù)組表示你消費
     * 了多少距離惦银。假設(shè)用戶滑動了100px,child 做了90px 的位移咆课,你需要把 consumed[1]的值改成90,
     * 這樣coordinatorLayout就能知道只處理剩下的10px的滾動扯俱。
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param dx  用戶水平方向的滾動距離
     * @param dy  用戶豎直方向的滾動距離
     * @param consumed
     */
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    }

    /**
     * 進(jìn)行嵌套滾動時被調(diào)用
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param dxConsumed target 已經(jīng)消費的x方向的距離
     * @param dyConsumed target 已經(jīng)消費的y方向的距離
     * @param dxUnconsumed x 方向剩下的滾動距離
     * @param dyUnconsumed y 方向剩下的滾動距離
     */
    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
    }

    /**
     *  嵌套滾動結(jié)束時被調(diào)用傀蚌,這是一個清除滾動狀態(tài)等的好時機(jī)。
     * @param coordinatorLayout
     * @param child
     * @param target
     */
    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
        super.onStopNestedScroll(coordinatorLayout, child, target);
    }

    /**
     * onStartNestedScroll返回true才會觸發(fā)這個方法蘸吓,接受滾動處理后回調(diào)善炫,可以在這個
     * 方法里做一些準(zhǔn)備工作,如一些狀態(tài)的重置等库继。
     * @param coordinatorLayout
     * @param child
     * @param directTargetChild
     * @param target
     * @param nestedScrollAxes
     */
    @Override
    public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

    /**
     * 用戶松開手指并且會發(fā)生慣性動作之前調(diào)用箩艺,參數(shù)提供了速度信息,可以根據(jù)這些速度信息
     * 決定最終狀態(tài)宪萄,比如滾動Header艺谆,是讓Header處于展開狀態(tài)還是折疊狀態(tài)。返回true 表
     * 示消費了fling.
     *
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param velocityX x 方向的速度
     * @param velocityY y 方向的速度
     * @return
     */
    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
        return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
    }

    //可以重寫這個方法對子View 進(jìn)行重新布局
    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        return super.onLayoutChild(parent, child, layoutDirection);
    }

以上就是Behavior的一些重要方法拜英,當(dāng)我們要自定義一個Behavior的時候静汤,就會去重寫上面的一些方法。自定義Behavior 會放在文章最后講。對Behavior 有了一些了解后虫给,接下來我們看一下Google給我提供了一些特殊場景的Behavior藤抡。

2,BottomSheetBehavior/BottomSheetDialog 的使用

BottomSheetBehavior 實現(xiàn)的效果在我們的項目中用的比較多,它就是從底部彈出一個布局抹估,在很多的應(yīng)用中缠黍,分享功能都有這樣一個交互。在以前我們通常都是用PopupWindow來搞定药蜻,前面也寫了一篇文章了瓷式,關(guān)于PupupWindow的使用和封裝,通用PopupWindow语泽,幾行代碼搞定PopupWindow彈窗,有了BottomSheetBehavior 實現(xiàn)起來就簡單一點了贸典。請看效果圖:

bottomSheetBehavior.gif

看看怎么用BottomSheetBehavior:
1,在xml布局文件中為需要從底部彈出的布局綁定BottomSheetBehavior,代碼如下:

<?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">
   <TextView
       android:id="@+id/btn_show_bottom_sheet"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="顯示/隱藏 BottomSheet"
       android:background="@android:color/darker_gray"
       android:textColor="@color/black"
       android:padding="10dp"
       />
 <FrameLayout
     android:id="@+id/share_view"
     app:layout_behavior="@string/bottom_sheet_behavior"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:background="@android:color/white"
     android:orientation="vertical"
     app:behavior_peekHeight="0dp"
     >
     <include layout="@layout/bottom_sheet_share_dialog"/>
 </FrameLayout>

</android.support.design.widget.CoordinatorLayout>

注意上面這行代碼: app:behavior_peekHeight="0dp",peekHeight 屬性是設(shè)置bottomSheet 折疊時的高度踱卵,我們設(shè)置為0表示折疊的時候完全隱藏廊驼,默認(rèn)情況時顯示布局的高度,布局會顯示在界面颊埃,所以蔬充,如果要一開始布局不顯示在界面上的話蝶俱,需要將peekHeight 設(shè)置為0班利。也可以在代碼中設(shè)置, 通過sheetBehavior.setPeekHeight(0)榨呆。

2罗标,在代碼中獲取到與布局相關(guān)聯(lián)的BottomSheetBehavior,設(shè)置展開與折疊的狀態(tài)就可以了,BottomSheetBehavior有5種狀態(tài):

1, STATE_EXPANDED 展開狀態(tài)积蜻,顯示完整布局闯割。
2,STATE_COLLAPSED 折疊狀態(tài)竿拆,顯示peekHeigth 的高度宙拉,如果peekHeight為0,則全部隱藏,與STATE_HIDDEN效果一樣丙笋。
3谢澈,STATE_DRAGGING 拖拽時的狀態(tài)
4,STATE_HIDDEN 隱藏時的狀態(tài)
5御板,STATE_SETTLING 釋放時的狀態(tài)

看代碼:

 View shareView = findViewById(R.id.share_view);
        //獲取BottomSheetBehavior
        final BottomSheetBehavior sheetBehavior = BottomSheetBehavior.from(shareView);
        
        //設(shè)置折疊時的高度
        //sheetBehavior.setPeekHeight(BottomSheetBehavior.PEEK_HEIGHT_AUTO);
        
        //監(jiān)聽BottomSheetBehavior 狀態(tài)的變化
        sheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
            @Override
            public void onStateChanged(@NonNull View bottomSheet, int newState) {

            }

            @Override
            public void onSlide(@NonNull View bottomSheet, float slideOffset) {

            }
        });
        //下滑的時候是否可以隱藏
        sheetBehavior.setHideable(true);
        findViewById(R.id.btn_show_bottom_sheet).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(sheetBehavior.getState() != BottomSheetBehavior.STATE_EXPANDED){
                    sheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
                }else {
                    sheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
                }

            }
        });

代碼很簡單锥忿,重要的就是通過方法 sheetBehavior.setState()來改變狀態(tài),是顯示還是隱藏怠肋。其他的幾個方法都添加了注釋敬鬓,不用多講。

2.1, BottomSheetDialog

上面說了BottomSheetBehavior, 接下來看一下BottomSheetDialog, 一看名字就知道,它就是一個Dialog钉答,使用方法和Dialog 一樣础芍,它是對BootomSheetBehavior 進(jìn)行了包裝,從底部彈出一個Dialog希痴。BottomSheetDialog 使用起來比BottomSheetBahvior更方便者甲,效果更佳∑龃矗看一下它的源碼也非常簡單虏缸,就是Dialog 顯示的布局綁定了BottomSheeBehavior,源碼如下:

 private View wrapInBottomSheet(int layoutResId, View view, ViewGroup.LayoutParams params) {
        final CoordinatorLayout coordinator = (CoordinatorLayout) View.inflate(getContext(),
                R.layout.design_bottom_sheet_dialog, null);
        if (layoutResId != 0 && view == null) {
            view = getLayoutInflater().inflate(layoutResId, coordinator, false);
        }
        FrameLayout bottomSheet = (FrameLayout) coordinator.findViewById(R.id.design_bottom_sheet);
        mBehavior = BottomSheetBehavior.from(bottomSheet);
        mBehavior.setBottomSheetCallback(mBottomSheetCallback);
        mBehavior.setHideable(mCancelable);
        if (params == null) {
            bottomSheet.addView(view);
        } else {
            bottomSheet.addView(view, params);
        }
        // We treat the CoordinatorLayout as outside the dialog though it is technically inside
        coordinator.findViewById(R.id.touch_outside).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (mCancelable && isShowing() && shouldWindowCloseOnTouchOutside()) {
                    cancel();
                }
            }
        });
        return coordinator;
    }

就這樣一個方法,獲取到Behavior,設(shè)置了一個監(jiān)聽狀態(tài)的回調(diào)嫩实,設(shè)置了下滑可以隱藏刽辙。然后將Dialog 顯示的布局添加到了綁定了BottomSheetBehavior 的ViewGroup 里。這個方法在setContent()方法被調(diào)用:

 @Override
    public void setContentView(View view) {
        super.setContentView(wrapInBottomSheet(0, view, null));
    }

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        super.setContentView(wrapInBottomSheet(0, view, params));
    }

接下來看一下使用方法甲献,非常簡單宰缤,以網(wǎng)易云音樂的歌單和分享UI為例:
網(wǎng)易云音樂歌單UI效果 如下:

網(wǎng)易云音樂歌單.png

來張gif圖效果更清楚:

網(wǎng)易云音樂效果圖.gif

本文通過BottomSheetDialog 實現(xiàn)的效果圖如下:

bottomSheetDialog.gif

歌單代碼如下:

private void showBottomSheetDialog(){
        BottomSheetDialog dialog = new BottomSheetDialog(this);
        View view = LayoutInflater.from(this).inflate(R.layout.bottom_sheet_dialog,null);

        handleList(view);

        dialog.setContentView(view);
        dialog.setCancelable(true);
        dialog.setCanceledOnTouchOutside(true);
        dialog.show();
    }

    private void handleList(View contentView){
        RecyclerView recyclerView = (RecyclerView) contentView.findViewById(R.id.recyclerView);
        LinearLayoutManager manager = new LinearLayoutManager(this);
        manager.setOrientation(LinearLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(manager);
        MusicAdapter adapter = new MusicAdapter();
        recyclerView.setAdapter(adapter);
        adapter.setData(mockData());
        adapter.notifyDataSetChanged();
    }

分享代碼如下:

/**
     * share Dialog
     */
    private void showShareDialog(){
        if(mBottomSheetDialog == null){
            mBottomSheetDialog = new BottomSheetDialog(this);
            View view = LayoutInflater.from(this).inflate(R.layout.bottom_sheet_share_dialog,null);
            mBottomSheetDialog.setContentView(view);
            mBottomSheetDialog.setCancelable(true);
            mBottomSheetDialog.setCanceledOnTouchOutside(true);
            // 解決下滑隱藏dialog 后,再次調(diào)用show 方法顯示時晃洒,不能彈出Dialog
            View view1 = mBottomSheetDialog.getDelegate().findViewById(android.support.design.R.id.design_bottom_sheet);
            final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(view1);
            bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
                @Override
                public void onStateChanged(@NonNull View bottomSheet, int newState) {
                    if (newState == BottomSheetBehavior.STATE_HIDDEN) {
                        Log.i("BottomSheet","onStateChanged");
                        mBottomSheetDialog.dismiss();
                        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
                    }
                }

                @Override
                public void onSlide(@NonNull View bottomSheet, float slideOffset) {

                }
            });
        }else{
            mBottomSheetDialog.show();
        }

    }

代碼很簡單慨灭,和其他普通Dialog的用法一樣。值的主意的一點是這里有個bug ,那就是當(dāng)你下滑隱藏了Dialog 之后球及,下次直接調(diào)用show方法來顯示Dialog時(沒有重新new 的情況下)氧骤,Dialog不能顯示,原因是因為BottomSheetDialog 源碼中吃引,關(guān)閉的Dialog 是依賴BottomSheetBehavior 的筹陵,當(dāng)下滑隱藏的時候,BottomSheet的狀態(tài)也為STATE_HIDDEN镊尺,并且同時dismiss Dialog,下次show 的時候朦佩,是沒有辦法顯示一個狀態(tài)為STATE_HIDDEN 的布局的。 網(wǎng)上搜了一下庐氮,有很多人都碰到過语稠,解決方法來自這篇文章Material之Behavior實現(xiàn)支付寶密碼彈窗 仿淘寶/天貓商品屬性選擇, 解決思路:獲取到BottomSheetDialog 的布局,然后拿到綁定的BottomSheetBehavior,重新設(shè)置監(jiān)聽弄砍,在調(diào)用dismiss 方法時仙畦,我們重新設(shè)置一些Behavior 的狀態(tài)。代碼如下:

            // 解決下滑隱藏dialog 后输枯,再次調(diào)用show 方法顯示時议泵,不能彈出Dialog
            View view1 = mBottomSheetDialog.getDelegate().findViewById(android.support.design.R.id.design_bottom_sheet);
            final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(view1);
            bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
                @Override
                public void onStateChanged(@NonNull View bottomSheet, int newState) {
                    if (newState == BottomSheetBehavior.STATE_HIDDEN) {
                        Log.i("BottomSheet","onStateChanged");
                        mBottomSheetDialog.dismiss();
                        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
                    }
                }

                @Override
                public void onSlide(@NonNull View bottomSheet, float slideOffset) {

                }
            });

以上就是BottomSheetBehavior 和BottomSheetDialog 的用法。

3桃熄,SwipeDissmissBehavior 的使用

上面講了BottomSheetBehavior 和BottomSheetDialog 的用法,接下來看另一種場景的Behavior-SwipeDissmissBehavior先口,叫滑動消失或者滑動關(guān)閉型奥,這個Behavior 在我們項目中用得可能就不是很多了。有個場景就是Snackbar的使用了碉京,Android 5.0 以上 厢汹,增加了Snackbar提示消息,Snackbar 的Behavior 的就是 SwipeDissmissBehavior 的應(yīng)用谐宙,當(dāng)滑動Snackbar 的時候烫葬,Snackbar 消失,效果如下:

snackbar的behavir.gif

使用也非常簡單凡蜻,在代碼中只接new 一個SwipeDismissBehavior搭综,設(shè)置一些屬性后,添加到CoordinatorLayout.LayoutParams划栓,代碼如下:

        mSwipeLayout = findViewById(R.id.swipe_layout);
        SwipeDismissBehavior swipe = new SwipeDismissBehavior();

        /**
         * //設(shè)置滑動的方向兑巾,有3個值
         *
         * 1,SWIPE_DIRECTION_ANY 表示向左像右滑動都可以忠荞,
         * 2蒋歌,SWIPE_DIRECTION_START_TO_END,只能從左向右滑
         * 3委煤,SWIPE_DIRECTION_END_TO_START堂油,只能從右向左滑
         */
        swipe.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);

        swipe.setStartAlphaSwipeDistance(0f);

        swipe.setSensitivity(0.2f);

        swipe.setListener(new SwipeDismissBehavior.OnDismissListener() {
            @Override
            public void onDismiss(View view) {
                Log.e(TAG,"------>onDissmiss");
            }

            @Override
            public void onDragStateChanged(int state) {
                Log.e(TAG,"------>onDragStateChanged");
            }
        });

        CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) mSwipeLayout.getLayoutParams();
        if(layoutParams!=null){
            layoutParams.setBehavior(swipe);
        }

有兩個重要的方法,wipe.setSwipeDirection設(shè)置滑動方向碧绞,有三個取值府框,上面已經(jīng)注釋,不過多解釋头遭,還有就是swipe.setListener可以監(jiān)聽dissmiss 和狀態(tài)改變寓免,在這些回調(diào)里面可以做一些自己的邏輯癣诱。最后效果圖:

swipeDissmissBehavir.gif
4,自定義Behavior

上面講了Google 為我們提供的一些場景使用的Behavior,當(dāng)然還有一些Google 提供的一些組件使用的Behavior,AppbarLayout內(nèi)部的Behavior,如專門協(xié)調(diào) AppbarLayout 與可滾動View(NestedScrollView,RecyclerView )的, FloatActionButton內(nèi)部的Behavior ,協(xié)調(diào)和Snackbar 的關(guān)系计维,保證Snackbar 彈出的時候不被FAB 遮擋。還有就是上面說的Snackbar內(nèi)部的Behavior 等等撕予。但是有時候鲫惶,要實現(xiàn)多個View之間的的交互時,我們可以自定義Behavior 实抡,下面就說說怎么自定義一個Behavior欠母。

自定義Behavior 最關(guān)鍵的就是文章第一部分介紹的Behavior 提供的那一些方法,忘了的請到回去看一下第一部分的方法注釋吆寨。自定義Behavior 分為兩種:

  • 第一種是通過監(jiān)聽一個View的狀態(tài)赏淌,如位置、大小的變化,來改變其他View的行為,這種只需要重寫2個方法就可以了报账,分別是layoutDependsOnonDependentViewChanged, layoutDependsOn方法判斷是指定依賴的View時鲤氢,返回true,然后在onDependentViewChanged 里恨搓,被依賴的View做需要的行為動作照瘾。

  • 第二種就是重寫onStartNestedScroll梅垄、onNestedPreScroll落剪、onNestedScroll等一系列方法想帅,前面第一步分已經(jīng)講過场靴。

上面兩種方法相比,第一種很簡單港准,第二種復(fù)雜一些旨剥,但是第二種實現(xiàn)的效果也要復(fù)雜。下面就以開眼首頁的滑動Header效果為例浅缸,來實現(xiàn)一個自定義的Behavior泞边。開眼首頁滑動header效果如下:


開眼首頁效果.gif

效果如上:就是列表滑動的時候是覆蓋Header(不是Header縮小,Header沒動)疗杉,然后就是Header有一個alpha 的變化阵谚。

1,首先是整個布局烟具,Header 固定在頂部梢什,列表在Header 的下方,CoordinatorLayout 是一個FrameLayout,不能提供這樣的布局朝聋,我們需要重寫onLayoutChild 來讓列表位于Header下面:


    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        Log.i(TAG,"onLayoutChild.....");
        CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
        if(params!=null && params.height == CoordinatorLayout.LayoutParams.MATCH_PARENT){
            child.layout(0,0,parent.getWidth(),parent.getHeight());
            child.setTranslationY(getHeaderHeight());
            return true;
        }

        return super.onLayoutChild(parent, child, layoutDirection);
    }

我們需要知道Header的高度嗡午,將Header的高度寫在dimens文件中,getHeaderHeight()方法如下:

/**
     * 獲取Header 高度
     * @return
     */
    public int getHeaderHeight(){
        return MaterialDesignSimpleApplication.getAppContext().getResources().getDimensionPixelOffset(R.dimen.header_height);
    }

2冀痕,當(dāng)開始滑動的時候荔睹,利用setTranslationY 來移動列表,知道完全蓋住header ,這是時候言蛇,列表就不移動了僻他,只是列表的滑動了。當(dāng)下滑到頂端的時候腊尚,又將列表向下滑動吨拗,直到header 完全顯示,思路就是這樣婿斥。開眼的首頁向上滑洞的時候劝篷,Header 有一個alpha的變化,本例子沒有實現(xiàn)民宿,其實也很簡單娇妓,只要重寫onDependentViewChanged方法,在里面根據(jù)滑動距離算出alpha 變化的值就可以了活鹰。自定義Behavior 完整代碼如下:

/**
 *
 *   自定義Behavior :實現(xiàn)RecyclerView(或者其他可滑動View哈恰,如:NestedScrollView) 滑動覆蓋header 的效果
 * Created by zhouwei on 16/12/19.
 */

public class CoverHeaderScrollBehavior extends CoordinatorLayout.Behavior<View> {
    public static final String TAG = "CoverHeaderScroll";

    public CoverHeaderScrollBehavior(Context context, AttributeSet attributeSet){
        super(context,attributeSet);
    }


    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        Log.i(TAG,"onLayoutChild.....");
        CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
        if(params!=null && params.height == CoordinatorLayout.LayoutParams.MATCH_PARENT){
            child.layout(0,0,parent.getWidth(),parent.getHeight());
            child.setTranslationY(getHeaderHeight());
            return true;
        }

        return super.onLayoutChild(parent, child, layoutDirection);
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        // 在這個方法里面只處理向上滑動
        if(dy < 0){
            return;
        }

        float transY =  child.getTranslationY() - dy;
        Log.i(TAG,"transY:"+transY+"++++child.getTranslationY():"+child.getTranslationY()+"---->dy:"+dy);
        if(transY > 0){
            child.setTranslationY(transY);
            consumed[1]= dy;
        }
    }

    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
        // 在這個方法里只處理向下滑動
        if(dyUnconsumed >0){
            return;
        }

        float transY = child.getTranslationY() - dyUnconsumed;
        Log.i(TAG,"------>transY:"+transY+"****** child.getTranslationY():"+child.getTranslationY()+"--->dyUnconsumed"+dxUnconsumed);
        if(transY > 0 && transY < getHeaderHeight()){
            child.setTranslationY(transY);
        }
    }

    /**
     * 獲取Header 高度
     * @return
     */
    public int getHeaderHeight(){
        return MaterialDesignSimpleApplication.getAppContext().getResources().getDimensionPixelOffset(R.dimen.header_height);
    }

}

xml 的代碼如下:

<?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:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
   <ImageView
       android:layout_width="match_parent"
       android:layout_height="@dimen/header_height"
       android:scaleType="centerCrop"
       android:src="@drawable/meizhi"
       />
    <android.support.v4.widget.NestedScrollView
        android:id="@+id/nested_scroll_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/white"
        app:layout_behavior="@string/cover_header_behavior"
        >
        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="@string/large_text"
            />
    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>

最后實現(xiàn)的效果如下:

仿開眼首頁效果.gif

最后

以上就是關(guān)于Behavior 的全部內(nèi)容坟桅,自定義Behavior 這一塊,特別是處理滑動嵌套對于剛接觸的同學(xué)來說還是挺難的蕊蝗,不過當(dāng)掌握了之后仅乓,我們能做出很多炫酷的效果。所以蓬戚,再困難也值得花時間去學(xué)習(xí)夸楣。本文到此結(jié)束,如有問題子漩,歡迎交流豫喧。所有關(guān)于Material Design 的使用示例都在這里:MaterialDesignSamples

參考資料:
1,自定義Behavior的藝術(shù)探索-仿UC瀏覽器主頁
2,使用 CoordinatorLayout 實現(xiàn)復(fù)雜聯(lián)動效果
3,Material之Behavior實現(xiàn)支付寶密碼彈窗 仿淘寶/天貓商品屬性選擇

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市幢泼,隨后出現(xiàn)的幾起案子紧显,更是在濱河造成了極大的恐慌,老刑警劉巖缕棵,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件孵班,死亡現(xiàn)場離奇詭異,居然都是意外死亡招驴,警方通過查閱死者的電腦和手機(jī)篙程,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來别厘,“玉大人虱饿,你說我怎么就攤上這事〈ヅ浚” “怎么了氮发?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長冗懦。 經(jīng)常有香客問我爽冕,道長,這世上最難降的妖魔是什么批狐? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任扇售,我火速辦了婚禮前塔,結(jié)果婚禮上嚣艇,老公的妹妹穿的比我還像新娘。我一直安慰自己华弓,他們只是感情好食零,可當(dāng)我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著寂屏,像睡著了一般贰谣。 火紅的嫁衣襯著肌膚如雪娜搂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天吱抚,我揣著相機(jī)與錄音百宇,去河邊找鬼。 笑死秘豹,一個胖子當(dāng)著我的面吹牛携御,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播既绕,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼啄刹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了凄贩?” 一聲冷哼從身側(cè)響起誓军,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎疲扎,沒想到半個月后昵时,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡椒丧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年债查,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瓜挽。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡盹廷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出久橙,到底是詐尸還是另有隱情俄占,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布淆衷,位于F島的核電站缸榄,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏祝拯。R本人自食惡果不足惜甚带,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望佳头。 院中可真熱鬧鹰贵,春花似錦、人聲如沸康嘉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽亭珍。三九已至敷钾,卻和暖如春枝哄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背阻荒。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工挠锥, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人侨赡。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓瘪贱,卻偏偏與公主長得像,于是被迫代替她去往敵國和親辆毡。 傳聞我的和親對象是個殘疾皇子菜秦,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容