RippleDrawable水波繪製分析

1、回顧

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上能夠這麼使用:數組

這裏沒用foreground屬性是由於在前面介紹了foreground是前置背景,所以用了background屬性來代替,在 android中drawable顯示到view上的過程裏說過 background屬性和 foreground屬性的若是有點擊效果,須要設置 view.setClickable(true)或者 view.setOnClickListener。下面正式進入正片:

代碼都是在android-27下分析,在android-28下的點擊波紋效果還不太同樣,這裏先申明下bash

2、概述

  • RippleDrawable裏面經過RippleForegroundRippleBackground兩個類的動畫來控制水波畫圓的半徑和圓心的位置,以及畫圓的透明度
  • RippleForegroundRippleBackground是RippleComponent的子類,在RippleDrawble的繪製部分會先去畫RippleDrawale的item部分,而且該item部分的id不是mask。緊接着繪製RippleBackground部分,若是RippleBackground是isVisible纔會去繪製,後面會講到何時是isVisible;緊接着繪製exit的時候沒有繪製完的rippleForeground動畫,因此在連續點得很快的時候,會有一層一層波紋的效果。
  • RippleForeground建立了softWarehardWare的動畫,默認狀況下,若是rippleDrawable是isBound,RippleForegroundenterSoftWare動畫是不建立的(注意:enter不建立該動畫是在27上面的,也就是手按下的時候),我在28上面看到的動畫效果在按下的時候就有波紋效果,所以能夠猜想28上面在按下的時候是建立了enterSoftWare動畫的。
  • RippleBackground中也是建立了softWarehardWare動畫,而RippleBackground中建立動畫的前提是view中的canvas.isHardwareAccelerated(),才能去繪製drawHardWare動畫,默認狀況下是沒開啓硬件加速的狀況,所以drawHardWare動畫是不會繪製的。
  • RippleForeground#createSoftwareEnter 融合了三個動畫,有水波半徑的增大、圓心漸變、透明度漸變的動畫。
  • RippleForeground#createSoftwareExit 融合了三個動畫,有水波半徑的增大、圓心漸變、透明度漸變的動畫。和enter的區別就是enter的透明度是0到1,而exit的透明度是1到0的過程。
  • RippleForeground#drawSoftware 該處是繪製的關鍵,主要在繪製的時候改變畫筆的透明度、繪製圓的圓心、改變圓的半徑大小。
  • RippleBackground#drawSoftware 在它的繪製裏面就是畫的一個固定的圓,圓心始終是(0,0),半徑大小不變。
  • 在手按下view和擡起view的時候,繪製流程是首先觸發RippleDrawableonStateChange方法,會調用RippleForegroundentersetup方法,隨後建立了softWare的動畫,在動畫裏面不斷地調用了RippleDrawableinvalidateSelf方法,而後會觸發RippleForegroundRippleBackgrounddraw方法,隨即到父類RippleComponent的draw方法,而RippleComponent方法會觸發drawSoftWare方法,最終到RippleForegrounddrawSoftWare方法。

3、RippleDrawable的初始化

3.1 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。工具

3.2 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);
    }
}
複製代碼

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了,這個其實跟上一節介紹StateListDrawableaddState相似: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放到了LayerStatemChildren數組中。到此,RippleDrawable的初始化講解完了,咱們來回顧下:this

  • inflate方法中首先調用了父類LayerDrawableinflate方法,在inflate方法中解析每個item標籤,每個item標籤對應一個ChildDrawable,其中解析完了id等屬性以後,緊接着解析drawable屬性的值,將屬性值依次放到ChildDrawable中。
  • 將上面解析好的ChildDrawable依次添加到LayerDrawable中的LayerState數組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裏面,後面繪製會用到。

4、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的狀態 final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); //裁剪drawable的區域 canvas.clipRect(bounds); //繪製content部分 drawContent(canvas); //繪製波紋部分 drawBackgroundAndRipples(canvas); 還原canvas的狀態 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;
        //擡起的次數
        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依次繪製出來,若是連續點擊的話,會出現水波一層一層的效果,該處就是繪製一層一層的效果
  • 繪製當前次的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();
        }
        //建立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);
}
複製代碼
  • 獲取到mask部分的maskType,若是mask部分的drawable顏色值透明度是255,獲取到的maskType=MASK_NONE,不然maskType=MASK_EXPLICIT
  • 生成mMaskBuffermMaskShadermMaskCanvas,建立了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數組,而且將數組自增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動態繪製部分。

4.4 觸摸繪製

在第一節view的ontouchEvent觸發後,緊接着會觸發drawable的setState方法,在setState中會觸發drawable的onStateChange方法,直接看RippleDrawableonStateChange方法:

@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狀態下會觸發setRippleActivesetBackgroundActive方法,先來看下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在按下的時候爲trueif (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、動畫部分

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方法建立了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中仍是定義這三個動畫:

  • tweenRadius定義水波畫圓的時候半徑的動畫
  • tweenOrigin定義水波畫圓的時候圓心的動畫
  • opacity定義水波透明度的動畫、 上面三個動畫都用到了動畫的Property形式實現當前類值的改變,都是從0到1的過程,在tweenRadius動畫中不斷改變RippleForeground中的mTweenRadius變量,在tweenOrigin動畫中不斷改變mTweenXmTweenX全局變量,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的動畫,這裏就不貼出建立動畫的代碼,簡單說下:

看到了沒,這裏跟enter的動畫區別是,若是isBounded會往下走建立動畫的,而上面分析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動畫結束後再執行透明度從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);
}
複製代碼

很一目瞭然吧,調用了RippleForegroundendRippleBackgroundend以及在cancelExitingRipples方法裏面調用了每次exit未完成的RippleForeground的end方法,因此歸根到最後,實際上是調用了父類RippleComponentend方法:

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中動畫中止後就到這裏了。

6、總結

咱們再來梳理下繪製流程:

  • RippleDrawableinflate過程初始化了一層層的layer,添加到LayerState裏面,初始化mask部分的drawable,放到了mMask全局drawable裏面,初始化了ripple標籤裏面的color屬性。
  • 在RippleDrawable靜態繪製部分先是繪製了非id=mask的item
  • mask部分color屬性值alpha=255是不會繪製的,所以顏色值的alpha值須要在[0,255)這個區間,mask繪製是在rippleForeground和RippleBackground的繪製下層。
  • 接着繪製RippleBackground部分,若是RippleBackground.isVisible才繪製。
  • 接着繪製每次exit未完成的RippleForeground部分,注意這裏是個集合遍歷繪製RippleForeground
  • 接着纔是繪製當前次的RippleForeground
  • 在動畫部分,先是觸發了RippleDrawableonStateChange方法,接着建立了RippleForeground,調用了RippleForegroundentersetup``方法,在enter裏面建立了softWare動畫,其中hardWare動畫是要開啓了硬件加速功能才能建立,因此默認不會建立softWare`動畫。
  • RippleForeground中的softWare建立的動畫有三個,一個是半徑、圓心、透明度變化的三個動畫,在enter的時候RippleForegroundRippleDrawable.isBounded的時候不建立動畫;在exit的時候不會限制建立動畫,這個是在android-27下面的源碼。在android-28的手機上面我看下了效果是在enter的時候有水波動畫,exit的時候沒有動畫,你們能夠用android-28的手機嘗試下。
  • RippleBackground中就一個動畫,改變畫筆的透明底,enter狀況下畫筆從0到1的過程;在exit的時候畫筆的透明度先是從1到0,而後又從0到1的過程。
  • 上面提到的enterexit中的動畫,都是不斷地調用到RippleDrawableinvalidateSelf方法,而invalidateSelf會觸發viewdraw方法,最後觸發了RippleDrawabledraw方法,最終會觸發到RippleForegrounddrawSoftwareRippleBackgrounddrawSoftware
  • RippleDrawable中動畫銷燬是在view#dispatchdetachedFromWindowRippleDrawablejumpToCurrentState方法。
相關文章
相關標籤/搜索