A review,

RippleDrawable: A custom view with RippleDrawable: a custom view with RippleDrawable Well, without further ado, or as usual, first with a demo to review the use of water waves:

XML to define a water wave:

<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>
Copy the code

Then on the view you can use:

The process by which a drawable is displayed on a View in Android
background
foreground
view.setClickable(true)
view.setOnClickListener

The code is analyzed under The Android -27, under the Android -28 click ripple effect is not quite the same, here is the first state

Second, the overview

  • RippleDrawableThrough the insideRippleForegroundandRippleBackgroundTwo classes of animation to control the radius and center position of the water wave drawing circle, and the transparency of the drawing circle
  • RippleForegroundandRippleBackgroundIs a subclass of RippleComponent. In the drawing part of RippleDrawble, the item part of RippleDrawale will be drawn first, and the ID of this item part is not mask. And then we’re going to draw RippleBackground, and we’re going to draw RippleBackground if it’s isVisible, and we’ll talk about when it’s isVisible; The rippleForeground animation that was not finished when I drew exit was followed, so the rippleForeground animation will appear layer by layer when the continuous points are kept very fast.
  • RippleForegroundTo create thesoftWareandhardWareAnimation, by default, ifrippleDrawableIs isBound,RippleForegroundtheenterSoftWareThe animation is not created (note: Enter does not create the animation on 27, when the hand is pressed), the animation I see on 28 has a ripple effect when pressed, so I can guess that 28 created the enterSoftWare animation when pressed.
  • RippleBackgroundIs also createdsoftWareandhardWareAnimation,RippleBackgroundCreated in the animation is the premise of the view of the canvas. IsHardwareAccelerated (), to draw drawHardWare animations, by default is not open hardware acceleration, therefore drawHardWare animation is not drawn.
  • RippleForeground#createSoftwareEnterThe fusion of three animations, the increase of the radius of the water wave, the center of the circle gradient, the gradient of transparency of the animation.
  • RippleForeground#createSoftwareExitThe fusion of three animations, the increase of the radius of the water wave, the center of the circle gradient, the gradient of transparency of the animation. The difference with Enter is that The transparency of Enter is 0 to 1, while the transparency of exit is 1 to 0.
  • RippleForeground#drawSoftwareThis is the key to draw, mainly in the drawing process to change the transparency of the brush, draw the center of the circle, change the radius of the circle.
  • RippleBackground#drawSoftwareIn its drawing, it draws a fixed circle, the center of which is always (0,0), and the radius remains the same.
  • The drawing process is triggered first when the view is pressed and lifted in the handRippleDrawabletheonStateChangeMethod is calledRippleForegroundtheenterandsetupMethod, which is then createdsoftWareAnimation, inside the animation constantly called upRippleDrawabletheinvalidateSelfMethod, which then firesRippleForegroundandRippleBackgroundthedrawMethod, then to the parent classRippleComponentDraw method, whileRippleComponentMethod firesdrawSoftWareMethod, and finally toRippleForegroundthedrawSoftWareMethods.

RippleDrawable initialization

3.1 RippleDrawable#inflate

And remember in the first article we talked about drawable, we said that the initialization of the drawable starts with the inflate method no, look at the initialization of the RippleDrawable directly, In the inflate method call the parent class method and updateStateFromTypedArray inflate method:

private void updateStateFromTypedArray(@NonNull TypedArray a) throws XmlPullParserException { //RippleState is a subclass of RippleDrawable. LayerState final RippleState state = mState; // See why we need to define ripple_color.xml in this example. Here is the access to a ColorStateList final ColorStateList color = al-qeada etColorStateList (R.s tyleable. RippleDrawable_color); // The obtained ColorStateList is passed to ripplestate.mcolorif(color ! = null) { mState.mColor = color; } // Get a radius property, not set in demo, So here with the default mState. MMaxRadius value mState. MMaxRadius = al-qeada etDimensionPixelSize (R.s tyleable RippleDrawable_radius, mState.mMaxRadius); }Copy the code

The color and RADIUS attributes of the Ripple tag are obtained during initialization and assigned to RippleState.

3.2 RippleDrawable#inflateLayers

View the inflate method of the parent class again. This is the inflate method of LayerDrawable, which calls the inflateLayers method to initialize the item inside:

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); / / here is where the analytical item attribute updateLayerFromTypedArray (layer, a); a.recycle();if(layer.mDrawable == null && (layer.mThemeAttrs == null || layer.mThemeAttrs[R.styleable.LayerDrawableItem_drawable] == 0)) {/ / if the item label is defined drawable XML file transferred here layer. MDrawable = drawable. CreateFromXmlInner (r, parser, attrs, theme); layer.mDrawable.setCallback(this); state.mChildrenChangingConfigurations |= layer.mDrawable.getChangingConfigurations(); } // Add each ChildDrawable to LayerState addLayer(layer); }}Copy the code

3.3 RippleDrawable#addLayer

Can see if the label is generated a ChildDrawable item objects, analytic item in updateLayerFromTypedArray method:

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) {// Get id, omit other attributes, not here, you try it yourselfcase R.styleable.LayerDrawableItem_id:
                layer.mId = a.getResourceId(attr, layer.mId);
                break; }} / / drawable attributes final drawable Dr = al-qeada etDrawable (R.s tyleable. LayerDrawableItem_drawable);if(dr ! = null) {if(layer.mDrawable ! = null) { layer.mDrawable.setCallback(null); } layer.mDrawable = Dr;} layer.mDrawable = Dr; layer.mDrawable.setCallback(this); state.mChildrenChangingConfigurations |= layer.mDrawable.getChangingConfigurations(); }}Copy the code

< span style = “box-sizing: border-box; color: RGB (51, 51, 51); line-height: 20px; font-size: 16px! Important; white-space: inherit! Important;” The next step is to get the drawable property value and place the drawable value into ChildDrawable. After updateLayerFromTypedArray afterward, and then the last is the addLayer, this actually keep up with the section introduces StateListDrawable addState similar to:

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];
        ifSystem. Arraycopy (St. mChildren, 0, nu, 0, I); } st.mChildren = nu; St. mChildren[I] = layer; st.mChildren[I] = layer; st.mNumChildren++; st.invalidateCache();return i;
}
Copy the code

The addLayer method also expands the mChildren array in LayerState to 10 elements, and then places the ChildDrawable passed in to the mChildren array in LayerState. Initialization of RippleDrawable

  • ininflateMethod is called first in the parent classLayerDrawabletheinflateMethods,inflateMethod to parse each oneitemTags, every single oneitemTag corresponds to oneChildDrawableAfter parsing the id and other attributes, the drawable attribute is then parsed, and the attribute values are placed in sequenceChildDrawableIn the.
  • Parse the aboveChildDrawableAdd toLayerDrawableIn theLayerStateAn array ofmChildrenIn the water.
  • inRippleDrawableConstructor is initialized in the inflate method inrippleThe color and RADIUS attribute values in theRippleStateIn the.

3.5 Initialize the mask section

Initialize the mask need to ppleDrawable. UpdateLocalState method to see:

private void updateLocalState() {
    // Initialize from constant state.
    mMask = findDrawableByLayerId(R.id.mask);
}
Copy the code
public Drawable findDrawableByLayerId(int id) {
    final ChildDrawable[] layers = mLayerState.mChildren;
    for (int i = mLayerState.mNumChildren - 1; i >= 0; i--) {
        if (layers[i].mId == id) {
            returnlayers[i].mDrawable; }}return null;
}
Copy the code

Drawable (drawale) {drawable (drawale) {drawable (drawale) {drawable (drawale) {drawable (drawale) {drawable (drawale) {drawable (drawale) {drawable (drawale) {drawable (drawale) {drawable (drawale);

4, Draw RippleDrawable

4.1 RippleDrawable#draw

Drawable drawable drawable drawable drawable drawable drawable drawable drawable drawable

@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(); Final int saveCount = canvas.save(canvas.clip_save_flag); // Clipping the drawable region canvas.clipRect(bounds); // Draw the content part drawContent(canvas); // Make the ripples part drawBackgroundAndRipples(canvas); Restore canvas state canvas.restoreToCount(saveCount); }Copy the code

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); }}}Copy the code

It is clear that the id of the item is drawn directly, not the drawable of the mask. In the opening example, drawable without id=mask =”# CCCCCC “, where is a colorDrawable.

4.3 Ripples: Draw the background and Ripples

This part is the key to the ripples effect, look at the drawBackgroundAndRipples method:

Ripples private void drawBackgroundAndRipples(Canvas Canvas) {// Animation class final RippleForeground active = mRipple; // Final RippleBackground background = mBackground; Final int count = mExitingRipplesCount;if(active == null && count <= 0 && (background == null || ! background.isVisible())) {return; } // get the coordinates when clicked finalfloat x = mHotspotBounds.exactCenterX();
        final floaty = mHotspotBounds.exactCenterY(); // Offset the canvas to the clicked position canvas.translate(x, y); // Draw mask part updateMaskShaderIfNeeded(); // Position the shader to accountfor canvas translation.
        if(mMaskShader ! = null) { final Rect bounds = getBounds(); mMaskMatrix.setTranslate(bounds.left - x, bounds.top - y); mMaskShader.setLocalMatrix(mMaskMatrix); } // If the color of the color attribute value in the Ripple tag has no transparency, the default transparency is 255/2 // half of the alpha value, Final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = final int color = mState.mColor.getColorForState(getState(), Color.BLACK); final int halfAlpha = (Color.alpha(color) / 2) << 24; final Paint p = getRipplePaint(); // Empty by defaultif(mMaskColorFilter ! = null) { final int fullAlphaColor = color | (0xFF << 24); mMaskColorFilter.setColor(fullAlphaColor); p.setColor(halfAlpha); p.setColorFilter(mMaskColorFilter); p.setShader(mMaskShader); }else{/ / color value and then with the alpha value or computing final int halfAlphaColor = (color & 0 XFFFFFF) | halfAlpha; p.setColor(halfAlphaColor); p.setColorFilter(null); p.setShader(null); } // If the background is not empty and isVisible draws the backgroundif(background ! = null && background.isVisible()) { background.draw(canvas, p); } // will be every timeexitYou can see that this is the key to drawing ripple effect.if (count > 0) {
            final RippleForeground[] ripples = mExitingRipples;
            for(int i = 0; i < count; i++) { ripples[i].draw(canvas, p); }} // Current rippleForeground drawnif(active ! = null) { active.draw(canvas, p); } // Restore the canvas's offset canvas.translate(-x, -y); }Copy the code

Draw ripple and background above:

  • Get the coordinates of the click
  • Offset the coordinates of the canvas to the clicked coordinates
  • Draw the mask part
  • Gets the value of ripple’s color property and reduces the alpha value of color by half
  • If background is not empty, and background.isVisible only draws background
  • Each exit ripple is drawn one by one. If the ripple is clicked continuously, the ripple will appear layer by layer, which is the effect of drawing layer by layer
  • Draws the current rippleForeground
  • Restores the offset of the canvas
4.3.1 Draw the mask part
private void updateMaskShaderIfNeeded() {// omit some null judgment // getMaskType final int maskType = getMaskType();if(mMaskBuffer == null || mMaskBuffer.getWidth() ! = bounds.width() || mMaskBuffer.getHeight() ! = bounds.height()) {if(mMaskBuffer ! = null) { mMaskBuffer.recycle(); } mMaskBuffer = bitmap.createbitmap (bounds.width(), bounds.height(), bitmap.config.alpha_8); // Place the mask part of the bitmap on the bitmapShader, Ripple mMaskShader = new BitmapShader(mMaskBuffer, shader.tilemode.CLAMP, shader.tilemode.CLAMP); MMaskCanvas = new Canvas(mMaskBuffer); }else {
        mMaskBuffer.eraseColor(Color.TRANSPARENT);
    }

    if (mMaskMatrix == null) {
        mMaskMatrix = new Matrix();
    } else{ mMaskMatrix.reset(); } // Create a PorterDuffColorFilter, which will be used when drawing riipleif(mMaskColorFilter == null) { mMaskColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_IN); } final int top = bounds.top; mMaskCanvas.translate(-left, -top); // By default maskType=MASK_NONEif (maskType == MASK_EXPLICIT) {
        drawMask(mMaskCanvas);
    } else if (maskType == MASK_CONTENT) {
        drawContent(mMaskCanvas);
    }
    mMaskCanvas.translate(left, top);
}
Copy the code
  • If the drawable value of the mask part is 255, maskType=MASK_NONE, otherwise maskType=MASK_EXPLICIT
  • generatemMaskBuffer,mMaskShader,mMaskCanvas, to create amMaskColorFilterAbout thePorterDuffColorFilterIn the application ofStateListDrawableAs mentioned in the section, SRC_IN mode is used here to indicate that the mask section is below the drawing.
  • Since we have analyzed maskType=MASK_NONE, we will not draw the mask partmMaskShaderPass to the Ripple section.

From above, the condition that we draw the background is that it is not empty and is isVisible.

public boolean isVisible() {
    return mOpacity > 0 || isHardwareAnimating();
}
Copy the code

MOpacity draws a variable that changes transparency when clicked, from 0 to 1 and from 1 to 0, isHardwareAnimating is also simple:

protected final boolean isHardwareAnimating() {
    returnmHardwareAnimator ! = null && mHardwareAnimator.isRunning() || mHasPendingHardwareAnimator; }Copy the code

Indicates that mHardwareAnimator is in progress, but we will see what this animation means later. Ripples We look at the mExitingRipples in what value:

// This method is drawn when the hand is raisedexitRipples When you assign mRipple to the mExitingRipples array and make the array incremented by 1. Call outexitAfter, set mRipple to empty private voidtryRippleExit() {
    if(mRipple ! = null) {if(mExitingRipples == null) { mExitingRipples = new RippleForeground[MAX_RIPPLES]; } mExitingRipples[mExitingRipplesCount++] = mRipple; mRipple.exit(); mRipple = null; }}Copy the code

So that’s the static drawing of rippleDrawable, and then the dynamic drawing of rippleDrawable.

4.4 Touch to draw

In the first section, after the ontouchEvent of the View is triggered, the setState method of drawable is triggered. In setState, the onStateChange method of drawable is triggered. Look directly at the onStateChange method for RippleDrawable:

@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; }} // Both press andenablestatesetRippleActive(enabled && pressed);
    setBackgroundActive(hovered || focused || (enabled && pressed), focused || hovered);

    return changed;
}
Copy the code

The onStateChange logic is clear. The setRippleActive and setBackgroundActive methods are triggered when pressed and enabled.

private void setRippleActive(boolean active) {
    if(mRippleActive ! = active) { mRippleActive = active;if(active) {// Call the method tryRippleEnter() when pressed; }else{// Call the method tryRippleExit() when lifting; }}}Copy the code

The tryRippleEnter method is called when pressed, and the tryRippleExit method is called when lifted:

private void tryRippleEnter() {// limits the maximum number of rippleif (mExitingRipplesCount >= MAX_RIPPLES) {
        return;
    }
    if (mRipple == null) {
        final float x;
        final floaty; //mHasPending is set when pressedtrue.if (mHasPending) {
            mHasPending = false; // press x = mPendingX; y = mPendingY; }else{/ / at the back of the coordinate with mHotspotBounds inside coordinate x = mHotspotBounds. ExactCenterX (); y = mHotspotBounds.exactCenterY(); } final boolean isBounded = isBounded(); // Generate a RippleForeground mRipple = new RippleForeground(this, mHotspotBounds, X, Y, isBounded, mForceSoftware); } // Next callsetUp and Enter methods mRipple. Setup (mstate.mmaxradius, mDensity); mRipple.enter(false);
}
Copy the code

RippleForeground (” x “, “y”); RippleForeground (” x “, “Y”); RippleForeground inherits RippleComponent; the setUp and Enter methods are all defined in the parent class.

public final void setup(floatMaxRadius, int densityDpi) {// Default maxRadius=-1, so goelseThe logic insideif (maxRadius >= 0) {
        mHasMaxRadius = true;
        mTargetRadius = maxRadius;
    } else{ mTargetRadius = getTargetRadius(mBounds); } // mDensityScale = densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE; onTargetRadiusChanged(mTargetRadius); }Copy the code

5. Animation

5.1 RippleForeground animation

MaxRadius =-1 by default, so get mTargetRadius through getTargetRadius, get half of the diagonals of view size through Pythagorean theorem. Finally, the onTargetRadiusChanged method is called, which is an empty method, so you can imagine leaving it up to the subclass to handle the mTargetRadius problem. Next, see what the Enter method does:

public final void enter(boolean fast) {
    cancel();
    mSoftwareAnimator = createSoftwareEnter(fast);
    if (mSoftwareAnimator != null) {
        mSoftwareAnimator.start();
    }
}
Copy the code

Cancel the previous animation, then create the mSoftwareAnimator animation through the createSoftwareEnter method, and finally start the animation. CreateSoftwareEnter is an abstract method that gets turned off RippleForeground:

@Override
protected Animator createSoftwareEnter(boolean fast) {
    // Bounded ripples donIf (mIsBounded) {return null; if (mIsBounded) {return null; if (mIsBounded) {return null; Final int duration = (int) (1000 * math.sqrt (mTargetRadius/WAVE_TOUCH_DOWN_ACCELERATION * MDensityScale) + 0.5); // Final ObjectAnimator tweenRadius = objectAnimator.offloat (this, TWEEN_RADIUS, 1); tweenRadius.setAutoCancel(true); tweenRadius.setDuration(duration); tweenRadius.setInterpolator(LINEAR_INTERPOLATOR); tweenRadius.setStartDelay(RIPPLE_ENTER_DELAY); // Animation of the center of a circle when a wave is drawn round, TweenOrigin = final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1); tweenOrigin.setAutoCancel(true); tweenOrigin.setDuration(duration); tweenOrigin.setInterpolator(LINEAR_INTERPOLATOR); tweenOrigin.setStartDelay(RIPPLE_ENTER_DELAY); // Opacity animation final ObjectAnimator = ObjectAnimator. OfFloat (this, 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; }Copy the code

In the enterSoftware animation, first check bounds, where isBound is passed from rippleDrawable:

private boolean isBounded() {
    return getNumberOfLayers() > 0;
}
Copy the code

That is, the number of mNumChildren in RippleState is greater than 0. It has been analyzed in the above initialization process. The number of addLayer method was actually added by the number of items in XML, so it was generally isBounded. Unless the Item tag is not defined within the Ripple tag.

SoftWareEnter () {return null;} softWareExit () {return null;}

  • TweenRadius defines the animation of the radius of the water wave drawing circles
  • TweenOrigin defines the animation of the center of a circle when a wave draws a circle
  • Opacity: an animation that defines the opacity of a water wave. The top three animations all use animationsPropertyThe form implements the current class value changes, all from 0 to 1 in the processtweenRadiusConstantly changing in the animationRippleForegroundIn themTweenRadiusVariables, intweenOriginConstantly changing in the animationmTweenXandmTweenXGlobal variables,opacityConstantly changing in the animationmOpacityGlobal variables. And is called in the setValue method of the animationinvalidateSelfMethod, which will eventually be called again into rippleDrawableinvalidateSelfMethods, briefly mentioned in section 1invalidateSelfMethod, which will eventually trigger the draw method of drawable, so you can see that rippleForeground animation is actually called all the timeRippleComponentDraw method:
Public Boolean draw(Canvas C, Paint P) {// If Canvas is hardwareAccelerated, hardWare will animate Default skips 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); Return drawSoftware(c, p); return drawSoftware(c, p); }Copy the code

In RippleComponent’s draw method, if hardWare acceleration is not enabled, the hardWare animation is not enabled. So let’s look at drawSoftware. DrawSoftware is abstract method in RippleComponent. So I still need to go to subclass RippleForeground:

@Override
protected boolean drawSoftware(Canvas c, Paint p) {
    boolean hasContent = false; Final int origAlpha = p.getalpha (); // Get the opacity of the beginning of the brush, which is half the opacity of the ripple label color. Final int alpha = (int) (origAlpha * mOpacity + 0.5f); // Get the radius of the current circle finalfloat radius = getCurrentRadius();
    if(alpha > 0 && radius > 0) {// Get the center position finalfloat x = getCurrentX();
        final float y = getCurrentY();
        p.setAlpha(alpha);
        c.drawCircle(x, y, radius, p);
        p.setAlpha(origAlpha);
        hasContent = true;
    }
    return hasContent;
}
Copy the code

MOpacity is used to calculate the opacity of the current brush. This method uses a +0.5f conversion from float to int, usually + 0.5F. The mOpacity variable is used to change global properties using its property in opacity animation. For animation, see the use of property, where the type of FloatProperty is used:

/**
 * 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) {
        returnobject.mOpacity; }};Copy the code

InvalidateSelf (object. InvalidateSelf); draw (RippleDrawable); The view’s draw method will be called.

The getCurrentRadius method gets the current RADIUS:

private float getCurrentRadius() {
    return MathUtils.lerp(0, mTargetRadius, mTweenRadius);
}
Copy the code

Here is the Android MathUtils utility class, the use of the differentiator, the first two arguments start value and end value, the third three is the percentage.

The getCurrentX and getCurrentY methods are similar to obtaining the center of a circle. Leaving the softWare part of the Enter part, let’s look at the exit part. Exit should start with tryRippleExit:

private void tryRippleExit() {
    if(mRipple ! = null) {if(mExitingRipples == null) { mExitingRipples = new RippleForeground[MAX_RIPPLES]; Ripples ripples ripples ripples ripples ripples ripples ripples ripples ripples ripples ripples ripples ripples ripples ripples ripples ripples ripples ripples ripples ripples ripples mRipple.exit(); mRipple = null; }}Copy the code

MRipple. Exit () triggers an animation of rippleForground’s createSoftwareExit.

android-28
android-28

5.2 RippleBackground animation

Rippleground foreground foreground Rippleground foreground foreground Rippleground foreground

@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;
}
Copy the code

I will not explain it here, but directly a opacity animation. Ok, it is too intuitive. After saying the animation of enter part, let’s go to the animation of exit part:

@Override
protected Animator createSoftwareExit() {
    final AnimatorSet set= new AnimatorSet(); // Transparency displays from 1 to 0 final ObjectAnimatorexit = 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) {final ObjectAnimator Enter = objectAnimator.offloat (this, RippleBackground.OPACITY, 1); enter.setInterpolator(LINEAR_INTERPOLATOR); enter.setDuration(fastEnterDuration); enter.setAutoCancel(true);
        builder.after(enter);
    }
    return set;
}
Copy the code

Exit animation is divided into two parts, a process of transparency from 1 to 0, and then from 0 to 1 again. According to this analysis, it is the process of lifting from opaque to completely transparent and then to not completely transparent. The AnimatorSet.Builder after method is used, which means that the transparency from 0 to 1 is entered after the exit animation.

Okay, so we’re done drawing and animating RippleForground, which is the water wave drawing, and RippleBackground, which is the transparency gradient animation.

5.3 Cancel the animation

In RippleDrawable, you need to know when the view is being destroyed. Do you usually override the onDetachViewFromWindow method of a view? All background and foreground of the view are destroyed during detach, so RippleDrawable is no exception.

void dispatchDetachedFromWindowOnDetachedFromWindow () {// Override this method when customizingviews, such as releasing animations, etc. / / destruction of drawable where onDetachedFromWindowInternal (); }Copy the code

Annotation to write very well, everyone in a custom view, onDetachedFromWindow method is useful, is by it, then see onDetachedFromWindowInternal method:

protected void onDetachedFromWindowInternal() {
    jumpDrawablesToCurrentState();
}
Copy the code

To make it easier for you to look at the code, I’ve reduced it to a single line of code and moved on:

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(); }}Copy the code

Drawable: jumpToCurrentState (drawable); drawable: jumpToCurrentState (drawable);

@Override
public void jumpToCurrentState() {
    super.jumpToCurrentState();
    if(mRipple ! = null) { mRipple.end(); }if(mBackground ! = null) { mBackground.end(); } cancelExitingRipples(); }Copy the code
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);
}
Copy the code

Ripples You can see clearly, call RippleForeground End, RippleBackground end and the ripples you can call each exit unfinished RippleForeground end method in the cancelExitingRipples method, So in the end, we call the end method of RippleComponent:

public void end() {
    endSoftwareAnimations();
    endHardwareAnimations();
}
Copy the code

See, the names are all there:

private void endSoftwareAnimations() {
    if(mSoftwareAnimator ! = null) { mSoftwareAnimator.end(); mSoftwareAnimator = null; } } private voidendHardwareAnimations() {
    if (mHardwareAnimator != null) {
        mHardwareAnimator.end();
        mHardwareAnimator = null;
    }
}
Copy the code

I’m not going to explain that the view detach from the window to the RippleDrawable and it ends up here when the animation stops.

Six, summarized

Let’s sort out the drawing process again:

  • RippleDrawableininflateThe process initializes layers oflayer, added to theLayerStateInside, the drawable that initializes the mask part is put in the mMask global drawable and initializedrippleInside the labelcolorProperties.
  • In RippleDrawableThe static drawing part first draws item that is not id=mask
  • The color attribute value of mask alpha=255 will not be drawn, so the alpha value of the color value should be within the range of [0,255). The mask is drawn below rippleForeground and RippleBackground.
  • Then drawRippleBackgroundPart, if RippleBackground isVisible to draw.
  • And then every timeexitunfinishedRippleForegroundPart of. Notice that this is a set traversal drawingRippleForeground.
  • And then I’m going to draw the currentRippleForeground.
  • In the animation section, it’s triggered firstRippleDrawabletheonStateChangeMethod is then createdRippleForegroundAnd call theRippleForegroundtheenterandThe setup method is created in EntersoftWareAnimation, wherehardWareAnimations can only be created with hardware acceleration enabled, so they are not created by defaultSoftWare ` animation.
  • RippleForegroundIn thesoftWareThere are three animations to create, one is the radius, the center of the circle, the transparency of the three animations, inenterwhenRippleForegroundinRippleDrawable.isBoundedDo not create an animation when inexitThere is no restriction on creating animations when this is doneandroid-27The source code below. inandroid-28I looked at the effect on the phoneenterWhen there is a wave animation,exitI don’t have an animation, so you can use itandroid-28Try it on your phone.
  • RippleBackgroundChange the transparent bottom of the brush,enterIn case the brush goes from 0 to 1; inexitThe transparency of the brush goes from 1 to 0 and then from 0 to 1 again.
  • Mentioned aboveenterandexitIn the animation, are constantly called toRippleDrawabletheinvalidateSelfMethod, andinvalidateSelfWill triggerviewthedrawMethod, and finally triggeredRippleDrawablethedrawMethod, which will eventually trigger toRippleForegroundthedrawSoftwareandRippleBackgroundthedrawSoftware.
  • RippleDrawable in the animation destruction is inview#dispatchdetachedFromWindowtoRippleDrawablethejumpToCurrentStateMethods.