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
RippleDrawable
Through the insideRippleForeground
andRippleBackground
Two classes of animation to control the radius and center position of the water wave drawing circle, and the transparency of the drawing circleRippleForeground
andRippleBackground
Is 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.RippleForeground
To create thesoftWare
andhardWare
Animation, by default, ifrippleDrawable
Is isBound,RippleForeground
theenterSoftWare
The 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.RippleBackground
Is also createdsoftWare
andhardWare
Animation,RippleBackground
Created 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#createSoftwareEnter
The 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#createSoftwareExit
The 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#drawSoftware
This 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#drawSoftware
In 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 hand
RippleDrawable
theonStateChange
Method is calledRippleForeground
theenter
andsetup
Method, which is then createdsoftWare
Animation, inside the animation constantly called upRippleDrawable
theinvalidateSelf
Method, which then firesRippleForeground
andRippleBackground
thedraw
Method, then to the parent classRippleComponent
Draw method, whileRippleComponent
Method firesdrawSoftWare
Method, and finally toRippleForeground
thedrawSoftWare
Methods.
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
- in
inflate
Method is called first in the parent classLayerDrawable
theinflate
Methods,inflate
Method to parse each oneitem
Tags, every single oneitem
Tag corresponds to oneChildDrawable
After parsing the id and other attributes, the drawable attribute is then parsed, and the attribute values are placed in sequenceChildDrawable
In the.- Parse the above
ChildDrawable
Add toLayerDrawable
In theLayerState
An array ofmChildren
In the water.- in
RippleDrawable
Constructor is initialized in the inflate method inripple
The color and RADIUS attribute values in theRippleState
In 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
- generate
mMaskBuffer
,mMaskShader
,mMaskCanvas
, to create amMaskColorFilter
About thePorterDuffColorFilter
In the application ofStateListDrawable
As 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 part
mMaskShader
Pass 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 animations
Property
The form implements the current class value changes, all from 0 to 1 in the processtweenRadius
Constantly changing in the animationRippleForeground
In themTweenRadius
Variables, intweenOrigin
Constantly changing in the animationmTweenX
andmTweenX
Global variables,opacity
Constantly changing in the animationmOpacity
Global variables. And is called in the setValue method of the animationinvalidateSelf
Method, which will eventually be called again into rippleDrawableinvalidateSelf
Methods, briefly mentioned in section 1invalidateSelf
Method, which will eventually trigger the draw method of drawable, so you can see that rippleForeground animation is actually called all the timeRippleComponent
Draw 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:
RippleDrawable
ininflate
The process initializes layers oflayer
, added to theLayerState
Inside, the drawable that initializes the mask part is put in the mMask global drawable and initializedripple
Inside the labelcolor
Properties.In RippleDrawable
The 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 draw
RippleBackground
Part, if RippleBackground isVisible to draw. - And then every time
exit
unfinishedRippleForeground
Part of. Notice that this is a set traversal drawingRippleForeground
. - And then I’m going to draw the current
RippleForeground
. - In the animation section, it’s triggered first
RippleDrawable
theonStateChange
Method is then createdRippleForeground
And call theRippleForeground
theenter
andThe setup method is created in Enter
softWareAnimation, where
hardWareAnimations can only be created with hardware acceleration enabled, so they are not created by default
SoftWare ` animation. RippleForeground
In thesoftWare
There are three animations to create, one is the radius, the center of the circle, the transparency of the three animations, inenter
whenRippleForeground
inRippleDrawable.isBounded
Do not create an animation when inexit
There is no restriction on creating animations when this is doneandroid-27
The source code below. inandroid-28
I looked at the effect on the phoneenter
When there is a wave animation,exit
I don’t have an animation, so you can use itandroid-28
Try it on your phone.RippleBackground
Change the transparent bottom of the brush,enter
In case the brush goes from 0 to 1; inexit
The transparency of the brush goes from 1 to 0 and then from 0 to 1 again.- Mentioned above
enter
andexit
In the animation, are constantly called toRippleDrawable
theinvalidateSelf
Method, andinvalidateSelf
Will triggerview
thedraw
Method, and finally triggeredRippleDrawable
thedraw
Method, which will eventually trigger toRippleForeground
thedrawSoftware
andRippleBackground
thedrawSoftware
. - RippleDrawable in the animation destruction is in
view#dispatchdetachedFromWindow
toRippleDrawable
thejumpToCurrentState
Methods.