Material Design 系列之 CardView、FAB 和 Snackbar

依然範特稀西發表於2019-02-25

前言

更多Material Design 文章請看:
Material Design 之 Toolbar 開發實踐總結
Material Design之 AppbarLayout 開發實踐總結
Material Design 之 Behavior的使用和自定義Behavior
Material Design 之 TabLayout 使用
Material Design 之 TextInputLayout和TextInputEditText
這是Material Design 系列的最後一篇文章,前面幾篇文章講了Material Design中一些比較重要並且常用的控制元件,最後這一篇文章算是一個補充,講一下CardView、FloatActionButton 和 Snackbar。由於用法比較簡單,所以就不每一個都拎出來單講。以下分別是這三個控制元件的用法。

CardView(卡片)

卡片是一張帶有材料屬性的紙片,用作展示更多詳細資訊的入口點。卡片包含了一組特定的資料集,資料集含有各種相關資訊,如主題照片、文字,連結等等。卡片有固定的寬度和可變的高度。最大高度限制於可適應平臺上單一檢視的內容,但如果需要它可以臨時擴充套件(例如,顯示評論欄)。卡片不會翻轉以展示其背後的資訊。

卡片集是共面的,或者統一平面的多張卡片佈局。如下:

Material Design 系列之 CardView、FAB 和 Snackbar
card_collection_1.png

一張卡片包含了一組特定的資料集,考慮在以下這些情況使用卡片:

  • 作為一個集合,比較多種資料型別,比如:圖片、視訊和文字。

  • 不需要直接比較(如:使用者不會直接比較圖片和文字)

  • 支援內容高度可變,比如評論。

  • 包含響應按鈕,比如+1 按鈕或者評論

  • 要使用網格列表,但需要顯示更多文字來補充圖片

以上就是使用卡片的一些場景,看一個使用不當的例子(圖片來自官網):

Material Design 系列之 CardView、FAB 和 Snackbar
card_wrong.png

錯誤示例: 這種卡片的使用分散了使用者的注意力,不能快速瀏覽,也不能忽略掉,所以將這些內容放在不同的卡片上是難以理解的。其實國內有些知名APP也沒有按照規範來做,給我們做了錯誤的示範,如知乎日報首頁:

Material Design 系列之 CardView、FAB 和 Snackbar
zhihuribao.png

正確的用法如下:

Material Design 系列之 CardView、FAB 和 Snackbar
card_right.png

正確示例:可快速瀏覽的列表,用來代替卡片,是表現沒有許多操作的同類內容的合適方法。

以上就是Material Design 設計規範裡給的使用卡片的一些場景和正確使用方法,更多的設計規範請看:Material Design 官網

我們要怎麼實現卡片設計呢?Google 給我們提供了CardView,並且是像下相容的(L 以下仍然可以用)。CardView 的用法比較簡單,重要的屬性也就幾個。其實用CardView主要實現的圓角和陰影效果。看一下CardView的屬性:

  • app:cardBackgroundColor 設定卡片的背景色

  • app:cardCornerRadius 設定卡片的圓角

  • app:cardElevation 設定卡片的陰影

  • app:cardUseCompatPadding 是否新增padding

  • app:cardPreventCornerOverlap 在v20和v20以前的版本新增padding,防止CardView的內容和圓角相交

上面幾個屬性是CardView的幾個常用的屬性,當然也可以在程式碼中設定,呼叫CardView.setXXX就行

       mCardView = (CardView) findViewById(R.id.card_view);
        //設定背景
        mCardView.setCardBackgroundColor(getColor(R.color.colorPrimary));
        //設定圓角
        mCardView.setRadius(5);
        //設定陰影
        mCardView.setCardElevation(3);
        //設定 相容padding 
        mCardView.setUseCompatPadding(true);
        //
        mCardView.setPreventCornerOverlap(true);複製程式碼

比較簡單,就上面幾個屬性,都一一介紹了,看一些示例:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
              android:layout_margin="8dp"
    >
  <android.support.v7.widget.CardView
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      app:cardCornerRadius="3dp"
      app:cardElevation="3dp"
      app:cardUseCompatPadding="true"
      >
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:orientation="vertical"
         >
          <ImageView
              android:layout_width="match_parent"
              android:layout_height="300dp"
              android:scaleType="centerCrop"
              android:src="@drawable/meizhi"
              />
          <TextView
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:textSize="24sp"
              android:text="Material Design"
              android:textColor="@color/black"
              android:layout_marginTop="16dp"
              android:paddingRight="16dp"
              android:paddingLeft="16dp"
              />
          <TextView
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:textSize="16sp"
              android:layout_marginTop="6dp"
              android:paddingRight="16dp"
              android:paddingLeft="16dp"
              android:text=" material metaphor is the unifying theory of a rationalized space and a system of motion."
              />
          <LinearLayout
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:orientation="horizontal"
              android:padding="16sp"
              >

              <TextView
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:textColor="@color/orange"
                  android:textSize="24sp"
                  android:text="SHARE"
                  />
              <TextView
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:textColor="@color/orange"
                  android:textSize="24sp"
                  android:text="EXPLORE"
                  android:layout_marginLeft="20dp"
                  />
          </LinearLayout>
     </LinearLayout>
  </android.support.v7.widget.CardView>
  <android.support.v7.widget.CardView
      android:id="@+id/card_view"
      android:layout_width="match_parent"
      android:layout_height="100dp"
      app:cardBackgroundColor="@color/DarkCyan"
      app:cardUseCompatPadding="true"
      app:cardElevation="4dp"
      app:cardCornerRadius="5dp"
      >
      <TextView
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:text="Card"
          android:textColor="@color/white"
          android:textSize="20sp"
          android:gravity="center"
          />
  </android.support.v7.widget.CardView>
</LinearLayout>複製程式碼

效果如下:

Material Design 系列之 CardView、FAB 和 Snackbar
CardView.png

CardView 點選效果
Material Design 的設計就是為了更貼近現實生活中的場景,當點選之後,是會有反饋的,可以給CardView 新增點選效果。

 android:clickable="true"
  android:foreground="?attr/selectableItemBackground"複製程式碼

這樣點選時就會有波紋擴散效果了,增加體驗。

Material Design 系列之 CardView、FAB 和 Snackbar
cardView_click.gif

FloatingActionButton (浮動操作按鈕)

FloatingActionButton(浮動操作按鈕,以下簡稱FAB)適用於特定的進階操作。它是漂浮在 UI 上的一個圓形圖示,具有一些動態的效果,比如變形、彈出、位移等等。FAB有3種尺寸,預設尺寸、mini 尺寸和 auto 尺寸 。

預設尺寸:適用於大多數情況
mini 尺寸:僅用於建立與其他螢幕元素視覺的連續性。
auto: 基於Window(視窗)大小變化的,當視窗大小小於470dp,會選擇一個較小尺寸的button,更大一點的視窗就選擇更大的button

可以通過 fabSize 來控制FAB的size。因為FAB這個類繼承自ImageView,所以我們可以通過setImageDrawable() 方法來控制FAB icon 的顯示。FAB 預設的背景色是colorAccent,如果你想在執行時改變它的顏色,你可以呼叫方法setBackgroundTintList(ColorStateList)) 來改變。

介紹一下FAB的幾個屬性:

  • app:elevation 設定陰影
  • app:rippleColor 擴散效果的顏色

  • app:fabSize 設定 FAB 的 size

  • app:layout_anchor 設定錨點

  • app:useCompatPadding 相容padding 可用

屬性比較簡單,前面講Behavior 的時候提到過,FAB 和 AppbarLayout 的聯動和FAB和Snackbar的Behavior 確保Snackbar 從底部彈出時,不會遮擋FAB,而會相應的上移。這2個也是FAB 常用的場景,效果如下:

Material Design 系列之 CardView、FAB 和 Snackbar
FAB.gif

佈局如下:

<?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">
  <android.support.design.widget.AppBarLayout
      android:id="@+id/appbar_layout"
      android:layout_width="match_parent"
      android:layout_height="wrap_content">

     <android.support.design.widget.CollapsingToolbarLayout
         android:layout_width="match_parent"
         android:layout_height="200dp"
         app:layout_scrollFlags="scroll|exitUntilCollapsed"
         >
          <ImageView
           android:layout_width="match_parent"
           android:layout_height="200dp"
           android:src="@drawable/meizhi"
           android:scaleType="centerCrop"
           app:layout_collapseMode="parallax"
           />
          <android.support.v7.widget.Toolbar
              android:layout_width="match_parent"
              android:layout_height="?attr/actionBarSize"
              app:layout_collapseMode="pin"
              />
     </android.support.design.widget.CollapsingToolbarLayout>
  </android.support.design.widget.AppBarLayout>
    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="18dp"
            android:text="@string/large_text"/>

    </android.support.v4.widget.NestedScrollView>
  <android.support.design.widget.FloatingActionButton
      android:id="@+id/fab1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:src="@android:drawable/ic_dialog_email"
      android:layout_marginBottom="20dp"
      android:layout_marginRight="15dp"
      android:layout_gravity="bottom|right"
      app:rippleColor="@android:color/darker_gray"
      app:elevation="3dp"
      />
  <android.support.design.widget.FloatingActionButton
      android:id="@+id/fab2"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:src="@drawable/ic_book_list"
      android:layout_marginBottom="20dp"
      android:layout_marginRight="15dp"
      app:layout_anchor="@+id/appbar_layout"
      app:layout_anchorGravity="bottom|right"
      app:elevation="5dp"
      />
</android.support.design.widget.CoordinatorLayout>複製程式碼

程式碼中改變FAB 顏色,icon等:


        fab1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Snackbar.make(fab1,"點選fab1",Snackbar.LENGTH_LONG).show();
            }
        });

        fab1.setBackgroundTintList(ColorStateList.valueOf(getResources().getColor(R.color.colorPrimary)));


        fab2.setImageResource(R.drawable.ic_book_list);

        fab2.setCompatElevation(6);

        fab2.setSize(FloatingActionButton.SIZE_NORMAL);複製程式碼

還可以監聽FAB的隱藏或者顯示,在專案中可能會有這樣的需求,當FAB隱藏或者顯示之後,接下來做什麼操作,監聽程式碼如下:

 fab2.hide(new FloatingActionButton.OnVisibilityChangedListener() {
            @Override
            public void onHidden(FloatingActionButton fab) {
                Log.i(TAG,"fab hidden...");
            }
        });
        fab2.show(new FloatingActionButton.OnVisibilityChangedListener() {
            @Override
            public void onShown(FloatingActionButton fab) {
                Log.i(TAG,"fab show...");
            }
        });複製程式碼

可以在對應的回撥方法裡做接下來的操作。

Snackbar

Snackbar 是一種針對操作的輕量級反饋機制,常以一個小的彈出框的形式,出現在手機螢幕下方或者桌面左下方。它們出現在螢幕所有層的最上方,包括浮動操作按鈕。

它們會在超時或者使用者在螢幕其他地方觸控之後自動消失。Snackbar 可以在螢幕上滑動關閉。當它們出現時,不會阻礙使用者在螢幕上的輸入,並且也不支援輸入。螢幕上同時最多隻能現實一個 Snackbar。

Android 也提供了一種主要用於提示系統訊息的膠囊狀的提示框 Toast。Toast 同 Snackbar 非常相似,但是 Toast 並不包含操作也不能從螢幕上滑動關閉。

用法:
Snackbar的高度應該能容納下所提示的 文字,並且提示與操作相關,所以不應該提示長文字,Snackbar的用法與Toast非常相似。

彈出一個Toast 的程式碼:

Toast.makeText(FABSimpleActivity.this,"哈哈,我是Toast",Toast.LENGTH_SHORT).show();複製程式碼

彈出一個snackbar的程式碼:

 fab2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Snackbar.make(fab2,"哈哈,我是Snackbar",Snackbar.LENGTH_SHORT).show();
            }
        });複製程式碼

效果如下:

Material Design 系列之 CardView、FAB 和 Snackbar
snackbar.png

從上面的程式碼可以看出,Toast與Snackbar的呼叫方法非常相似,第一個引數有點區別,Toast的第一個引數是一個Context,Snackbar的第一個引數是View,但其實都是殊途同歸的,Snackbar也是根據傳入的View找到一個Parent View ,然後再獲取Context。或許你們跟我一樣很奇怪為什麼要繞著麼大一圈來獲取這個Context,像Toast 一樣直接傳一個Context不行嗎?答案是不行的,因為需要告訴Snackbar,讓它顯示在哪個容器內。Snackbar 和Toast的方式不一樣,看一下原始碼一目瞭然,走讀一下原始碼:
1,在make方法裡構造了Snackbar,傳入的引數是根據傳的View找到的Parent View :

 public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
            @Duration int duration) {
        Snackbar snackbar = new Snackbar(findSuitableParent(view));
        snackbar.setText(text);
        snackbar.setDuration(duration);
        return snackbar;
    }複製程式碼

Snackbar 構造方法:


    private Snackbar(ViewGroup parent) {
        mTargetParent = parent;
        mContext = parent.getContext();

        ThemeUtils.checkAppCompatTheme(mContext);

        LayoutInflater inflater = LayoutInflater.from(mContext);
        mView = (SnackbarLayout) inflater.inflate(
                R.layout.design_layout_snackbar, mTargetParent, false);

        mAccessibilityManager = (AccessibilityManager)
                mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
    }複製程式碼

上面inflate 的時候用到了mTargetParent,告訴Snackbar要顯示在哪個容器內。
再看一下show 的方式:

 public void show() {
        SnackbarManager.getInstance().show(mDuration, mManagerCallback);
    }複製程式碼

然後呼叫scheduleTimeoutLocked 方法:

 private void scheduleTimeoutLocked(SnackbarRecord r) {
        if (r.duration == Snackbar.LENGTH_INDEFINITE) {
            // If we're set to indefinite, we don't want to set a timeout
            return;
        }

        int durationMs = LONG_DURATION_MS;
        if (r.duration > 0) {
            durationMs = r.duration;
        } else if (r.duration == Snackbar.LENGTH_SHORT) {
            durationMs = SHORT_DURATION_MS;
        }
        mHandler.removeCallbacksAndMessages(r);
        mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_TIMEOUT, r), durationMs);
    }複製程式碼

最後是用Handler 發了一條訊息,通知顯示:


    static {
        sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
            @Override
            public boolean handleMessage(Message message) {
                switch (message.what) {
                    case MSG_SHOW:
                        ((Snackbar) message.obj).showView();
                        return true;
                    case MSG_DISMISS:
                        ((Snackbar) message.obj).hideView(message.arg1);
                        return true;
                }
                return false;
            }
        });
    }複製程式碼

看到這兒大概就明白了,最終呼叫的是showView()這個方法顯示:

final void showView() {
...
// 上面省略的部分主要是判斷是不是CoordinatorLayout的子View,如果新增Behavior 

if (ViewCompat.isLaidOut(mView)) {
            if (shouldAnimate()) {
                // If animations are enabled, animate it in
                animateViewIn();
            } else {
                // Else if anims are disabled just call back now
                onViewShown();
            }
        } else {
            // Otherwise, add one of our layout change listeners and show it in when laid out
            mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {
                @Override
                public void onLayoutChange(View view, int left, int top, int right, int bottom) {
                    mView.setOnLayoutChangeListener(null);

                    if (shouldAnimate()) {
                        // If animations are enabled, animate it in
                        animateViewIn();
                    } else {
                        // Else if anims are disabled just call back now
                        onViewShown();
                    }
                }
            });
        }
}複製程式碼

以上分析了Snackbar 的建立顯示過程。其實使用是很簡單的,跟以前使用Toast提示差不多。

最後還有一點就是,Toast只能給個提示,而Snackbar我們還可以給他設定一個Action,當顯示Snackbar的時候,我們點選Action按鈕,執行相應的操作

程式碼:

private void showSnackbar(){
        Snackbar snackbar = Snackbar.make(fab2,"哈哈,我是Snackbar",Snackbar.LENGTH_SHORT);
        snackbar.setAction("UNDO", new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(FABSimpleActivity.this,"執行Undo操作",Toast.LENGTH_LONG).show();
            }
        });

        snackbar.setActionTextColor(getResources().getColor(R.color.DarkCyan));
        snackbar.setText("已經刪除1張照片");
        snackbar.show();
    }複製程式碼

效果如下:

Material Design 系列之 CardView、FAB 和 Snackbar
snackbar_action.gif

如上圖所示,新增了一個UNDO 按鈕,點選按鈕之行相應操作。

最後

這個三個控制元件的用法比較簡單,本文從它們的使用場景和原理講了它們的基本用法,瞭解這些之後,可以加深印象。本文是Material Design 相關的最後一篇文章,可能還有一些零碎的東西沒有講到,還有一些像RecyclerView 這些的用法網上的部落格已經很多了,有的也寫得很好很詳細,不打算再寫。另外,Material Design 系列的Demo在這兒:MaterialDesignSamples

參考
Material Design 設計規範
Material Design 中文版

相關文章