Android Material Design Animatio
1 动画的规范
Material Design
是 Google 总结的一套具有创新性和符合科学的视觉规范,其中包含动画、排版、控件、资源、样式等,包含的内容比较全,感兴趣的同学可以查看官方文档 Material design。
对于动画,Material Design
定义的一套规则,这里简单总结如下:
-
自然的加减速
将动画的主体
imageView
看做是一个实际物体,有质量和重量,那么做出来的动画更符合人们的认知。不仅动画的速度改变要平滑,加速度的改变也要平滑,位移动画减速到停止的瞬间,移动速度的减慢速度也在变慢。 -
自然的入场和出场
image -
用户输入反馈
-
满足波纹效果的触屏反馈,一种点击水面的触感
image -
改变控件高度的触屏反馈
image
-
-
生动的场景切换动画
image -
连贯的图标渐变动画
image
2 动画的实现
贴心的 Google 大神么不仅定义了这些规范,也提供相关的类和方法帮助我们实现他们的规范。根据动画的类别和提供的类和属性等,大概整理下,这里将 Material Design Animation
分为 6 类,分别是:
-
Touch Feedback (触摸反馈)
-
Reveal Effect (揭露效果)
-
Activity Transitions ( Activity 切换效果 )
-
Curved Motion (曲线运动)
-
View State Changes (视图状态改变)
-
Animate View Drawables (可绘矢量动画)
2.1 Touch Feedback (触摸反馈)
触摸反馈比较好理解,大家在做项目的时候,经常碰到视觉同学会要求我们做出来的按钮在点击的时候,修改外观,比如置灰显示。下面这段 xml 代码,就是解决这个问题的一种常规办法,相信 Android 小伙伴们都非常熟悉,当然不使用 xml 的话,也可以自定义在 Java 代码中自定义使用 ColorStateList
,来达到相同的效果。不过这种常规实现的效果中规中矩,不算酷炫。
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@mipmap/ic_btn_pressed" android:state_pressed="true"/>
<item android:drawable="@mipmap/ic_btn"/>
</selector>
为了实现上面规范中讲述的波纹效果,可以使用 RippleDrawable
类,简单的我们可以在 xml
文件中定义。
-
?android:attr/selectableItemBackground - 有界限的波纹
<Button style="@style/style_touch_feedback_btn" android:background="?android:attr/selectableItemBackground" android:id="@+id/btn_normal" />
-
?android:attr/selectableItemBackgroundBorderless - 延伸到view之外的波纹
<Button style="@style/style_touch_feedback_btn" android:background="?android:attr/selectableItemBackgroundBorderless" android:id="@+id/btn_borderless"/>
-
使用RippleDrawer自定义的类型
<Button style="@style/style_touch_feedback_btn" android:background="@drawable/custom_ripple" android:id="@+id/btn_ripple"/>
custom_ripple.xml
<?xml version="1.0" encoding="utf-8"?> <!-- A blue ripple drawn atop a drawable resource. --> <ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="#ff0000ff"> <item android:drawable="@drawable/bg_loadmore0" /> </ripple>
-
在 Android 21 以上是默认有波纹效果的,可以使用
android:background="@null"
或者指定其他颜色,取消波纹<Button style="@style/style_touch_feedback_btn" android:background="?android:attr/selectableItemBackground" android:id="@+id/btn_normal" />
整体显示效果如下:
image另外,因为 Touch Feedback
是 Android 5.0 上默认支持的,如果需要在低版本的 Android 应用中也要实现的话,那可以参见 RippleEffect
2.2 Reveal Effect (揭露效果)
Android 5.0 引入了 ViewAnimationUtils.createCircularReveal()
接口,能很方便的实现圆形缩放效果。
private static void circularRevealBox(View view) {
Rect rect = new Rect();
view.getLocalVisibleRect(rect);
int cx = rect.left + rect.width()/2;
int cy = rect.top + rect.height()/2;
// get the final radius for the clipping circle
int finalRadius = view.getWidth();
Animator anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, 0, finalRadius);
anim.setDuration(1000);
anim.start();
view.setVisibility(View.VISIBLE);
}
效果如下:
image2.3 Curved Motion (曲线运动)
2.3.1 使用 TypeEvaluator 实现
在 Android 5.0 之前,如果需要实现曲线位移运动,则需要自己写过程插值器 TypeEvaluator
,代码如下:
public class PathEvaluator implements TypeEvaluator<PathPoint> {
@Override
public PathPoint evaluate(float t, PathPoint startValue, PathPoint endValue) {
float x, y;
if (endValue.mOperation == PathPoint.CURVE) {
float oneMinusT = 1 - t;
x = oneMinusT * oneMinusT * oneMinusT * startValue.mX +
3 * oneMinusT * oneMinusT * t * endValue.mControl0X +
3 * oneMinusT * t * t * endValue.mControl1X +
t * t * t * endValue.mX;
y = oneMinusT * oneMinusT * oneMinusT * startValue.mY +
3 * oneMinusT * oneMinusT * t * endValue.mControl0Y +
3 * oneMinusT * t * t * endValue.mControl1Y +
t * t * t * endValue.mY;
} else if (endValue.mOperation == PathPoint.LINE) {
x = startValue.mX + t * (endValue.mX - startValue.mX);
y = startValue.mY + t * (endValue.mY - startValue.mY);
} else {
x = endValue.mX;
y = endValue.mY;
}
return PathPoint.moveTo(x, y);
}
}
自定义的动画路径类 AnimatorPath
public class AnimatorPath {
// The points in the path
ArrayList<PathPoint> mPoints = new ArrayList<PathPoint>();
/**
* Move from the current path point to the new one
* specified by x and y. This will create a discontinuity if this point is
* neither the first point in the path nor the same as the previous point
* in the path.
*/
public void moveTo(float x, float y) {
mPoints.add(PathPoint.moveTo(x, y));
}
/**
* Create a straight line from the current path point to the new one
* specified by x and y.
*/
public void lineTo(float x, float y) {
mPoints.add(PathPoint.lineTo(x, y));
}
/**
* Create a cubic Bezier curve from the current path point to the new one
* specified by x and y. The curve uses the current path location as the first anchor
* point, the control points (c0X, c0Y) and (c1X, c1Y), and (x, y) as the end anchor point.
*/
public void curveTo(float c0X, float c0Y, float c1X, float c1Y, float x, float y) {
mPoints.add(PathPoint.curveTo(c0X, c0Y, c1X, c1Y, x, y));
}
/**
* Returns a Collection of PathPoint objects that describe all points in the path.
*/
public Collection<PathPoint> getPoints() {
return mPoints;
}
}
之后曲线动画设置和启动代码如下
// Set up the path we're animating along
AnimatorPath path = new AnimatorPath();
path.moveTo(0, 0);
path.lineTo(0, 300);
path.curveTo(100, 0, 300, 900, 400, 500);
// Set up the animation
final ObjectAnimator anim = ObjectAnimator.ofObject(this, "buttonLoc",
new PathEvaluator(), path.getPoints().toArray());
anim.setDuration(3000);
anim.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mMaterialButton.setVisibility(View.INVISIBLE);
}
@Override
public void onAnimationEnd(Animator animation) {
mMaterialButton.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
效果如下:
image2.3.2 使用 PathInterpolator 和 ObjectAnimator.ofFloat 接口实现
在 Android 5.0 提供了
-
PathInterpolator
: 以三次 bezier 曲线中间 2 个控制点的坐标来定义动画的速率快慢 -
ObjectAnimator.ofFloat(T target, Property<T, Float> xProperty, Property<T, Float> yProperty, Path path)
: 中间的path
参数定义位移动画的路径。
Path path = new Path();
//path.moveTo(0, 100);
path.lineTo(0, 400);
path.cubicTo(100, 0, 300, 900, 500, 600);
PathInterpolator pathInterpolator = new PathInterpolator(0.8f, 0f, 1f, 1f);
final ObjectAnimator mAnimator = ObjectAnimator.ofFloat(mMaterialButton, View.X, View.Y, path);
mAnimator.setInterpolator(pathInterpolator);
mAnimator.setDuration(3000);
效果如下:
image2.4 View State Changes (视图状态改变)
在 Android 5.0 新提供了 StateListAnimator
和 AnimatedStateListDrawable
类用于实现视图状态改变动画
-
StateListAnimator
res/anim/anim_view_state_changes.xml
<?xml version="1.0" encoding="utf-8"?> <!-- 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="4dp" android:valueType="floatType"/> </set> </item> <item android:state_pressed="false"> <set> <objectAnimator android:propertyName="translationZ" android:duration="100" android:valueTo="0" android:valueType="floatType"/> </set> </item> </selector>
说明 :定义了一个按下时,控件 Z 值变大的动画;松开时,控件 Z 值变小的动画。其中 android 中 Z 值的改变是通过控件的阴影大小来体现
具体应用:
-
xml
中定义
<TextView android:text="animate_view_state_changes" android:layout_width="100dp" android:layout_height="100dp" android:stateListAnimator="@anim/anim_view_state_changes"
-
java
中定义
TextView textView = (TextView)findViewById(R.id.animate_view_state_changes); StateListAnimator stateLAnim = AnimatorInflater.loadStateListAnimator(this, R.anim.anim_view_state_changes); textView.setStateListAnimator(stateLAnim);
效果图如下:
image -
-
AnimatedStateListDrawable
res/drawable/my_anim_state_drawable.xml
<?xml version="1.0" encoding="utf-8"?> <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/bg_loadmore5" android:state_pressed="true"/> <item android:id="@+id/focused" android:drawable="@drawable/bg_loadmore5" android:state_focused="true"/> <item android:id="@+id/normal" android:drawable="@drawable/bg_loadmore0"/> <!-- specify a transition --> <transition android:fromId="@+id/normal" android:toId="@+id/pressed"> <animation-list> <item android:duration="15" android:drawable="@drawable/bg_loadmore0"/> <item android:duration="15" android:drawable="@drawable/bg_loadmore1"/> <item android:duration="15" android:drawable="@drawable/bg_loadmore2"/> <item android:duration="15" android:drawable="@drawable/bg_loadmore3"/> <item android:duration="15" android:drawable="@drawable/bg_loadmore4"/> <item android:duration="15" android:drawable="@drawable/bg_loadmore5"/> </animation-list> </transition> </animated-selector>
说明 :当处于press状态的时候,animation-list 正着走一遍,drawable使用最后一个;当处于press状态的时候,animation-list反着走一遍,drawable使用第一个
具体应用:
<TextView android:text="my_anim_state_drawable" android:layout_width="250dp" android:layout_height="100dp" android:background="@drawable/my_anim_state_drawable" />
效果图如下:
image
2.5 Animate View Drawables (可绘矢量动画)
通常在三种xml定义的矢量图
-
静态矢量图,使用
<vector>
元素的矢量图,在res/drawable/
矢量图可以定义的属性元素有<group>和<path>,<group>定义了一个<path>的集合,或者子<group>,<path>定义绘制的路径。
<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>
元素,在res/animator
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" 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" />
其中,
valueFrom
和valueTo
是矢量动画的关键,分别指定了矢量图的起始路径。这里的矢量指令是
SVG
标准指令,规则如下:-
代码中的
M
表示MoveTo
;l
表示LineTo;
z` 表示收尾闭合 -
矢量图 path 从一个图形到另一个,
from
和to
的路径必须一致:相同数量的命令和相同数量的每个命令的参数 -
全部的指令使用一个字母表示,如
M
,l
-
指令中的逗号、空格符可以被省略,如
M 100 100 L 200 20
→M100 100L200 200
-
连续使用的相同指令可以被省略,如M 100 200 L 200 100 L -100 -200 → M 100 200 L 200 100 -100 -200
-
大写指令代码绝对坐标,小写指令代码相对坐标
-
Unicode U+0046 FULL STOP (“.”) 是唯一被允许的小数点。如数字
13,000.56
是不合法的 -
进一步的详细细节请参见 SVG Paths
-
-
一个或多个
object animator
,使用<animated-vector>
元素,在res/drawable/
示例代码
res/drawable/face.xml
如下:<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="200dp" android:width="200dp" android:viewportHeight="100" android:viewportWidth="100" > <path android:fillColor="@color/yellow" android:pathData="@string/path_circle"/> <path android:fillColor="@android:color/black" android:pathData="@string/path_face_left_eye"/> <path android:fillColor="@android:color/black" android:pathData="@string/path_face_right_eye"/> <path android:name="mouth" android:strokeColor="@android:color/black" android:strokeWidth="@integer/stroke_width" android:strokeLineCap="round" android:pathData="@string/path_face_mouth_sad"/> </vector>
res/drawable/avd.xml
如下:<?xml version="1.0" encoding="utf-8"?> <animated-vector xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/face" > <target android:name="mouth" android:animation="@anim/smile" /> </animated-vector>
说明:
<target>
标签里面的name
标签指定face.xml
中必须一个name
相吻合具体应用
<ImageView android:id="@+id/image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/avd" />
效果图如下:
image