前言
更多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(卡片)
卡片是一张带有材料属性的纸片,用作展示更多详细信息的入口点。卡片包含了一组特定的数据集,数据集含有各种相关信息,如主题照片、文本,链接等等。卡片有固定的宽度和可变的高度。最大高度限制于可适应平台上单一视图的内容,但如果需要它可以临时扩展(例如,显示评论栏)。卡片不会翻转以展示其背后的信息。
卡片集是共面的,或者统一平面的多张卡片布局。如下:
一张卡片包含了一组特定的数据集,考虑在以下这些情况使用卡片:
作为一个集合,比较多种数据类型,比如:图片、视频和文本。
不需要直接比较(如:用户不会直接比较图片和文本)
支持内容高度可变,比如评论。
包含响应按钮,比如+1 按钮或者评论
要使用网格列表,但需要显示更多文本来补充图片
以上就是使用卡片的一些场景,看一个使用不当的例子(图片来自官网):
** 错误示例:** 这种卡片的使用分散了用户的注意力,不能快速浏览,也不能忽略掉,所以将这些内容放在不同的卡片上是难以理解的。其实国内有些知名APP也没有按照规范来做,给我们做了错误的示范,如知乎日报首页:
正确的用法如下:
正确示例:可快速浏览的列表,用来代替卡片,是表现没有许多操作的同类内容的合适方法。
以上就是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);
比较简单,就上面几个属性,都一一介绍了,看一些示例:
效果如下:
CardView 点击效果
Material Design 的设计就是为了更贴近现实生活中的场景,当点击之后,是会有反馈的,可以给CardView 添加点击效果。
android:clickable="true"
android:foreground="?attr/selectableItemBackground"
这样点击时就会有波纹扩散效果了,增加体验。
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 颜色,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();
}
});
效果如下:
从上面的代码可以看出,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();
}
效果如下:
如上图所示,添加了一个UNDO 按钮,点击按钮之行相应操作。
最后
这个三个控件的用法比较简单,本文从它们的使用场景和原理讲了它们的基本用法,了解这些之后,可以加深印象。本文是Material Design 相关的最后一篇文章,可能还有一些零碎的东西没有讲到,还有一些像RecyclerView 这些的用法网上的博客已经很多了,有的也写得很好很详细,不打算再写。另外,Material Design 系列的Demo在这儿:MaterialDesignSamples
参考
Material Design 设计规范
Material Design 中文版