hello,這節接着上一節介紹RippleDrawable
的水波實現效果,順便帶着你們本身動手實現一款帶水波的自定義view。好了廢話很少說,仍是像往常同樣,先用一個demo來回顧水波的使用:android
定義一個水波的xml:
canvas
<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上能夠這麼使用:
數組
background
屬性和
foreground
屬性的若是有點擊效果,須要設置
view.setClickable(true)
或者
view.setOnClickListener
。下面正式進入正片:
代碼都是在android-27下分析,在android-28下的點擊波紋效果還不太同樣,這裏先申明下bash
RippleDrawable
裏面經過RippleForeground
和RippleBackground
兩個類的動畫來控制水波畫圓的半徑和圓心的位置,以及畫圓的透明度RippleForeground
和RippleBackground
是RippleComponent的子類,在RippleDrawble的繪製部分會先去畫RippleDrawale的item部分,而且該item部分的id不是mask。緊接着繪製RippleBackground部分,若是RippleBackground是isVisible纔會去繪製,後面會講到何時是isVisible;緊接着繪製exit的時候沒有繪製完的rippleForeground動畫,因此在連續點得很快的時候,會有一層一層波紋的效果。RippleForeground
建立了softWare
和hardWare
的動畫,默認狀況下,若是rippleDrawable
是isBound,RippleForeground
的enterSoftWare
動畫是不建立的(注意:enter不建立該動畫是在27上面的,也就是手按下的時候),我在28上面看到的動畫效果在按下的時候就有波紋效果,所以能夠猜想28上面在按下的時候是建立了enterSoftWare動畫的。RippleBackground
中也是建立了softWare
和hardWare
動畫,而RippleBackground
中建立動畫的前提是view中的canvas.isHardwareAccelerated(),才能去繪製drawHardWare動畫,默認狀況下是沒開啓硬件加速的狀況,所以drawHardWare動畫是不會繪製的。RippleForeground#createSoftwareEnter
融合了三個動畫,有水波半徑的增大、圓心漸變、透明度漸變的動畫。RippleForeground#createSoftwareExit
融合了三個動畫,有水波半徑的增大、圓心漸變、透明度漸變的動畫。和enter的區別就是enter的透明度是0到1,而exit的透明度是1到0的過程。RippleForeground#drawSoftware
該處是繪製的關鍵,主要在繪製的時候改變畫筆的透明度、繪製圓的圓心、改變圓的半徑大小。RippleBackground#drawSoftware
在它的繪製裏面就是畫的一個固定的圓,圓心始終是(0,0),半徑大小不變。RippleDrawable
的onStateChange
方法,會調用RippleForeground
的enter
和setup
方法,隨後建立了softWare
的動畫,在動畫裏面不斷地調用了RippleDrawable
的invalidateSelf
方法,而後會觸發RippleForeground
和RippleBackground
的draw
方法,隨即到父類RippleComponent
的draw方法,而RippleComponent
方法會觸發drawSoftWare
方法,最終到RippleForeground
的drawSoftWare
方法。RippleDrawable#inflate
還記得在第一篇介紹drawable的時候,說過drawable初始化是從inflate方法開始的不,知道這個直接看RippleDrawable的初始化,在inflate方法中調用了父類的inflate方法和updateStateFromTypedArray
方法:ide
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。工具
RippleDrawable#inflateLayers
再來看下父類的inflate方法,這個得去LayerDrawable的inflate方法,該方法中調用了inflateLayers
方法,用來初始化裏面的item:post
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);
}
}
複製代碼
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
了,這個其實跟上一節介紹StateListDrawable
的addState
相似:ui
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) {
//數組擴容到10個元素的大小
System.arraycopy(st.mChildren, 0, nu, 0, i);
}
st.mChildren = nu;
}
將上面生成的ChildDrawable放到了LayerState的mChildren數組中
st.mChildren[i] = layer;
st.mNumChildren++;
st.invalidateCache();
return i;
}
複製代碼
在addLayer方法中也是將LayerState
中的mChildren數組擴容到10個元素的大小,而後將傳過來的ChildDrawable
放到了LayerState
的mChildren
數組中。到此,RippleDrawable的初始化講解完了,咱們來回顧下:this
- 在
inflate
方法中首先調用了父類LayerDrawable
的inflate
方法,在inflate
方法中解析每個item
標籤,每個item
標籤對應一個ChildDrawable
,其中解析完了id等屬性以後,緊接着解析drawable屬性的值,將屬性值依次放到ChildDrawable
中。- 將上面解析好的
ChildDrawable
依次添加到LayerDrawable
中的LayerState
數組mChildren
裏。- 在
RippleDrawable
中的inflate方法中,初始化了ripple
標籤中的color和radius屬性值,而後放到RippleState
中。
初始化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#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的狀態 final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); //裁剪drawable的區域 canvas.clipRect(bounds); //繪製content部分 drawContent(canvas); //繪製波紋部分 drawBackgroundAndRipples(canvas); 還原canvas的狀態 canvas.restoreToCount(saveCount); } 複製代碼
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。
繪製background、Ripples部分
這部分是波紋效果的關鍵,看下drawBackgroundAndRipples方法:
private void drawBackgroundAndRipples(Canvas canvas) {
//繪製水波的動畫類
final RippleForeground active = mRipple;
//繪製背景的動畫類
final RippleBackground background = mBackground;
//擡起的次數
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部分
private void updateMaskShaderIfNeeded() {
//省略一些空判斷
//獲取maskType
final int maskType = getMaskType();
if (mMaskBuffer == null
|| mMaskBuffer.getWidth() != bounds.width()
|| mMaskBuffer.getHeight() != bounds.height()) {
if (mMaskBuffer != null) {
mMaskBuffer.recycle();
}
//建立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);
//建立mask部分的畫布
mMaskCanvas = new Canvas(mMaskBuffer);
} else {
mMaskBuffer.eraseColor(Color.TRANSPARENT);
}
if (mMaskMatrix == null) {
mMaskMatrix = new Matrix();
} else {
mMaskMatrix.reset();
}
//建立了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);
}
複製代碼
mMaskBuffer
、mMaskShader
、mMaskCanvas
,建立了mMaskColorFilter
,關於PorterDuffColorFilter
的應用,在StateListDrawable
部分有提到過,此處使用SRC_IN模式,說明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數組,而且將數組自增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靜態繪製部分就先說到這裏,下面到rippleDrawable動態繪製部分。
觸摸繪製
在第一節view的ontouchEvent觸發後,緊接着會觸發drawable的setState方法,在setState中會觸發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狀態
setRippleActive(enabled && pressed);
setBackgroundActive(hovered || focused || (enabled && pressed), focused || hovered);
return changed;
}
複製代碼
onStateChange
邏輯很清晰,在enable而且pressed狀態下會觸發setRippleActive
和setBackgroundActive
方法,先來看下setRippleActive
方法是幹嗎的:
private void setRippleActive(boolean active) {
if (mRippleActive != active) {
mRippleActive = active;
if (active) {
//按下的時候調用該方法
tryRippleEnter();
} else {
//擡起的時候調用該方法
tryRippleExit();
}
}
}
複製代碼
按下的時候調用了tryRippleEnter
方法,擡起的時候調用了tryRippleExit
方法:
private void tryRippleEnter() {
//限制了ripple最大的次數
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);
}
複製代碼
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
方法建立了mSoftwareAnimator
動畫,最後是啓動動畫。createSoftwareEnter
是一個抽象的方法,來到RippleForeground
看下該方法:
@Override
protected Animator createSoftwareEnter(boolean fast) {
// Bounded ripples don't have enter animations. //註釋說得很清楚,若是當前rippleDrawable是bounded直接返回null,也就是按下的時候沒有動畫 if (mIsBounded) { return null; } //動畫時間會根據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個數大於0來判斷的,在上面初始化過程當中已經分析過了,addLayer方法添加的個數實際是經過xml中的item個數來添加的,所以通常狀況下都是isBounded的,除非在ripple標籤裏面不定義item標籤。
雖然在softWareEnter裏面通常都是return null,可是後面的動畫,仍是分析下,由於在softWareExit中仍是定義這三個動畫:
Property
形式實現當前類值的改變,都是從0到1的過程,在tweenRadius
動畫中不斷改變RippleForeground
中的mTweenRadius
變量,在tweenOrigin
動畫中不斷改變mTweenX
和mTweenX
全局變量,opacity
動畫中不斷改變mOpacity
全局變量。而且在動畫的setValue方法中都會調用invalidateSelf
方法,最終會從新調用到rippleDrawable的invalidateSelf
方法,在第一節中簡單提過invalidateSelf
方法,最終會觸發drawable的draw方法,所以能夠想到實際上rippleForeground中的動畫會不斷調用到RippleComponent
的draw方法:public boolean draw(Canvas c, Paint p) {
//若是canvas是hardwareAccelerated模式纔會走hardWare的動畫,默認直接跳過
final boolean hasDisplayListCanvas = !mForceSoftware && c.isHardwareAccelerated()
&& c instanceof 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靜態繪製部分已經講過
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類型的計算方式吧,一般在現有基礎上+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工具類,差值器的利用,前面兩個參數起始值和終止值,第三個三處是百分比。
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()
會觸發到rippleForground的createSoftwareExit的動畫,這裏就不貼出建立動畫的代碼,簡單說下:
android-28
的手機上看到按下才有波紋效果,因此還得看下
android-28
是否是改了enter的邏輯。
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動畫結束後再執行透明度從0到1的enter動畫。
好了,關於RippleForground的繪製、動畫以及RippleBackground繪製和動畫都講完了,RippleForground負責水波的繪製,RippleBackground負責繪製透明度漸變的動畫。
取消動畫
關於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
靜態繪製部分先是繪製了非id=mask的itemRippleBackground
部分,若是RippleBackground.isVisible才繪製。exit
未完成的RippleForeground
部分,注意這裏是個集合遍歷繪製RippleForeground
。RippleForeground
。RippleDrawable
的onStateChange
方法,接着建立了RippleForeground
,調用了RippleForeground
的enter
和setup``方法,在enter裏面建立了
softWare動畫,其中
hardWare動畫是要開啓了硬件加速功能才能建立,因此默認不會建立
softWare`動畫。RippleForeground
中的softWare
建立的動畫有三個,一個是半徑、圓心、透明度變化的三個動畫,在enter
的時候RippleForeground
在RippleDrawable.isBounded
的時候不建立動畫;在exit
的時候不會限制建立動畫,這個是在android-27
下面的源碼。在android-28
的手機上面我看下了效果是在enter
的時候有水波動畫,exit
的時候沒有動畫,你們能夠用android-28
的手機嘗試下。RippleBackground
中就一個動畫,改變畫筆的透明底,enter
狀況下畫筆從0到1的過程;在exit
的時候畫筆的透明度先是從1到0,而後又從0到1的過程。enter
和exit
中的動畫,都是不斷地調用到RippleDrawable
的invalidateSelf
方法,而invalidateSelf
會觸發view
的draw
方法,最後觸發了RippleDrawable
的draw
方法,最終會觸發到RippleForeground
的drawSoftware
和RippleBackground
的drawSoftware
。view#dispatchdetachedFromWindow
到RippleDrawable
的jumpToCurrentState
方法。