Material Design为UI设计提供了许多准则,保证统一的视觉效果和合理的交互。

比较明显的是:

  • 通过高度描述层次

  • 通过阴影表达高度

  • 通过动画引起关注

假如有了Material风格的设计稿,怎样在Android里实现呢?

1. Android 4.X或以下版本

1.1 主题、布局、配色

一直都有,改配置即可

1.2 全屏扩散的水波纹触摸反馈

如果非要做,我猜可以这样:在根布局上再加一层,经过一堆坐标转换和裁切,把动画绘制在那个层,但工作量、性能和体验都可想而知。

1.3 共享元素转换动画

如果不是真爱,我会跟设计师协商换方案……

Activity基本不能直接用了,需要用Fragment重写,自己管理Fragment的生命周期,控制窗口事件,控制转场动画,光想想都觉得很蛋疼。

1.4 阴影

每个程序员应该都会对阴影深恶痛绝,因为阴影很难处理。可是设计师不知道啊,在扁平风没流行的时候,设计师们恨不得给每个浮起的部件都加上阴影,结果阴影成了程序员的阴影,成了各种geek方案的实验田。

在Android 4.X或更低版本的系统里,一个View如果要显示阴影,通常是通过nine-patch背景图实现的,所以View的尺寸会包含阴影的部分,于是便带来各种计算问题:

1.4.1 如何确定View的尺寸?

尺寸问题大多可以用土方法解决:

  • 如果View固定长宽,测量完尺寸后,额外加上阴影的尺寸
  • 为mdpi/hdpi/xhdpi/xxhdpi指定不同阴影尺寸的nine-patch图片
  • 定义shadowLeft/shadowRight/shadowTop/shadowBottom之类参数辅助计算

1.4.2 如何定位?

定位问题处理起来非常麻烦。

例如一个很简单的界面,ActionBar下面一个全屏的ScrollView。 从布局来看,外层应该是个垂直的LinearLayout,一个ActionBar,一个ScrollView搞定。

可是Material风格的ActionBar下面是有个半透明阴影的,阴影和ScrollView的是部分重合的!

噩梦开始了,先把LinearLayout改成ReleativeLayout,ActionBar指定高度为actionBarHeight+shadowBottom,ScrollView指定topMargin为actionBarHeight。当然还有其他的方法,例如把阴影单独切图做成个View跟ScrollView重叠放置。

1.4.3 高度变化时阴影尺寸怎么改变?

几乎无法实现,除非恶劣地提供一系列不同阴影尺寸的背景图……

1.4.4 列表的dividerHeight如果大于0小于shadowBottom怎么办?

定义个垂直方向裁切过的Drawable作为divider,底部可能还要补一个完整高度的阴影,如果每项的底色是透明的还要补个被遮盖的部分阴影……

1.4.5 问题根源在哪?

根据Material风格的定义,高度是View的基本属性,阴影属于环境,应该由环境结合View的高度进行渲染。
用背景图来实现阴影,视图模型就跟设计稿不一致,任何补救都是拆东墙补西墙。

2. Android 5.x或以上版本

有了API21和v7兼容包,上面的那堆问题终于不是问题了!

官方文档: How to implement material design on Android

2.1 Material主题

官方提供深色版主题(Theme.Material)和浅色版主题(Theme.Material.Light)

可以自定义主题色和强调色:

1
2
3
4
5
6
7
8
9
10
11
<resources>
<style name="AppTheme" parent="android:Theme.Material">
<!-- Main theme colors -->
<!-- your app branding color for the app bar -->
<item name="android:colorPrimary">@color/primary</item>
<!-- darker variant for the status bar and contextual app bars -->
<item name="android:colorPrimaryDark">@color/primary_dark</item>
<!-- theme UI controls like checkboxes and text fields -->
<item name="android:colorAccent">@color/accent</item>
</style>
</resources>

状态栏颜色由statusBarColor决定,默认将继承colorPrimaryDark的值,可设置为@android:color/transparent使之透明。

2.2 RecyclerView

1
2
3
dependencies {
compile 'com.android.support:recyclerview-v7:21.0.+'
}

内置的布局管理器包括:

  • LinearLayoutManager: 以垂直或水平滚动列表方式显示项目

  • GridLayoutManager: 在网格中显示项目

  • StaggeredGridLayoutManager: 在分散对齐网格中显示项目

RecyclerView添加与删除项目时有默认动画,可通过setItemAnimator()自定义动画

2.3 CardView

1
2
3
dependencies {
compile 'com.android.support:cardview-v7:21.0.+'
}

包含以下属性/方法:

  • 阴影:cardElevation

  • 圆角半径:cardCornerRadius/setRadius()

  • 背景颜色:cardBackgroundColor

2.4 阴影

Z = elevation + translationZ,单位dp

2.4.1 静止高度 elevation

1
2
3
4
5
6
7
<TextView
android:id="@+id/my_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/next"
android:background="@color/white"
android:elevation="5dp" />

2.4.2 高度偏移 translationZ

使用 ViewPropertyAnimator.z() 和 ViewPropertyAnimator.translationZ() 可创建视图高度动画

2.4.3 轮廓 Outline

视图将投射一个带有圆角的阴影,轮廓则用于渲染阴影。

默认情况下轮廓是背景的形状,使用View.setOutlineProvider()可自定义轮廓,如果希望禁止阴影,可以设为null。

2.5 裁剪

  • 通过clipToOutline/View.setClipToOutline()可指定轮廓,但目前仅支持矩形、圆形和圆角矩形。

  • 裁剪视图是高成本操作,因此不可用于动画

2.6 图片

2.6.1 着色

通过tint/tintMode可为BitmapDrawable/NinePatchDrawable着色。

2.6.2 萃取颜色

1
2
3
dependencies {
compile 'com.android.support:palette-v7:21.0.0'
}

Palette类可从图像萃取下列突出颜色:

  • 鲜艳 Vibrant
  • 鲜艳深色 Vibrant Dark
  • 鲜艳浅色 Vibrant Light
  • 低调 Muted
  • 低调深色 Muted Dark
  • 低调浅色 Muted Light
1
2
3
4
5
6
7
8
9
// Synchronous
Palette p = Palette.from(bitmap).generate();
// Asynchronous
Palette.from(bitmap).generate(new PaletteAsyncListener() {
public void onGenerated(Palette p) {
// Use generated instance
}
});

2.6.3 矢量图片

可使用vector和SVG语法描述矢量图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- res/drawable/heart.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
<!-- intrinsic size of the drawable -->
android:height="256dp"
android:width="256dp"
<!-- size of the virtual canvas -->
android:viewportWidth="32"
android:viewportHeight="32">
<!-- draw a path -->
<path android:fillColor="#8fff"
android:pathData="M20.5,9.5
c-1.955,0,-3.83,1.268,-4.5,3
c-0.67,-1.732,-2.547,-3,-4.5,-3
C8.957,9.5,7,11.432,7,14
c0,3.53,3.793,6.257,9,11.5
c5.207,-5.242,9,-7.97,9,-11.5
C25,11.432,23.043,9.5,20.5,9.5z" />
</vector>

2.7 动画

2.7.1 触摸反馈

使用ripple/RippleDrawable实现水波纹效果

  • ?android:attr/selectableItemBackground 指定有界的波纹。
  • ?android:attr/selectableItemBackgroundBorderless 指定无界的波纹
  • ?android:colorControlHighlight 指定默认触摸反馈颜色

2.7.2 揭露效果 (reveal)

用于显示或隐藏一组UI元素。ViewAnimationUtils.createCircularReveal()能够为裁剪区域添加动画以揭露或隐藏视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// previously invisible view
View myView = findViewById(R.id.my_view);
// get the center for the clipping circle
int cx = (myView.getLeft() + myView.getRight()) / 2;
int cy = (myView.getTop() + myView.getBottom()) / 2;
// 显示:
int initialRadius = 0;
int finalRadius = Math.max(myView.getWidth(), myView.getHeight());
Animator anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, initialRadius, finalRadius);
myView.setVisibility(View.VISIBLE);
anim.start();
// 隐藏
int initialRadius = myView.getWidth();
int finalRadius = 0;
Animator anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, initialRadius, finalRadius);
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
myView.setVisibility(View.INVISIBLE);
}
});
anim.start();

2.7.3 转换

转换继承自android.transition.Visibility,默认转换是淡入淡出

  • 分解 Explode
  • 滑动 Slide
  • 淡入淡出 Fade

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<style name="BaseAppTheme" parent="android:Theme.Material">
<!-- enable window content transitions -->
<item name="android:windowContentTransitions">true</item>
<!-- specify enter and exit transitions -->
<item name="android:windowEnterTransition">@transition/explode</item>
<item name="android:windowExitTransition">@transition/explode</item>
<!-- specify shared element transitions -->
<item name="android:windowSharedElementEnterTransition">
@transition/change_image_transform</item>
<item name="android:windowSharedElementExitTransition">
@transition/change_image_transform</item>
</style>
<!-- res/transition/change_image_transform.xml -->
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:transitionOrdering="sequential">
<changeBounds/>
<fade android:fadingMode="fade_out" >
<targets>
<target android:targetId="@id/grayscaleContainer" />
</targets>
</fade>
</transitionSet>
<!-- res/transition/custom.xml -->
<transition class="my.app.transition.CustomTransition"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
setContentView(R.layout.activity_my);
}
public void onSomeButtonClicked(View view) {
getWindow().setExitTransition(new Explode());
Intent intent = new Intent(this, MyOtherActivity.class);
startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle());
}
}
  • 使用Window.setAllowEnterTransitionOverlap()可以尽快开始转换。

  • 如果结束时希望反转播放转换动画,应调用finishAfterTransition()而非finish()

2.7.4 共享元素转换

共享元素转换继承自android.transition.Transition

  • changeBounds
  • changeClipBounds
  • changeTransform
  • changeImageTransform
以共享元素启动一个操作行为
  1. 在主题中启用窗口内容转换
  2. 在风格中指定一个共享元素转换
  3. 将您的转换定义为 XML 资源
  4. 指定共享元素通用名称 android:transitionName/View.setTransitionName()
  5. 使用 ActivityOptions.makeSceneTransitionAnimation()
1
2
3
4
5
6
7
8
9
10
11
// 单个共享元素转换
View androidRobotView = findViewById(R.id.image_small);
ActivityOptions options = ActivityOptions
.makeSceneTransitionAnimation(this, androidRobotView, "robot");
// 多个共享元素转换
// ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(this,
// Pair.create(view1, "agreedName1"),
// Pair.create(view2, "agreedName2"));
startActivity(new Intent(this, Activity2.class), options.toBundle());

2.7.5 曲线运动

PathInterpolator可基于贝塞尔曲线或Path

1
2
3
4
5
<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
android:controlX1="0.4"
android:controlY1="0"
android:controlX2="1"
android:controlY2="1"/>

系统提供了三种基本曲线:

@interpolator/fast_out_linear_in.xml
@interpolator/fast_out_slow_in.xml
@interpolator/linear_out_slow_in.xml

可通过ObjectAnimator的新构造函数添加路径动画

1
2
3
4
ObjectAnimator mAnimator;
mAnimator = ObjectAnimator.ofFloat(view, View.X, View.Y, path);
...
mAnimator.start();

2.7.6 状态动画

通过android:stateListAnimator可以为View指定状态动画(神一般的功能,再也不用写视图状态机了)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- animate the translationZ property of a view when pressed -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<set>
<objectAnimator android:propertyName="translationZ"
android:duration="@android:integer/config_shortAnimTime"
android:valueTo="2dp"
android:valueType="floatType"/>
<!-- you could have other objectAnimator elements
here for "x" and "y", or other properties -->
</set>
</item>
<item android:state_enabled="true"
android:state_pressed="false"
android:state_focused="true">
<set>
<objectAnimator android:propertyName="translationZ"
android:duration="100"
android:valueTo="0"
android:valueType="floatType"/>
</set>
</item>
</selector>

Material主题的按钮默认情况下包含Z动画,可将stateListAnimator设为@null以避免这种行为。

还可通过animated-selector/AnimatedStateListDrawable用图片表示View的不同状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- res/drawable/myanimstatedrawable.xml -->
<animated-selector
xmlns:android="http://schemas.android.com/apk/res/android">
<!-- provide a different drawable for each state-->
<item android:id="@+id/pressed" android:drawable="@drawable/drawableP"
android:state_pressed="true"/>
<item android:id="@+id/focused" android:drawable="@drawable/drawableF"
android:state_focused="true"/>
<item android:id="@id/default"
android:drawable="@drawable/drawableD"/>
<!-- specify a transition -->
<transition android:fromId="@+id/default" android:toId="@+id/pressed">
<animation-list>
<item android:duration="15" android:drawable="@drawable/dt1"/>
<item android:duration="15" android:drawable="@drawable/dt2"/>
...
</animation-list>
</transition>
...
</animated-selector>

2.7.7 矢量动画

  • res/drawable/ - 定义矢量图片

  • res/anim/ - 定义对象变化

  • res/drawable/ - 通过target(name,animation)关联vector和objectAnimator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!-- res/drawable/vectordrawable.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="64dp"
android:width="64dp"
android:viewportHeight="600"
android:viewportWidth="600">
<group
android:name="rotationGroup"
android:pivotX="300.0"
android:pivotY="300.0"
android:rotation="45.0" >
<path
android:name="v"
android:fillColor="#000000"
android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
</group>
</vector>
<!-- res/anim/rotation.xml -->
<objectAnimator
android:duration="6000"
android:propertyName="rotation"
android:valueFrom="0"
android:valueTo="360" />
<!-- res/anim/path_morph.xml -->
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:duration="3000"
android:propertyName="pathData"
android:valueFrom="M300,70 l 0,-70 70,70 0,0 -70,70z"
android:valueTo="M300,70 l 0,-70 70,0 0,140 -70,0 z"
android:valueType="pathType" />
</set>
<!-- res/drawable/animvectordrawable.xml -->
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/vectordrawable" >
<target
android:name="rotationGroup"
android:animation="@anim/rotation" />
<target
android:name="v"
android:animation="@anim/path_morph" />
</animated-vector>

2.8 兼容性

下列功能仅在 Android 5.0(API21)及更高版本中提供:

  • 操作行为转换
  • 触摸反馈
  • 揭露动画
  • 基于路径的动画
  • 矢量图片
  • 图片着色

v7r21兼容库包括以下功能:

2.8.1 为下列组件提供Material风格主题

  • EditText
  • Spinner
  • CheckBox
  • RadioButton
  • SwitchCompat
  • CheckedTextView

2.8.2 配色

1
2
3
4
5
6
7
<!-- extend one of the Theme.AppCompat themes -->
<style name="Theme.MyTheme" parent="Theme.AppCompat.Light">
<!-- customize the color palette -->
<item name="colorPrimary">@color/material_blue_500</item>
<item name="colorPrimaryDark">@color/material_blue_700</item>
<item name="colorAccent">@color/material_green_A200</item>
</style>

2.8.3 RecyclerView和CardView

1
2
3
4
5
dependencies {
compile 'com.android.support:appcompat-v7:21.0.+'
compile 'com.android.support:cardview-v7:21.0.+'
compile 'com.android.support:recyclerview-v7:21.0.+'
}

但早期版本会有下面的限制:

  • CardView使用额外边距返回编程阴影实现

  • CardView不会裁剪其与圆角相交的子视图

2.8.4 Palette