Material之Behavior實現支付寶密碼彈窗 仿淘寶/天貓商品屬性選擇
今天的效果在支付寶、淘寶、京東等電商App中很常見。比如支付寶輸入密碼彈窗、商城下單時選擇商品屬性時,從下面浮動上來一個PopupWindow
,那麼今天就帶大家用Behavior
來實現這兩個效果,結果你會發現簡直只需要一行程式碼。
總結下現在用的APP:
1. 仿支付寶彈出的輸入支付密碼視窗。
2. 仿淘寶/天貓彈出商品屬性選擇框。
3. 知乎首頁上下滑動隱藏ToolBar和NavigationBar。
4. …
效果預覽
引文
在我的技術群裡有小夥伴們討論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
和一個Button
:sheet 顯示/隱藏
,然後android:id="@+id/tab_layout"
這個佈局是橫向的,給它設定了Behavior
:app: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_EXPANDED
和STATE_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)將不會繼續往上滑動,下面的內容會繼續跟著出來,但是同樣可以向下滑動隱藏,也可以呼叫dismiss
和close
關閉。
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
關閉後不能再次開啟的問題了。
2016.12.11更新:我的程式碼發現用的是23的,如果是用的是24或者以上的話要在xml中加app:behavior_hideable=”true”的屬性,sheet的隱藏和顯示也要用STATE_EXPANDED 和 STATE_HIDDEN這兩個狀態,這一點感謝狂愛柚子茶同學的反饋。