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">               <item name="android:colorPrimary">@color/primary</item>          <item name="android:colorPrimaryDark">@color/primary_dark</item>          <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.+' } 
  | 
 
包含以下属性/方法:
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 裁剪
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 
  | Palette p = Palette.from(bitmap).generate(); Palette.from(bitmap).generate(new PaletteAsyncListener() {     public void onGenerated(Palette p) {              } }); 
  | 
 
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 
  | View myView = findViewById(R.id.my_view); 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">      <item name="android:windowContentTransitions">true</item>      <item name="android:windowEnterTransition">@transition/explode</item>   <item name="android:windowExitTransition">@transition/explode</item>      <item name="android:windowSharedElementEnterTransition">     @transition/change_image_transform</item>   <item name="android:windowSharedElementExitTransition">     @transition/change_image_transform</item> </style> <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> <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());     } } 
  | 
 
2.7.4 共享元素转换
共享元素转换继承自android.transition.Transition 
- changeBounds
 
- changeClipBounds
 
- changeTransform
 
- changeImageTransform
 
以共享元素启动一个操作行为
- 在主题中启用窗口内容转换
 
- 在风格中指定一个共享元素转换
 
- 将您的转换定义为 XML 资源
 
- 指定共享元素通用名称 android:transitionName/View.setTransitionName() 
 
- 使用 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"); 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 
  | <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"/>                       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 矢量动画
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 
  | <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> <objectAnimator     android:duration="6000"     android:propertyName="rotation"     android:valueFrom="0"     android:valueTo="360" /> <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> <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