在Android5.0中新增了一種水波效果控件,叫RippleDrawable,當控件使用RippleDrawable作為背景(android:background)且在控件可以接受點選動作(android:clickable="true")的條件下,當按下或擡起手指時,會出現水波效果。效果如下圖所示:
以上圖檔的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繪制邏輯的一個簡析。 手指移動,擡起,也是相似的一個流程,以後再補充吧~
第一次寫技術部落格,寫的比較簡單,可能會有漏洞,歡迎大家指出~