一、回顧
hello,這節(jié)接著上一節(jié)介紹RippleDrawable的水波實現(xiàn)效果,順便帶著大家自己動手實現(xiàn)一款帶水波的自定義view。好了廢話不多說,還是像往常一樣,先用一個demo來回顧水波的使用:
定義一個水波的xml:
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/colorPrimary">
<item
android:id="@android:id/mask"
android:drawable="@android:color/white" />
<item android:drawable="@color/cccccc" />
</ripple>
然后在view上可以這么使用:
這里沒用foreground屬性是因為在前面介紹了foreground是前置背景,因此用了background屬性來代替,在android中drawable顯示到view上的過程里說過
background屬性和foreground屬性的如果有點擊效果,需要設置view.setClickable(true)或者view.setOnClickListener。下面正式進入正片:
代碼都是在android-27下分析,在android-28下的點擊波紋效果還不太一樣,這里先申明下
二、概述
-
RippleDrawable里面通過RippleForeground和RippleBackground兩個類的動畫來控制水波畫圓的半徑和圓心的位置,以及畫圓的透明度 -
RippleForeground和RippleBackground是RippleComponent的子類,在RippleDrawble的繪制部分會先去畫RippleDrawale的item部分,并且該item部分的id不是mask。緊接著繪制RippleBackground部分,如果RippleBackground是isVisible才會去繪制,后面會講到什么時候是isVisible;緊接著繪制exit的時候沒有繪制完的rippleForeground動畫,所以在連續(xù)點得很快的時候,會有一層一層波紋的效果。 -
RippleForeground創(chuàng)建了softWare和hardWare的動畫,默認情況下,如果rippleDrawable是isBound,RippleForeground的enterSoftWare動畫是不創(chuàng)建的(注意:enter不創(chuàng)建該動畫是在27上面的,也就是手按下的時候),我在28上面看到的動畫效果在按下的時候就有波紋效果,因此可以猜測28上面在按下的時候是創(chuàng)建了enterSoftWare動畫的。 -
RippleBackground中也是創(chuàng)建了softWare和hardWare動畫,而RippleBackground中創(chuàng)建動畫的前提是view中的canvas.isHardwareAccelerated(),才能去繪制drawHardWare動畫,默認情況下是沒開啟硬件加速的情況,因此drawHardWare動畫是不會繪制的。 -
RippleForeground#createSoftwareEnter
融合了三個動畫,有水波半徑的增大、圓心漸變、透明度漸變的動畫。 -
RippleForeground#createSoftwareExit
融合了三個動畫,有水波半徑的增大、圓心漸變、透明度漸變的動畫。和enter的區(qū)別就是enter的透明度是0到1,而exit的透明度是1到0的過程。 -
RippleForeground#drawSoftware
該處是繪制的關鍵,主要在繪制的時候改變畫筆的透明度、繪制圓的圓心、改變圓的半徑大小。 -
RippleBackground#drawSoftware
在它的繪制里面就是畫的一個固定的圓,圓心始終是(0,0),半徑大小不變。 - 在手按下view和抬起view的時候,繪制流程是首先觸發(fā)
RippleDrawable的onStateChange方法,會調用RippleForeground的enter和setup方法,隨后創(chuàng)建了softWare的動畫,在動畫里面不斷地調用了RippleDrawable的invalidateSelf方法,然后會觸發(fā)RippleForeground和RippleBackground的draw方法,隨即到父類RippleComponent的draw方法,而RippleComponent方法會觸發(fā)drawSoftWare方法,最終到RippleForeground的drawSoftWare方法。
三、RippleDrawable的初始化
3.1 RippleDrawable#inflate
還記得在第一篇介紹drawable的時候,說過drawable初始化是從inflate方法開始的不,知道這個直接看RippleDrawable的初始化,在inflate方法中調用了父類的inflate方法和updateStateFromTypedArray方法:
private void updateStateFromTypedArray(@NonNull TypedArray a) throws XmlPullParserException {
//RippleState是RippleDrawable的子類,繼承自父類LayerDrawable的LayerState
final RippleState state = mState;
//看到了沒,上面例子中為什么要定義一個ripple_color.xml,這里就是獲取到一個ColorStateList
final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color);
//獲取到的ColorStateList交給了RippleState.mColor
if (color != null) {
mState.mColor = color;
}
//獲取一個半徑的屬性,在demo里面沒設置,所以這里用默認的mState.mMaxRadius的值
mState.mMaxRadius = a.getDimensionPixelSize(
R.styleable.RippleDrawable_radius, mState.mMaxRadius);
}
初始化中將獲取到ripple標簽的color屬性和radius屬性,賦值給了RippleState。
3.2 RippleDrawable#inflateLayers
再來看下父類的inflate方法,這個得去LayerDrawable的inflate方法,該方法中調用了inflateLayers方法,用來初始化里面的item:
private void inflateLayers(@NonNull Resources r, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, @Nullable Theme theme)
throws XmlPullParserException, IOException {
final LayerState state = mLayerState;
final int innerDepth = parser.getDepth() + 1;
int type;
int depth;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
if (type != XmlPullParser.START_TAG) {
continue;
}
if (depth > innerDepth || !parser.getName().equals("item")) {
continue;
}
final ChildDrawable layer = new ChildDrawable(state.mDensity);
final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.LayerDrawableItem);
//此處是解析item屬性的地方
updateLayerFromTypedArray(layer, a);
a.recycle();
if (layer.mDrawable == null && (layer.mThemeAttrs == null ||
layer.mThemeAttrs[R.styleable.LayerDrawableItem_drawable] == 0)) {
//如果item標簽定義的是drawable的xml文件調走這里
layer.mDrawable = Drawable.createFromXmlInner(r, parser, attrs, theme);
layer.mDrawable.setCallback(this);
state.mChildrenChangingConfigurations |=
layer.mDrawable.getChangingConfigurations();
}
//將每一個ChildDrawable添加到LayerState中
addLayer(layer);
}
}
3.3 RippleDrawable#addLayer
可以看到如果標簽是item生成一個ChildDrawable對象,解析item在updateLayerFromTypedArray方法里:
private void updateLayerFromTypedArray(@NonNull ChildDrawable layer, @NonNull TypedArray a) {
final LayerState state = mLayerState;
final int N = a.getIndexCount();
for (int i = 0; i < N; i++) {
final int attr = a.getIndex(i);
switch (attr) {
//獲取id,省略了其他屬性的獲取,這里就不介紹了,大家自己嘗試
case R.styleable.LayerDrawableItem_id:
layer.mId = a.getResourceId(attr, layer.mId);
break;
}
}
//獲取drawable屬性
final Drawable dr = a.getDrawable(R.styleable.LayerDrawableItem_drawable);
if (dr != null) {
if (layer.mDrawable != null) {
layer.mDrawable.setCallback(null);
}
//將獲取到的drawable值放到ChildDrawable中
layer.mDrawable = dr;
layer.mDrawable.setCallback(this);
state.mChildrenChangingConfigurations |=
layer.mDrawable.getChangingConfigurations();
}
}
該方法里面先是遍歷除了drawable值以外,其他的屬性都獲取了,比如id屬性,還有其他的比如width、gravity屬性等就不說了,大家自己嘗試。
緊接著就是獲取到drawable屬性值,將drawable值放到ChildDrawable中。updateLayerFromTypedArray完事了后,緊接著最后就是addLayer了,這個其實跟上一節(jié)介紹StateListDrawable的addState類似:
int addLayer(@NonNull ChildDrawable layer) {
final LayerState st = mLayerState;
final int N = st.mChildren != null ? st.mChildren.length : 0;
final int i = st.mNumChildren;
if (i >= N) {
final ChildDrawable[] nu = new ChildDrawable[N + 10];
if (i > 0) {
//數(shù)組擴容到10個元素的大小
System.arraycopy(st.mChildren, 0, nu, 0, i);
}
st.mChildren = nu;
}
將上面生成的ChildDrawable放到了LayerState的mChildren數(shù)組中
st.mChildren[i] = layer;
st.mNumChildren++;
st.invalidateCache();
return i;
}
在addLayer方法中也是將LayerState中的mChildren數(shù)組擴容到10個元素的大小,然后將傳過來的ChildDrawable放到了LayerState的mChildren數(shù)組中。到此,RippleDrawable的初始化講解完了,我們來回顧下:
- 在
inflate方法中首先調用了父類LayerDrawable的inflate方法,在inflate方法中解析每一個item標簽,每一個item標簽對應一個ChildDrawable,其中解析完了id等屬性之后,緊接著解析drawable屬性的值,將屬性值依次放到ChildDrawable中。- 將上面解析好的
ChildDrawable依次添加到LayerDrawable中的LayerState數(shù)組mChildren里。- 在
RippleDrawable中的inflate方法中,初始化了ripple標簽中的color和radius屬性值,然后放到RippleState中。
3.5 初始化mask部分
初始化mask需要到ppleDrawable.updateLocalState法看下:
private void updateLocalState() {
// Initialize from constant state.
mMask = findDrawableByLayerId(R.id.mask);
}
public Drawable findDrawableByLayerId(int id) {
final ChildDrawable[] layers = mLayerState.mChildren;
for (int i = mLayerState.mNumChildren - 1; i >= 0; i--) {
if (layers[i].mId == id) {
return layers[i].mDrawable;
}
}
return null;
}
上面兩個方法不用解釋了吧,獲取id=R.id.mask的layer,講獲取到的drawable放到mMask全局drawale里面,后面繪制會用到。
四、RippleDrawable的繪制
4.1 RippleDrawable#draw
關于drawable的繪制,直接看RippleDrawable的draw方法:
@Override
public void draw(@NonNull Canvas canvas) {
pruneRipples();
// Clip to the dirty bounds, which will be the drawable bounds if we
// have a mask or content and the ripple bounds if we're projecting.
final Rect bounds = getDirtyBounds();
//先保存canvas的狀態(tài)
final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
//裁剪drawable的區(qū)域
canvas.clipRect(bounds);
//繪制content部分
drawContent(canvas);
//繪制波紋部分
drawBackgroundAndRipples(canvas);
還原canvas的狀態(tài)
canvas.restoreToCount(saveCount);
}
4.2 RippleDrawable#drawContent
private void drawContent(Canvas canvas) {
// Draw everything except the mask.
final ChildDrawable[] array = mLayerState.mChildren;
final int count = mLayerState.mNumChildren;
for (int i = 0; i < count; i++) {
if (array[i].mId != R.id.mask) {
array[i].mDrawable.draw(canvas);
}
}
}
很清晰,直接繪制item的id不是mask的drawable。在開篇的事例中,不帶id=mask的drawable="#cccccc",此處是一個colorDrawable。
4.3 繪制background、Ripples部分
這部分是波紋效果的關鍵,看下drawBackgroundAndRipples方法:
private void drawBackgroundAndRipples(Canvas canvas) {
//繪制水波的動畫類
final RippleForeground active = mRipple;
//繪制背景的動畫類
final RippleBackground background = mBackground;
//抬起的次數(shù)
final int count = mExitingRipplesCount;
if (active == null && count <= 0 && (background == null || !background.isVisible())) {
return;
}
//獲取到點擊時的坐標
final float x = mHotspotBounds.exactCenterX();
final float y = mHotspotBounds.exactCenterY();
//將畫布偏移到點擊的坐標位置
canvas.translate(x, y);
//繪制mask部分
updateMaskShaderIfNeeded();
// Position the shader to account for canvas translation.
if (mMaskShader != null) {
final Rect bounds = getBounds();
mMaskMatrix.setTranslate(bounds.left - x, bounds.top - y);
mMaskShader.setLocalMatrix(mMaskMatrix);
}
//如果在ripple標簽的color屬性值的顏色沒有透明度,默認透明度是255/2
//得到alpha值后的一半,再往左移24位正好是得到透明度的16進制值
//11111111 11111111 11111111 11111111
// alpha值左移24位跑到最前面去了
final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
final int halfAlpha = (Color.alpha(color) / 2) << 24;
final Paint p = getRipplePaint();
//默認為空
if (mMaskColorFilter != null) {
final int fullAlphaColor = color | (0xFF << 24);
mMaskColorFilter.setColor(fullAlphaColor);
p.setColor(halfAlpha);
p.setColorFilter(mMaskColorFilter);
p.setShader(mMaskShader);
} else {
//color值位與之后再與alpha值進行或運算
final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha;
p.setColor(halfAlphaColor);
p.setColorFilter(null);
p.setShader(null);
}
//如果background不為空,并且isVisible才去繪制background
if (background != null && background.isVisible()) {
background.draw(canvas, p);
}
//將每一次exit的ripple依次繪制出來,可以看出來該處是繪制波紋效果的關鍵,
if (count > 0) {
final RippleForeground[] ripples = mExitingRipples;
for (int i = 0; i < count; i++) {
ripples[i].draw(canvas, p);
}
}
//當前次的rippleForeground繪制
if (active != null) {
active.draw(canvas, p);
}
//還原畫布的偏移量
canvas.translate(-x, -y);
}
上面在繪制ripple和background:
- 獲取到點擊時候的坐標
- 偏移畫布的坐標到點擊的坐標
- 繪制mask部分
- 獲取ripple的color屬性的值,并將color的alpha值減小一半
- 如果background不為空,并且background.isVisible才繪制background
- 將每一次exit的ripple依次繪制出來,如果連續(xù)點擊的話,會出現(xiàn)水波一層一層的效果,該處就是繪制一層一層的效果
- 繪制當前次的rippleForeground
- 還原畫布的偏移量
4.3.1 繪制mask部分
private void updateMaskShaderIfNeeded() {
//省略一些空判斷
//獲取maskType
final int maskType = getMaskType();
if (mMaskBuffer == null
|| mMaskBuffer.getWidth() != bounds.width()
|| mMaskBuffer.getHeight() != bounds.height()) {
if (mMaskBuffer != null) {
mMaskBuffer.recycle();
}
//創(chuàng)建mask部分畫布需要的bitmap
mMaskBuffer = Bitmap.createBitmap(
bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8);
//將mask部分的bitmap放到bitmapShader上面,后面會用到ripple上面
mMaskShader = new BitmapShader(mMaskBuffer,
Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
//創(chuàng)建mask部分的畫布
mMaskCanvas = new Canvas(mMaskBuffer);
} else {
mMaskBuffer.eraseColor(Color.TRANSPARENT);
}
if (mMaskMatrix == null) {
mMaskMatrix = new Matrix();
} else {
mMaskMatrix.reset();
}
//創(chuàng)建了PorterDuffColorFilter,后面繪制riiple的時候會用到
if (mMaskColorFilter == null) {
mMaskColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_IN);
}
final int top = bounds.top;
mMaskCanvas.translate(-left, -top);
//默認情況下maskType=MASK_NONE,大家可以看下getMaskType怎么獲取的
if (maskType == MASK_EXPLICIT) {
drawMask(mMaskCanvas);
} else if (maskType == MASK_CONTENT) {
drawContent(mMaskCanvas);
}
mMaskCanvas.translate(left, top);
}
- 獲取到mask部分的maskType,如果mask部分的drawable顏色值透明度是255,獲取到的maskType=MASK_NONE,否則maskType=MASK_EXPLICIT
- 生成
mMaskBuffer、mMaskShader、mMaskCanvas,創(chuàng)建了mMaskColorFilter,關于PorterDuffColorFilter的應用,在StateListDrawable部分有提到過,此處使用SRC_IN模式,說明mask部分在要繪制的下面。 - 由于我們分析過maskType=MASK_NONE,所以不會繪制mask部分,直接將
mMaskShader傳給ripple部分。
從上面看我們繪制background的條件是不為空,并且是isVisible,此處可不是view中的visible的意思:
public boolean isVisible() {
return mOpacity > 0 || isHardwareAnimating();
}
mOpacity在點擊的時候繪制透明度變化的一個變量,從0到1和1到0變化的過程,isHardwareAnimating也很簡單:
protected final boolean isHardwareAnimating() {
return mHardwareAnimator != null && mHardwareAnimator.isRunning()
|| mHasPendingHardwareAnimator;
}
表示mHardwareAnimator正在進行中,先姑且不管,后面我們再看該動畫是什么意思。
我們看下mExitingRipples是在什么付的值:
//該方法是在手抬起的時候繪制的,實際是在exit的時候,將mRipple賦值給mExitingRipples數(shù)組,并且將數(shù)組自增1。調用完了exit后,將mRipple至為空
private void tryRippleExit() {
if (mRipple != null) {
if (mExitingRipples == null) {
mExitingRipples = new RippleForeground[MAX_RIPPLES];
}
mExitingRipples[mExitingRipplesCount++] = mRipple;
mRipple.exit();
mRipple = null;
}
}
關于rippleDrawable靜態(tài)繪制部分就先說到這里,下面到rippleDrawable動態(tài)繪制部分。
4.4 觸摸繪制
在第一節(jié)view的ontouchEvent觸發(fā)后,緊接著會觸發(fā)drawable的setState方法,在setState中會觸發(fā)drawable的onStateChange方法,直接看RippleDrawable的onStateChange方法:
@Override
protected boolean onStateChange(int[] stateSet) {
final boolean changed = super.onStateChange(stateSet);
boolean enabled = false;
boolean pressed = false;
boolean focused = false;
boolean hovered = false;
for (int state : stateSet) {
if (state == R.attr.state_enabled) {
enabled = true;
} else if (state == R.attr.state_focused) {
focused = true;
} else if (state == R.attr.state_pressed) {
pressed = true;
} else if (state == R.attr.state_hovered) {
hovered = true;
}
}
//既按下了又是enable狀態(tài)
setRippleActive(enabled && pressed);
setBackgroundActive(hovered || focused || (enabled && pressed), focused || hovered);
return changed;
}
onStateChange邏輯很清晰,在enable并且pressed狀態(tài)下會觸發(fā)setRippleActive和setBackgroundActive方法,先來看下setRippleActive方法是干嘛的:
private void setRippleActive(boolean active) {
if (mRippleActive != active) {
mRippleActive = active;
if (active) {
//按下的時候調用該方法
tryRippleEnter();
} else {
//抬起的時候調用該方法
tryRippleExit();
}
}
}
按下的時候調用了tryRippleEnter方法,抬起的時候調用了tryRippleExit方法:
private void tryRippleEnter() {
//限制了ripple最大的次數(shù)
if (mExitingRipplesCount >= MAX_RIPPLES) {
return;
}
if (mRipple == null) {
final float x;
final float y;
//mHasPending在按下的時候為true,
if (mHasPending) {
mHasPending = false;
//按下時候的坐標
x = mPendingX;
y = mPendingY;
} else {
//后面的坐標用mHotspotBounds里面的坐標
x = mHotspotBounds.exactCenterX();
y = mHotspotBounds.exactCenterY();
}
final boolean isBounded = isBounded();
//生成了一個RippleForeground
mRipple = new RippleForeground(this, mHotspotBounds, x, y, isBounded, mForceSoftware);
}
//緊接著調用了setUp和enter方法
mRipple.setup(mState.mMaxRadius, mDensity);
mRipple.enter(false);
}
rippleEnter里面的邏輯還是挺清晰的,先是判斷RippleForeground是否為空,將按下時候的x、y的坐標傳給RippleForeground,緊接著調用了setUp和enter方法,RippleForeground是繼承自RippleComponent,setUp和enter方法都是父類中定義的,看下這兩個方法的定義:
public final void setup(float maxRadius, int densityDpi) {
//默認maxRadius=-1,因此走else里面的邏輯
if (maxRadius >= 0) {
mHasMaxRadius = true;
mTargetRadius = maxRadius;
} else {
mTargetRadius = getTargetRadius(mBounds);
}
//縮放的單位密度
mDensityScale = densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
onTargetRadiusChanged(mTargetRadius);
}
五、動畫部分
5.1 RippleForeground的動畫
默認傳過來的maxRadius=-1,因此通過getTargetRadius得到mTargetRadius,getTargetRadius里面通過勾股定理得到view大小的對角線的一半。最后調用了onTargetRadiusChanged方法,該方法是個空方法,可以想到是交給子類自己去處理mTargetRadius的問題,緊接著看下enter方法做了些什么:
public final void enter(boolean fast) {
cancel();
mSoftwareAnimator = createSoftwareEnter(fast);
if (mSoftwareAnimator != null) {
mSoftwareAnimator.start();
}
}
先是取消之前的動畫,緊接著在通過createSoftwareEnter方法創(chuàng)建了mSoftwareAnimator動畫,最后是啟動動畫。createSoftwareEnter是一個抽象的方法,來到RippleForeground看下該方法:
@Override
protected Animator createSoftwareEnter(boolean fast) {
// Bounded ripples don't have enter animations.
//注釋說得很清楚,如果當前rippleDrawable是bounded直接返回null,也就是按下的時候沒有動畫
if (mIsBounded) {
return null;
}
//動畫時間會根據(jù)mTargetRadius成正比
final int duration = (int)
(1000 * Math.sqrt(mTargetRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensityScale) + 0.5);
//radius動畫
final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1);
tweenRadius.setAutoCancel(true);
tweenRadius.setDuration(duration);
tweenRadius.setInterpolator(LINEAR_INTERPOLATOR);
tweenRadius.setStartDelay(RIPPLE_ENTER_DELAY);
//水波畫圓的時候圓心動畫,從點擊的點到rippleDrawable中心位置一直到點擊的點到rippleDrawable中心位置的0.7的圓心漸變動畫
final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1);
tweenOrigin.setAutoCancel(true);
tweenOrigin.setDuration(duration);
tweenOrigin.setInterpolator(LINEAR_INTERPOLATOR);
tweenOrigin.setStartDelay(RIPPLE_ENTER_DELAY);
//透明度的動畫
final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
opacity.setAutoCancel(true);
opacity.setDuration(OPACITY_ENTER_DURATION_FAST);
opacity.setInterpolator(LINEAR_INTERPOLATOR);
final AnimatorSet set = new AnimatorSet();
set.play(tweenOrigin).with(tweenRadius).with(opacity);
return set;
}
在enterSoftware動畫里面,先是判斷是不是bounds,此處的isBound是從rippleDrawable中傳過來的:
private boolean isBounded() {
return getNumberOfLayers() > 0;
}
也就是通過RippleState中的mNumChildren個數(shù)大于0來判斷的,在上面初始化過程中已經分析過了,addLayer方法添加的個數(shù)實際是通過xml中的item個數(shù)來添加的,因此一般情況下都是isBounded的,除非在ripple標簽里面不定義item標簽。
雖然在softWareEnter里面一般都是return null,但是后面的動畫,還是分析下,因為在softWareExit中還是定義這三個動畫:
- tweenRadius定義水波畫圓的時候半徑的動畫
- tweenOrigin定義水波畫圓的時候圓心的動畫
- opacity定義水波透明度的動畫、
上面三個動畫都用到了動畫的Property形式實現(xiàn)當前類值的改變,都是從0到1的過程,在tweenRadius動畫中不斷改變RippleForeground中的mTweenRadius變量,在tweenOrigin動畫中不斷改變mTweenX和mTweenX全局變量,opacity動畫中不斷改變mOpacity全局變量。并且在動畫的setValue方法中都會調用invalidateSelf方法,最終會重新調用到rippleDrawable的invalidateSelf方法,在第一節(jié)中簡單提過invalidateSelf方法,最終會觸發(fā)drawable的draw方法,因此可以想到實際上rippleForeground中的動畫會不斷調用到RippleComponent的draw方法:
public boolean draw(Canvas c, Paint p) {
//如果canvas是hardwareAccelerated模式才會走hardWare的動畫,默認直接跳過
final boolean hasDisplayListCanvas = !mForceSoftware && c.isHardwareAccelerated()
&& c instance DisplayListCanvas;
if (mHasDisplayListCanvas != hasDisplayListCanvas) {
mHasDisplayListCanvas = hasDisplayListCanvas;
if (!hasDisplayListCanvas) {
// We've switched from hardware to non-hardware mode. Panic.
endHardwareAnimations();
}
}
if (hasDisplayListCanvas) {
final DisplayListCanvas hw = (DisplayListCanvas) c;
startPendingAnimation(hw, p);
if (mHardwareAnimator != null) {
return drawHardware(hw);
}
}
//默認會去繪制softWare部分
return drawSoftware(c, p);
}
在RippleComponent的draw方法里面,如果沒開啟硬件加速,hardWare動畫是沒有打開的,因此直接看drawSoftware部分,drawSoftware在RippleComponent里面是抽象方法,因此還是得需要到子類RippleForeground里面看下:
@Override
protected boolean drawSoftware(Canvas c, Paint p) {
boolean hasContent = false;
//獲取到畫筆最開始的透明度,透明度是ripple標簽color顏色值透明度的一半,這個在rippleDrawable靜態(tài)繪制部分已經講過
final int origAlpha = p.getAlpha();
final int alpha = (int) (origAlpha * mOpacity + 0.5f);
//獲取到當前的圓的半徑
final float radius = getCurrentRadius();
if (alpha > 0 && radius > 0) {
//獲取圓心的位置
final float x = getCurrentX();
final float y = getCurrentY();
p.setAlpha(alpha);
c.drawCircle(x, y, radius, p);
p.setAlpha(origAlpha);
hasContent = true;
}
return hasContent;
}
上面通過mOpacity算出當前畫筆的透明度,這里用了一個+0.5f轉成int類型,這個是很常用的float轉int類型的計算方式吧,通常在現(xiàn)有基礎上+0.5f。mOpacity變量是在opacity動畫中通過它的property改變全局屬性的方式,關于動畫大家可以看看property的使用,這里用到的是FloatProperty的類型:
/**
* Property for animating opacity between 0 and its target value.
*/
private static final FloatProperty<RippleForeground> OPACITY =
new FloatProperty<RippleForeground>("opacity") {
@Override
public void setValue(RippleForeground object, float value) {
object.mOpacity = value;
object.invalidateSelf();
}
@Override
public Float get(RippleForeground object) {
return object.mOpacity;
}
};
關于動畫網上的用法很多,大家可以自己嘗試寫些動畫,在上面動畫中setValue中,調用了object.invalidateSelf方法,這個就是不斷遞歸調用到RippleDrawable的draw方法的原因,其實說白了最終會調用view的draw方法。
getCurrentRadius方法是獲取當前radius:
private float getCurrentRadius() {
return MathUtils.lerp(0, mTargetRadius, mTweenRadius);
}
這里是android的MathUtils工具類,差值器的利用,前面兩個參數(shù)起始值和終止值,第三個三處是百分比。
getCurrentX和getCurrentY方法也是和圓心的獲取是類似的,說完了enter部分的softWare部分,我們來看下exit部分,上面已經分析了exit得從tryRippleExit方法說起:
private void tryRippleExit() {
if (mRipple != null) {
if (mExitingRipples == null) {
mExitingRipples = new RippleForeground[MAX_RIPPLES];
}
//將每一次的rippleForground存起來,在draw方法中繪制完未繪制完的rippleForground
mExitingRipples[mExitingRipplesCount++] = mRipple;
mRipple.exit();
mRipple = null;
}
}
mRipple.exit()會觸發(fā)到rippleForground的createSoftwareExit的動畫,這里就不貼出創(chuàng)建動畫的代碼,簡單說下:
看到了沒,這里跟enter的動畫區(qū)別是,如果isBounded會往下走創(chuàng)建動畫的,而上面分析enter的時候,默認是isbounded直接return了,因此看不到enter的動畫效果的,而我在
android-28的手機上看到按下才有波紋效果,所以還得看下android-28是不是改了enter的邏輯。
5.2 RippleBackground的動畫
說完了RippleForeground的繪制和動畫部分,其實到了Rippleground部分就簡單多了,因為他只有透明度的動畫:
@Override
protected Animator createSoftwareEnter(boolean fast) {
// Linear enter based on current opacity.
final int maxDuration = fast ? OPACITY_ENTER_DURATION_FAST : OPACITY_ENTER_DURATION;
final int duration = (int) ((1 - mOpacity) * maxDuration);
final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
opacity.setAutoCancel(true);
opacity.setDuration(duration);
opacity.setInterpolator(LINEAR_INTERPOLATOR);
return opacity;
}
我去,這里不解釋,直接一個opacity的動畫,好吧,太直觀了點,說完了enter部分的動畫,下面接著看下exit部分的動畫:
@Override
protected Animator createSoftwareExit() {
final AnimatorSet set = new AnimatorSet();
//透明度顯示從1到0
final ObjectAnimator exit = ObjectAnimator.ofFloat(this, RippleBackground.OPACITY, 0);
exit.setInterpolator(LINEAR_INTERPOLATOR);
exit.setDuration(OPACITY_EXIT_DURATION);
exit.setAutoCancel(true);
final AnimatorSet.Builder builder = set.play(exit);
final int fastEnterDuration = mIsBounded ?
(int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST) : 0;
if (fastEnterDuration > 0) {
//這里又從0到1的過程
final ObjectAnimator enter = ObjectAnimator.ofFloat(this, RippleBackground.OPACITY, 1);
enter.setInterpolator(LINEAR_INTERPOLATOR);
enter.setDuration(fastEnterDuration);
enter.setAutoCancel(true);
builder.after(enter);
}
return set;
}
exit動畫分為兩部分,一個透明度從1到0,然后又從0到1的過程,這個分析下來,就是抬起的時候先從不透明到完全透明再到不完全透明的過程。上面用到了動畫集合AnimatorSet.Builder的after方法,這個我也沒用過,從字面意思理解是在上面的exit動畫結束后再執(zhí)行透明度從0到1的enter動畫。
好了,關于RippleForground的繪制、動畫以及RippleBackground繪制和動畫都講完了,RippleForground負責水波的繪制,RippleBackground負責繪制透明度漸變的動畫。
5.3 取消動畫
關于RippleDrawable中的水波動畫,還得需要了解view的銷毀時機,不知道大家平時有沒有重寫一個view的onDetachViewFromWindow方法沒,view上的background和foreground都是在detach的時候進行銷毀,所以RippleDrawable也不例外,先順著view往下看:
void dispatchDetachedFromWindow() {
//一般自定義view的時候重寫該方法,比如釋放動畫等等
onDetachedFromWindow();
//銷毀drawable的地方
onDetachedFromWindowInternal();
}
注釋寫得很清楚,大家在自定義view的時候,是不是有用過onDetachedFromWindow方法,就是由這而來,接著看onDetachedFromWindowInternal方法:
protected void onDetachedFromWindowInternal() {
jumpDrawablesToCurrentState();
}
為了方便大家看代碼,我把代碼精簡到一行代碼,接著往下看:
public void jumpDrawablesToCurrentState() {
if (mBackground != null) {
mBackground.jumpToCurrentState();
}
if (mStateListAnimator != null) {
mStateListAnimator.jumpToCurrentState();
}
if (mDefaultFocusHighlight != null) {
mDefaultFocusHighlight.jumpToCurrentState();
}
if (mForegroundInfo != null && mForegroundInfo.mDrawable != null) {
mForegroundInfo.mDrawable.jumpToCurrentState();
}
}
看到了沒,都是調用了drawable的jumpToCurrentState方法,直接來到RippleDrawable下面的該方法:
@Override
public void jumpToCurrentState() {
super.jumpToCurrentState();
if (mRipple != null) {
mRipple.end();
}
if (mBackground != null) {
mBackground.end();
}
cancelExitingRipples();
}
private void cancelExitingRipples() {
final int count = mExitingRipplesCount;
final RippleForeground[] ripples = mExitingRipples;
for (int i = 0; i < count; i++) {
ripples[i].end();
}
if (ripples != null) {
Arrays.fill(ripples, 0, count, null);
}
mExitingRipplesCount = 0;
// Always draw an additional "clean" frame after canceling animations.
invalidateSelf(false);
}
很一目了然吧,調用了RippleForeground的end、RippleBackground的end以及在cancelExitingRipples方法里面調用了每次exit未完成的RippleForeground的end方法,所以歸根到最后,其實是調用了父類RippleComponent中end方法:
public void end() {
endSoftwareAnimations();
endHardwareAnimations();
}
看到了吧,方法名都擺出來了:
private void endSoftwareAnimations() {
if (mSoftwareAnimator != null) {
mSoftwareAnimator.end();
mSoftwareAnimator = null;
}
}
private void endHardwareAnimations() {
if (mHardwareAnimator != null) {
mHardwareAnimator.end();
mHardwareAnimator = null;
}
}
直接不解釋,關于view從window上detach后到RippleDrawable中動畫停止后就到這里了。
六、總結
我們再來梳理下繪制流程:
-
RippleDrawable在inflate過程初始化了一層層的layer,添加到LayerState里面,初始化mask部分的drawable,放到了mMask全局drawable里面,初始化了ripple標簽里面的color屬性。 -
在RippleDrawable靜態(tài)繪制部分先是繪制了非id=mask的item - mask部分color屬性值alpha=255是不會繪制的,因此顏色值的alpha值需要在[0,255)這個區(qū)間,mask繪制是在rippleForeground和RippleBackground的繪制下層。
- 接著繪制
RippleBackground部分,如果RippleBackground.isVisible才繪制。 - 接著繪制每次
exit未完成的RippleForeground部分,注意這里是個集合遍歷繪制RippleForeground。 - 接著才是繪制當前次的
RippleForeground。 - 在動畫部分,先是觸發(fā)了
RippleDrawable的onStateChange方法,接著創(chuàng)建了RippleForeground,調用了RippleForeground的enter和setup``方法,在enter里面創(chuàng)建了softWare動畫,其中hardWare動畫是要開啟了硬件加速功能才能創(chuàng)建,所以默認不會創(chuàng)建softWare`動畫。 -
RippleForeground中的softWare創(chuàng)建的動畫有三個,一個是半徑、圓心、透明度變化的三個動畫,在enter的時候RippleForeground在RippleDrawable.isBounded的時候不創(chuàng)建動畫;在exit的時候不會限制創(chuàng)建動畫,這個是在android-27下面的源碼。在android-28的手機上面我看下了效果是在enter的時候有水波動畫,exit的時候沒有動畫,大家可以用android-28的手機嘗試下。 -
RippleBackground中就一個動畫,改變畫筆的透明底,enter情況下畫筆從0到1的過程;在exit的時候畫筆的透明度先是從1到0,然后又從0到1的過程。 - 上面提到的
enter和exit中的動畫,都是不斷地調用到RippleDrawable的invalidateSelf方法,而invalidateSelf會觸發(fā)view的draw方法,最后觸發(fā)了RippleDrawable的draw方法,最終會觸發(fā)到RippleForeground的drawSoftware和RippleBackground的drawSoftware。 - RippleDrawable中動畫銷毀是在
view#dispatchdetachedFromWindow到RippleDrawable的jumpToCurrentState方法。