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方法。
相关文章
相关标签/搜索