Material Design 系列之CardView、FAB和Snackbar

前言

更多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(卡片)

卡片是一张带有材料属性的纸片,用作展示更多详细信息的入口点。卡片包含了一组特定的数据集,数据集含有各种相关信息,如主题照片、文本,链接等等。卡片有固定的宽度和可变的高度。最大高度限制于可适应平台上单一视图的内容,但如果需要它可以临时扩展(例如,显示评论栏)。卡片不会翻转以展示其背后的信息。

卡片集是共面的,或者统一平面的多张卡片布局。如下:

card_collection_1.png

一张卡片包含了一组特定的数据集,考虑在以下这些情况使用卡片:

  • 作为一个集合,比较多种数据类型,比如:图片、视频和文本。

  • 不需要直接比较(如:用户不会直接比较图片和文本)

  • 支持内容高度可变,比如评论。

  • 包含响应按钮,比如+1 按钮或者评论

  • 要使用网格列表,但需要显示更多文本来补充图片

以上就是使用卡片的一些场景,看一个使用不当的例子(图片来自官网):

card_wrong.png

** 错误示例:** 这种卡片的使用分散了用户的注意力,不能快速浏览,也不能忽略掉,所以将这些内容放在不同的卡片上是难以理解的。其实国内有些知名APP也没有按照规范来做,给我们做了错误的示范,如知乎日报首页:

zhihuribao.png

正确的用法如下:


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>

效果如下:

CardView.png

CardView 点击效果
Material Design 的设计就是为了更贴近现实生活中的场景,当点击之后,是会有反馈的,可以给CardView 添加点击效果。

 android:clickable="true"
  android:foreground="?attr/selectableItemBackground"

这样点击时就会有波纹扩散效果了,增加体验。

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 常用的场景,效果如下:

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();
            }
        });

效果如下:

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();
    }

效果如下:

snackbar_action.gif

如上图所示,添加了一个UNDO 按钮,点击按钮之行相应操作。

最后

这个三个控件的用法比较简单,本文从它们的使用场景和原理讲了它们的基本用法,了解这些之后,可以加深印象。本文是Material Design 相关的最后一篇文章,可能还有一些零碎的东西没有讲到,还有一些像RecyclerView 这些的用法网上的博客已经很多了,有的也写得很好很详细,不打算再写。另外,Material Design 系列的Demo在这儿:MaterialDesignSamples

参考
Material Design 设计规范
Material Design 中文版

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,416评论 25 707
  • 内容抽屉菜单ListViewWebViewSwitchButton按钮点赞按钮进度条TabLayout图标下拉刷新...
    皇小弟阅读 46,705评论 22 664
  • 我个人认为,学画呢,一边学,一边买,一边画,是最好的结构。大画家从小学画,顿悟一生,传承并创新,师古人师造化师我心...
    富山居阅读 301评论 0 1
  • 不知道 你有什么忧伤 因为快乐 并没有写在你的脸上 不快乐 也不要把眉头紧锁 当春天来临的时候 总会有鸟儿 在缤纷...
    江城妖怪阅读 390评论 0 0
  • 林志玲和比尔盖茨似乎不应该是出现在同一维度上的名字,但无可否认,这是两个智商、情商都极为出色且极富文艺情怀的人。 ...
    水水马阅读 446评论 0 1