天天看點

Android5.0 水波控件RippleDrawable簡析Step 1:Step 2:Step 3:Step 4:

在Android5.0中新增了一種水波效果控件,叫RippleDrawable,當控件使用RippleDrawable作為背景(android:background)且在控件可以接受點選動作(android:clickable="true")的條件下,當按下或擡起手指時,會出現水波效果。效果如下圖所示:

Android5.0 水波控件RippleDrawable簡析Step 1:Step 2:Step 3:Step 4:

以上圖檔的layout代碼如下:

ripple_bg.xml 内容如下:

以上的代碼需要sdk版本為21的Eclipse或者Android Studio中編輯,并在Android5.0的虛拟機或真機上運作。

以下從源碼的角度分析一下RippleDrawable的邏輯:

如下面的的時序圖,以下将從4步簡介按下時的效果:

Android5.0 水波控件RippleDrawable簡析Step 1:Step 2:Step 3:Step 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繪制邏輯的一個簡析。 手指移動,擡起,也是相似的一個流程,以後再補充吧~

第一次寫技術部落格,寫的比較簡單,可能會有漏洞,歡迎大家指出~