Material Design系列,Behavior之BottomSheetBehavior與BottomSheetDialog

版權聲明:轉載必須註明本文轉自嚴振杰的博客: http://blog.csdn.net/yanzhenjie1003

今天的效果在支付寶、淘寶、京東等電商App中很常見。比如支付寶輸入密碼彈窗、商城下單時選擇商品屬性時,從下面浮動上來一個PopupWindow,那麼今天就帶大家用Behavior來實現這兩個效果,結果你會發現簡直只需要一行代碼。

總結下現在用的APP:
1. 仿支付寶彈出的輸入支付密碼窗口。
2. 仿淘寶/天貓彈出商品屬性選擇框。
3. 知乎首頁上下滑動隱藏ToolBar和NavigationBar。
4. …

系列博客:
1. Material Design系列,Behavior之BottomSheetBehavior與BottomSheetDialog
2. Material Design系列,Behavior之SwipeDismissBehavior
3. Material Design系列,自定義Behavior之上滑顯示返回頂部按鈕
4. Material Design系列,自定義Behavior實現Android知乎首頁
5. Material Design系列,自定義Behavior支持所有 View

效果預覽

效果預覽

源碼下載:http://download.csdn.net/detail/yanzhenjie1003/9578770,推薦先閱讀博客理解原理。

引文

在我的技術羣裏有小夥伴們討論Behavior,我也去玩了玩,我也對Behavior寫了系列博客。選中Behavior然後ctrol + t後發現Behavior的一個實現類:BottomSheetBehavior,我就到Android官網上翻了下資料,一翻就發現了驚喜啊,下面就把這些驚喜介紹給大家。

更多文章請Google/百度搜索我名字:嚴振杰,排名第一的就是我。

BottomSheetBehavior怎麼玩(知乎Bottom隱藏和顯示)

玩這個東西,首先Behavior作爲CoordinatorLayout的子View的LayoutParams(原因看後文解釋),所以CoordinatorLayout是萬萬不能少的,先來亮出整個佈局:

<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.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:popupTheme="@style/AppTheme.PopupOverlay" />
    </android.support.design.widget.AppBarLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/tab_layout"
        android:gravity="center"
        android:orientation="vertical"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <Button
            android:id="@+id/btn_bottom_sheet_control"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="sheet 顯示/隱藏" />

    </LinearLayout>

    <LinearLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="?actionBarSize"
        android:layout_alignParentBottom="true"
        android:background="@android:color/holo_purple"
        app:layout_behavior="@string/bottom_sheet_behavior">

        <Button
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:text="第一" />

        <Button
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:text="第二" />

        <Button
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:text="第三" />

        <Button
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:text="第四" />
    </LinearLayout>

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

大概介紹下,頁面上只能看到Toolbar和一個Buttonsheet 顯示/隱藏,然後android:id="@+id/tab_layout"這個佈局是橫向的,給它設置了Behaviorapp:layout_behavior="@string/bottom_sheet_behavior",經過測試發現,如果不給tab_layout設置BottomSheetBehavior,它會浮動在整個頁面的頂部,並在Toolbar的下面。設置了BottomSheetBehavior它會被BottomSheetBehavior自動移動到頁面底部外邊,所以在頁面上是看不到android:id="@+id/tab_layout"這個佈局的。

頁面畫好了,難道它會自動開關嗎,怎麼去控制它的打開和關閉呢?那麼我們就來看看這貨的真實面貌,經過我看Android的官方api發現,BottomSheetBehavior這個貨有一個靜態方法BottomSheetBehavior.from(View),會返回這個View引用的BottomSheetBehavior

public static <V extends View> BottomSheetBehavior<V> from(V view) {
    ViewGroup.LayoutParams params = view.getLayoutParams();
    if (!(params instanceof CoordinatorLayout.LayoutParams)) {
        throw new Exception("The view is not a child of CoordinatorLayout");
    }
    CoordinatorLayout.Behavior behavior = params.getBehavior();
    if (!(behavior instanceof BottomSheetBehavior)) {
        throw new IllegalArgumentException("...");
    }
    return (BottomSheetBehavior<V>) behavior;
}

這個方法會檢查這個View是否是CoordinatorLayout的子View,如果是纔會去拿到這個View的Behavior,所以諸位也應該明白爲什麼我開頭說Behavior作爲CoordinatorLayout子View的LayoutParams了。

接下來我們看看拿到這個貨後怎麼用:

private BottomSheetBehavior mBottomSheetBehavior;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.bsbehavior_activity);
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);

    findViewById(R.id.btn_bottom_sheet_control).setOnClickListener(onClickListener);
    // 拿到這個tab_layout對應的BottomSheetBehavior
    mBottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.tab_layout));
}

findViewById(R.id.btn_bottom_sheet_control).setOnClickListener(onClickListener);是給剛纔說的頁面中的Button設置了監聽,我們用這個按鈕來控制tab_layout的顯示和隱藏。

我發現有一個方法可以獲取到它所依附的View此時的狀態:mBottomSheetBehavior.getState(),翻閱了源碼後發現它的返回值有以下幾種:

/**
 * The bottom sheet is dragging.
 */
public static final int STATE_DRAGGING = 1;

/**
 * The bottom sheet is settling.
 */
public static final int STATE_SETTLING = 2;

/**
 * The bottom sheet is expanded.
 */
public static final int STATE_EXPANDED = 3;

/**
 * The bottom sheet is collapsed.
 */
public static final int STATE_COLLAPSED = 4;

/**
 * The bottom sheet is hidden.
 */
public static final int STATE_HIDDEN = 5;

當我看到STATE_EXPANDEDSTATE_COLLAPSED就明白了它的用法了,不就是展開和隱藏起來了麼?所以我們判斷這個狀態,如果是隱藏就顯示,如果是顯示就隱藏:

@Override
public void onClick(View v) {
    if (v.getId() == R.id.btn_bottom_sheet_control) {
        if (mBottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
            mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
        } else if (mBottomSheetBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {
            mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
        }
    }
}

到這裏,知乎首頁的Bottom的隱藏和顯示也就講玩了,接下來我們來看看支付寶淘寶的下方彈窗如何實現。

BottomSheetDialog怎麼玩(商城下單商品屬性選擇彈窗)

這個類的發現也是在Android官網搜索BottomSheetBehavior時發現的,一看到BottomSheetDialog後心中狂喜,後來經過我驗證,它顯示的效果和我猜想的一模一樣啊,既然是個Dialog,那麼用法應該和普通Dialog沒啥去區別了吧。

然後我就順勢new了一個BottomSheetDialog

private BottomSheetDialog mBottomSheetDialog;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...

    createBottomSheetDialog();
}

private void createBottomSheetDialog() {
    mBottomSheetDialog = new BottomSheetDialog(this);
    View view = LayoutInflater.from(this).inflate(R.layout.dialog_bottom_sheet, null, false);
    mBottomSheetDialog.setContentView(view);

    RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recyclerView);
    ...
    recyclerView.setAdapter(adapter);
}

View裏面是一個RecyclerView

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

然後用下面的代碼去控制它的顯示和隱藏。

if (mBottomSheetDialog.isShowing()) {
    mBottomSheetDialog.dismiss();
} else {
    mBottomSheetDialog.show();
}

當這個Dialog Show出來的時候發現它顯示了一半,嗯這個效果確實不錯,這樣就達到了我們最初說的支付寶密碼彈窗和淘寶/天貓商品屬性選擇。我們滑動的時候如果下面有內容它就會EXPANDED,如果是一個普通的View(非RecyclerView、NestedScrollView)將不會繼續往上滑動,下面的內容會繼續跟着出來,但是同樣可以向下滑動隱藏,也可以調用dismissclose關閉。

BottomSheetDialog的神坑

作爲一個有情懷的程序員,這裏把我踩過的坑和解決方案跟大家分享一下。

我發現當這個Dilaog打開再關閉後,無法用Dialog.show()再次打開,爲什麼呢?

我去閱讀了一下BottomSheetDialog源代碼,發現瞭如下代碼:

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

private View wrapInBottomSheet(int layoutResId, View view, ViewGroup.LayoutParams params) {
    final CoordinatorLayout coordinator = View.inflate(getContext(),R.layout...., null);
    FrameLayout bottomSheet = (FrameLayout) coordinator.findViewById(R.id.design_bottom_sheet);
    BottomSheetBehavior.from(bottomSheet).setBottomSheetCallback(mBottomSheetCallback);
    ...
    return coordinator;
}

private BottomSheetCallback mBottomSheetCallback = new BottomSheetCallback() {
    @Override
    public void onStateChanged(@NonNull View bottomSheet, int newState) {
        if (newState == BottomSheetBehavior.STATE_HIDDEN) {
            dismiss();
        }
    }

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

也就是說,系統的BottomSheetDialog是基於BottomSheetBehavior封裝的,這裏判斷了當我們滑動隱藏了BottomSheetBehavior中的View後,內部是設置了BottomSheetBehavior的狀態爲STATE_HIDDEN,接着它替我們關閉了Dialog,所以我們再次調用dialog.show()的時候Dialog沒法再此打開狀態爲HIDE的dialog了。

這裏就有個疑問了:
Google爲啥沒有提供我們自己設置BottomSheetCallback的接口呢?

沒有關係,看了源碼發現很簡單,我們自己來實現,並且在監聽到用戶滑動關閉BottomSheetDialog後,我們把BottomSheetBehavior的狀態設置爲BottomSheetBehavior.STATE_COLLAPSED,也就是半個打開狀態(BottomSheetBehavior.STATE_EXPANDED爲全打開),根據源碼我把設置的方法提供下:

private void setBehaviorCallback() {
    View view = mBottomSheetDialog.getDelegate().findViewById(android.support.design.R.id.design_bottom_sheet);
    final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(view);
    bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
        @Override
        public void onStateChanged(@NonNull View bottomSheet, int newState) {
            if (newState == BottomSheetBehavior.STATE_HIDDEN) {
                mBottomSheetDialog.dismiss();
                bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
            }
        }
        @Override
        public void onSlide(@NonNull View bottomSheet, float slideOffset) {
        }
    });
}

這樣就解決了BottomSheetDialog關閉後不能再次打開的問題了。

源碼下載:http://download.csdn.net/detail/yanzhenjie1003/9578770

版權聲明:轉載必須註明本文轉自嚴振杰的博客: http://blog.csdn.net/yanzhenjie1003

發佈了52 篇原創文章 · 獲贊 1019 · 訪問量 159萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章