Android材料设计动画之触摸反馈
定制触摸反馈
前言
在Android 5.0版本发布时,所公布的Android材料设计之动画更新幅度很大,有各种各样,各种场景下的动画。动画对于增强用户体验的感受有着超级重要的贡献,为APP适量添加合适的动画,会让APP“生龙活虎”,焕发APP的活力。不仅能给用户带来超棒的视觉享受,也能增强人机交互的能力,最终带给用户一流的用户体验。
在Android 5.0发布了一个触摸反馈的动画,即当用户触摸(点击、长按)应用的控件的时候,会有一个持续一小段时间的动画,表示设备已经接收到这一用户操作便以实时动画反馈用户。
本文的demo继续使用文章《 Android材料设计》一文中的demo。
如何使用触摸反馈
触摸反馈在Android 5.0之前的老版本就有,只不过这是一种静态的反馈,那就是State list,我们把一个控件的状态以seletor为标签定义在xml文件中,如下:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true" android:drawable="@android:color/holo_green_dark"/>
<item android:state_pressed="true" android:drawable="@android:color/holo_green_dark"/>
<item android:drawable="@android:color/holo_blue_bright"/>
</selector>
正常状态时显示的颜色为holo_blue_bright,按下状态显示的颜色为holo_green_dark。在控件不同的状态下,显示不同的背景颜色。
在Android 5.0推出一种新的触摸反馈,波纹动画,较之前的State list,不仅有颜色的变化,还有一个波纹样式的动画持续一小段时间。
在Android 5.0后,不需要任何特殊代码,控件默认采用这种波纹动画作为控件的触摸反馈,以点击的地方为中心,有水波波纹向四周扩散。如下图是一个Button控件默认情况下的点击效果:
定制触摸反馈动画
Android提供的默认的样式往往不能满足我们的需求,这时就需要对样式等进行定制。这里说的定制,不是定制动画的样式,即不能改变波纹动画,而是对动画开始前的颜色,以及波纹的范围和颜色等进行定制。
定制波纹颜色
修改默认的触摸反馈波纹的颜色,重写主题的android:colorControlHighlight 属性即可,如下:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="@style/AppThemeBase">
<!-- Customize your theme here. -->
<!-- 修改波纹颜色,使用主色调colorPrimary #3F51B5 深蓝色 -->
<item name="android:colorControlHighlight">@color/colorPrimary</item>
<item name="android:navigationBarColor">@color/navigationBarColor</item>
</style>
</resources>
效果如下:
定制波纹边界
把控件的background属性的值设置成如下:
- ?android:attr/selectableItemBackground 指定波纹有边界,即波纹只在控件的范围内,默认值。
- ?android:attr/selectableItemBackgroundBorderless 指定越过视图边界的波纹。 它将由一个非空背景的视图的最近父项所绘制和设定边界。API 级别 21 中推出的新属性
有边界的效果就是上一章节中的的图示,越过控件边界的代码如下:
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:textColor="@color/colorPrimary"
android:text="@string/button_text"/>
效果如下:
长按的效果如下:
用xml代替
上两章节中,定义波纹颜色要在主题中,定义波纹范围在控件属性中,这样在修改和维护都不方便,可以把它们都统一写在一个xml文件中。和State list类似的selector类似,波纹动画也可以在drawable目录下创建一个ripple文件,如下:
<ripple android:color="@color/colorPrimary"
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/mask"
android:drawable="@android:color/holo_green_light" />
</ripple>
android:color=”@color/colorPrimary表示波纹的颜色,作用和android:colorControlHighlight一样。
item表示正常状态的颜色,即没有press/focus的状态。
android:id=”@android:id/mask”表示是否显示item的颜色,有android:id=”@android:id/mask”表示默认不显示item的颜色,相反显示item的颜色
如果没有item,表示波纹边界越过控件,类似?android:attr/selectableItemBackgroundBorderless。如下:
<ripple android:color="@color/colorPrimary"
xmlns:android="http://schemas.android.com/apk/res/android">
</ripple>
注意:触摸反馈波纹动画是Android 5.0材料设计的组成部分,所以如果APP需要兼容Android 5.0之前的版本,需要考虑兼容性。需要把相关代码放置value-v21中。
波纹动画的原理
我们知道,一个view有图像需要绘制时,会回调draw()方法,draw()方法再调用Drawable的draw()方法,把图像绘制出来。波纹动画有RippleDrawable负责绘制,RippleDrawable继承LayerDrawable,继承Drawable,当点击一个控件时,调用RippleDrawable的draw()方法绘制波纹,如下:
public void draw(@NonNull Canvas canvas) {
pruneRipples();
......
drawContent(canvas);
//绘制波纹
drawBackgroundAndRipples(canvas);
canvas.restoreToCount(saveCount);
}
这个方法定义在文件frameworks/base/graphics/java/android/graphics/drawable/RippleDrawable.java中。
接着调用drawBackgroundAndRipples()方法,接着看:
private void drawBackgroundAndRipples(Canvas canvas) {
final RippleForeground active = mRipple;
final RippleBackground background = mBackground;
final int count = mExitingRipplesCount;
......
final int halfAlpha = (Color.alpha(color) / 2) << 24;
final Paint p = getRipplePaint();
......
if (background != null && background.isVisible()) {
background.draw(canvas, p);
}
if (count > 0) {
final RippleForeground[] ripples = mExitingRipples;
for (int i = 0; i < count; i++) {
ripples[i].draw(canvas, p);
}
}
......
}
这个方法定义在文件frameworks/base/graphics/java/android/graphics/drawable/RippleDrawable.java中。
上面的代码,有一个RippleBackground和一个RippleForeground,RippleBackground是用于绘制Ripple的背景的,单击控件时,我们看到的波纹动画由RippleForeground绘制。调用getRipplePaint()获取到画波纹的画笔,如果count > 0,就调用ripples[i].draw()方法。count表示波纹的计数,1表示一次波纹,2表示2次波纹,最多10次波纹,这里count = 1。继续往下看RippleForeground的draw()方法:
public boolean draw(Canvas c, Paint p) {
......
if (hasDisplayListCanvas) {
final DisplayListCanvas hw = (DisplayListCanvas) c;
startPendingAnimation(hw, p);
if (mHardwareAnimator != null) {
return drawHardware(hw);
}
}
return drawSoftware(c, p);
}
这个方法定义在文件frameworks/base/graphics/java/android/graphics/drawable/RippleComponent.java中。
上面这个方法,会判断是否用硬件加速,如果是调用drawHardware(hw),如果不是调用drawSoftware(c, p),对于上层,它们没有区别,这两个方法,最终都是调用Canvas的drawCircle()方法,即画一个圆,这个圆表示波纹最先开始的第一道波纹,然而它是静止的,通过调用startPendingAnimation(hw, p)方法,让这个波纹从第一道波纹逐渐向四周扩散,看起来就和现实生活中的波纹的效果一样。startPendingAnimation(hw, p)的实现如下:
private void startPendingAnimation(DisplayListCanvas hw, Paint p) {
if (mHasPendingHardwareAnimator) {
mHasPendingHardwareAnimator = false;
mHardwareAnimator = createHardwareExit(new Paint(p));
mHardwareAnimator.start(hw);
......
}
}
这个方法定义在文件frameworks/base/graphics/java/android/graphics/drawable/RippleComponent.java中。
这里直接调用了createHardwareExit()方法创建一个RenderNodeAnimatorSet对象,然后调用start()方法启动动画,createHardwareExit()的实现如下:
protected RenderNodeAnimatorSet createHardwareExit(Paint p) {
//波纹的幅度,即一个圆
final int radiusDuration;
//波纹持续的时间
final int originDuration;
//波纹的模糊度
final int opacityDuration;
if (mIsBounded) {
computeBoundedTargetValues();
radiusDuration = BOUNDED_RADIUS_EXIT_DURATION;
originDuration = BOUNDED_ORIGIN_EXIT_DURATION;
opacityDuration = BOUNDED_OPACITY_EXIT_DURATION;
} else {
radiusDuration = getRadiusExitDuration();
originDuration = radiusDuration;
opacityDuration = getOpacityExitDuration();
}
//波纹开始的坐标,即点击的地方
final float startX = getCurrentX();
final float startY = getCurrentY();
final float startRadius = getCurrentRadius();
//设置波纹的透明度
p.setAlpha((int) (p.getAlpha() * mOpacity + 0.5f));
mPropPaint = CanvasProperty.createPaint(p);
mPropRadius = CanvasProperty.createFloat(startRadius);
mPropX = CanvasProperty.createFloat(startX);
mPropY = CanvasProperty.createFloat(startY);
final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius);
radius.setDuration(radiusDuration);
radius.setInterpolator(DECELERATE_INTERPOLATOR);
final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mTargetX);
x.setDuration(originDuration);
x.setInterpolator(DECELERATE_INTERPOLATOR);
final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mTargetY);
y.setDuration(originDuration);
y.setInterpolator(DECELERATE_INTERPOLATOR);
final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint,
RenderNodeAnimator.PAINT_ALPHA, 0);
opacity.setDuration(opacityDuration);
opacity.setInterpolator(LINEAR_INTERPOLATOR);
opacity.addListener(mAnimationListener);
final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet();
set.add(radius);
set.add(opacity);
set.add(x);
set.add(y);
return set;
}
这个方法定义在文件frameworks/base/graphics/java/android/graphics/drawable/RippleForeground.java中。
上面的代码对应radius,x,y,opacity实例化一个RenderNodeAnimator对象,表示是一个动画,然后把每个动画add()到动画的集合中,这个和Android其它常用的动画使用是基本类似的。再往下就是动画的原理了,不在本文阐述的范围,读者可以查阅相关的Android动画的显示过程的资料。