在Android5.0中新增了一种水波效果控件,叫RippleDrawable,当控件使用RippleDrawable作为背景(android:background)且在控件可以接受点击动作(android:clickable="true")的条件下,当按下或抬起手指时,会出现水波效果。效果如下图所示:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0NXYFhGd192UvwVe0lmdhJ3ZvwFM38CXlZHbvN3cpR2Lc1TPB10QGtWUCpEMJ9CXsxWam9CXwADNvwVZ6l2c052bm9CXUJDT1wkNhVzLcRnbvZ2Lc1TPnp1dw1WYxJFSkZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39DNwQjNykTMxIDOwIDM1EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
以上图片的layout代码如下:
ripple_bg.xml 内容如下:
以上的代码需要sdk版本为21的Eclipse或者Android Studio中编辑,并在Android5.0的虚拟机或真机上运行。
以下从源码的角度分析一下RippleDrawable的逻辑:
如下面的的时序图,以下将从4步简介按下时的效果:
Step 1:
RippleDrawable::onStateChange()
@Override
protected boolean onStateChange(int[] stateSet) {
final boolean changed = super.onStateChange(stateSet);
boolean enabled = false;
boolean pressed = false;
boolean focused = false;
for (int state : stateSet) {
if (state == R.attr.state_enabled) {
enabled = true;
}
if (state == R.attr.state_focused) {
focused = true;
}
if (state == R.attr.state_pressed) {
pressed = true;
}
}
setRippleActive(enabled && pressed);
setBackgroundActive(focused || (enabled && pressed));
return changed;
}
以上代码中,又调用了setRippleActive()方法
private void setRippleActive(boolean active) {
if (mRippleActive != active) {
mRippleActive = active;
if (active) {
tryRippleEnter();
} else {
tryRippleExit();
}
}
}
而在setRippleActive()中,又调用了tryRippleEnter()方法
/**
* Attempts to start an enter animation for the active hotspot. Fails if
* there are too many animating ripples.
*/
private void tryRippleEnter() {
if (mExitingRipplesCount >= MAX_RIPPLES) {
// This should never happen unless the user is tapping like a maniac
// or there is a bug that's preventing ripples from being removed.
return;
}
if (mRipple == null) {
final float x;
final float y;
if (mHasPending) {
mHasPending = false;
x = mPendingX;
y = mPendingY;
} else {
x = mHotspotBounds.exactCenterX();
y = mHotspotBounds.exactCenterY();
}
mRipple = new Ripple(this, mHotspotBounds, x, y);
}
final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT);
mRipple.setup(mState.mMaxRadius, color, mDensity);
mRipple.enter();
}
Step 2:
在tryRippleEnter中,首先调用Ripple的setup方法,主要是传入最大半径(maxRadius),水波的颜色(color)和屏幕密度(density),并初始化水波的圆心半径等。
public void setup(int maxRadius, int color, float density) {
mColorOpaque = color | 0xFF000000;
if (maxRadius != RippleDrawable.RADIUS_AUTO) {
mHasMaxRadius = true;
mOuterRadius = maxRadius;
} else {
final float halfWidth = mBounds.width() / 2.0f;
final float halfHeight = mBounds.height() / 2.0f;
mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
}
mOuterX = 0;
mOuterY = 0;
mDensity = density;
clampStartingPosition();
}
接着才开始真正的动画逻辑: Ripple::enter()
/**
* Starts the enter animation.
*/
public void enter() {
cancel();
final int radiusDuration = (int)
(1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5);
final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
radius.setAutoCancel(true);
radius.setDuration(radiusDuration);
radius.setInterpolator(LINEAR_INTERPOLATOR);
radius.setStartDelay(RIPPLE_ENTER_DELAY);
final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1);
cX.setAutoCancel(true);
cX.setDuration(radiusDuration);
cX.setInterpolator(LINEAR_INTERPOLATOR);
cX.setStartDelay(RIPPLE_ENTER_DELAY);
final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1);
cY.setAutoCancel(true);
cY.setDuration(radiusDuration);
cY.setInterpolator(LINEAR_INTERPOLATOR);
cY.setStartDelay(RIPPLE_ENTER_DELAY);
mAnimRadius = radius;
mAnimX = cX;
mAnimY = cY;
// Enter animations always run on the UI thread, since it's unlikely
// that anything interesting is happening until the user lifts their
// finger.
radius.start();
cX.start();
cY.start();
}
在上面的代码中,主要是实例化了三个ObjectAnimator,然后分别给它们设置时长(setDuration()),动画变化速率(setInterpolator())和开始延时(setStartDelay()),最后调用start()函数使得动画开始。
Step 3:
那么,什么是ObjectAnimator呢?简单说就是在规定时间内,按照指定的动画变化速率调用指定类的对应的setter方法(setter方法就是以set开头,并将传入的属性名首字母大写后拼接而成的函数),从而实现让类的成员变量渐变的效果。 比如 ObjectAnimator . ofFloat ( this , "radiusGravity" , 1 );这个ObjectAnimator,就是调用当前类(Ripple.java)的setRadiusGravity方法,并传入由0到1的参数。
@SuppressWarnings("unused")
public void setRadiusGravity(float r) {
mTweenRadius = r;
invalidateSelf();
}
这里为什么是0到1呢?这是因为,ObjectAnimator其实还会调用getter方法,也就是getRadiusGravity方法获取到初始值,
@SuppressWarnings("unused")
public float getRadiusGravity() {
return mTweenRadius;
}
在这个类中,mTweenRadius的初始值就是0:private float mTweenRadius = 0; 因此说是由0到1的渐变。 而传入的动画变化速率是LINEAR_INTERPOLATOR,这是一个线性变化的速率,因此当按下时,水波是匀速散开的。
Step 4:
最后一部是水波的绘制,在Step3的 setRadiusGravity()方法中可以看到,每次修改完成员变量的值之后,都会调用invalidateSelf()方法,这个方法其实就是通知系统,可以开始绘制了,最终是会调用到Ripple:draw():
/**
* Draws the ripple centered at (0,0) using the specified paint.
*/
public boolean draw(Canvas c, Paint p) {
final boolean canUseHardware = c.isHardwareAccelerated();
if (mCanUseHardware != canUseHardware && mCanUseHardware) {
// We've switched from hardware to non-hardware mode. Panic.
cancelHardwareAnimations(true);
}
mCanUseHardware = canUseHardware;
final boolean hasContent;
if (canUseHardware && mHardwareAnimating) {
hasContent = drawHardware((HardwareCanvas) c);
} else {
hasContent = drawSoftware(c, p);
}
return hasContent;
}
在以上的函数中 ,会根据当前的Canvas是否打开硬件加速选择执行 drawHardware()或者drawSoftware()。 这里以drawSoftware()为例看一下水波是怎么绘制的:
private boolean drawSoftware(Canvas c, Paint p) {
boolean hasContent = false;
p.setColor(mColorOpaque);
final int alpha = (int) (255 * mOpacity + 0.5f);
final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
if (alpha > 0 && radius > 0) {
final float x = MathUtils.lerp(
mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
final float y = MathUtils.lerp(
mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
p.setAlpha(alpha);
p.setStyle(Style.FILL);
c.drawCircle(x, y, radius, p);
hasContent = true;
}
return hasContent;
}
通过以上代码可以看到,”水波“其实就是在给定的Canvas上画一个实心圆,这个圆的透明度,半径,圆心是渐变的而已。 以上便是对手指按下时,RippleDrawable绘制逻辑的一个简析。 手指移动,抬起,也是相似的一个流程,以后再补充吧~
第一次写技术博客,写的比较简单,可能会有漏洞,欢迎大家指出~