天天看点

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绘制逻辑的一个简析。 手指移动,抬起,也是相似的一个流程,以后再补充吧~

第一次写技术博客,写的比较简单,可能会有漏洞,欢迎大家指出~