Merge "Implementing the new material ripple" into sc-dev

This commit is contained in:
Jay Aliomer
2021-02-16 21:38:47 +00:00
committed by Android (Google) Code Review
12 changed files with 709 additions and 23 deletions

View File

@@ -1185,6 +1185,7 @@ package android {
field public static final int right = 16843183; // 0x10101af
field public static final int ringtonePreferenceStyle = 16842899; // 0x1010093
field public static final int ringtoneType = 16843257; // 0x10101f9
field public static final int rippleStyle = 16844337; // 0x1010631
field public static final int rollbackDataPolicy = 16844311; // 0x1010617
field public static final int rotation = 16843558; // 0x1010326
field public static final int rotationAnimation = 16844090; // 0x101053a
@@ -16533,9 +16534,13 @@ package android.graphics.drawable {
public class RippleDrawable extends android.graphics.drawable.LayerDrawable {
ctor public RippleDrawable(@NonNull android.content.res.ColorStateList, @Nullable android.graphics.drawable.Drawable, @Nullable android.graphics.drawable.Drawable);
method public int getRadius();
method public int getRippleStyle();
method public void setColor(android.content.res.ColorStateList);
method public void setRadius(int);
method public void setRippleStyle(int) throws java.lang.IllegalArgumentException;
field public static final int RADIUS_AUTO = -1; // 0xffffffff
field public static final int STYLE_PATTERNED = 1; // 0x1
field public static final int STYLE_SOLID = 0; // 0x0
}
public class RotateDrawable extends android.graphics.drawable.DrawableWrapper {

View File

@@ -15,7 +15,8 @@
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
android:color="?attr/colorControlHighlight"
android:rippleStyle="?attr/rippleStyle">
<item android:id="@id/mask"
android:drawable="@drawable/btn_default_mtrl_shape" />
</ripple>

View File

@@ -19,7 +19,8 @@
android:insetTop="@dimen/button_inset_vertical_material"
android:insetRight="@dimen/button_inset_horizontal_material"
android:insetBottom="@dimen/button_inset_vertical_material">
<ripple android:color="?attr/colorControlHighlight">
<ripple android:color="?attr/colorControlHighlight"
android:rippleStyle="?attr/rippleStyle">
<item>
<shape android:shape="rectangle"
android:tint="@color/btn_colored_background_material">

View File

@@ -15,6 +15,7 @@
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
android:color="?attr/colorControlHighlight"
android:rippleStyle="?attr/rippleStyle">
<item android:drawable="@drawable/btn_default_mtrl_shape" />
</ripple>

View File

@@ -21,7 +21,8 @@
android:insetBottom="@dimen/button_inset_vertical_material">
<layer-list android:paddingMode="stack">
<item>
<ripple android:color="?attr/colorControlHighlight">
<ripple android:color="?attr/colorControlHighlight"
android:rippleStyle="?attr/rippleStyle">
<item>
<shape android:shape="rectangle"
android:tint="?attr/colorButtonNormal">

View File

@@ -6389,6 +6389,13 @@
<!-- The radius of the ripple when fully expanded. By default, the
radius is computed based on the size of the ripple's container. -->
<attr name="radius" />
<!-- The style of the ripple drawable is solid by default -->
<attr name="rippleStyle">
<!-- Solid is the default style -->
<enum name="solid" value="0" />
<!-- Patterned style-->
<enum name="patterned" value="1" />
</attr>
</declare-styleable>
<declare-styleable name="ScaleDrawable">

View File

@@ -3071,6 +3071,7 @@
<public name="windowSplashScreenAnimationDuration"/>
<public name="windowSplashScreenBrandingImage"/>
<public name="splashScreenTheme" />
<public name="rippleStyle" />
</public-group>
<public-group type="drawable" first-id="0x010800b5">

View File

@@ -224,6 +224,9 @@ easier.
<item name="textColorTertiary">@color/text_color_tertiary_device_default_dark</item>
<item name="colorForeground">@color/foreground_device_default_dark</item>
<item name="colorForegroundInverse">@color/foreground_device_default_light</item>
<!-- Ripple style-->
<item name="rippleStyle">solid</item>
</style>
<style name="Theme.DeviceDefault" parent="Theme.DeviceDefaultBase" />

View File

@@ -0,0 +1,284 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.graphics.drawable;
import android.animation.Animator;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Canvas;
import android.graphics.CanvasProperty;
import android.graphics.Paint;
import android.graphics.RecordingCanvas;
import android.graphics.animation.RenderNodeAnimator;
import android.util.ArraySet;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;
import java.util.function.Consumer;
/**
* @hide
*/
public final class RippleAnimationSession {
private static final int ENTER_ANIM_DURATION = 350;
private static final int EXIT_ANIM_OFFSET = ENTER_ANIM_DURATION;
private static final int EXIT_ANIM_DURATION = 350;
private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
// Matches R.interpolator.fast_out_slow_in but as we have no context we can't just import that
private static final TimeInterpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator();
private Consumer<RippleAnimationSession> mOnSessionEnd;
private AnimationProperties<Float, Paint> mProperties;
private AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> mCanvasProperties;
private Runnable mOnUpdate;
private long mStartTime;
private boolean mForceSoftware;
private ArraySet<Animator> mActiveAnimations = new ArraySet(3);
RippleAnimationSession(@NonNull AnimationProperties<Float, Paint> properties,
boolean forceSoftware) {
mProperties = properties;
mForceSoftware = forceSoftware;
}
void end() {
for (Animator anim: mActiveAnimations) {
if (anim != null) anim.end();
}
mActiveAnimations.clear();
}
@NonNull RippleAnimationSession enter(Canvas canvas) {
if (isHwAccelerated(canvas)) {
enterHardware((RecordingCanvas) canvas);
} else {
enterSoftware();
}
mStartTime = System.nanoTime();
return this;
}
@NonNull RippleAnimationSession exit(Canvas canvas) {
if (isHwAccelerated(canvas)) exitHardware((RecordingCanvas) canvas);
else exitSoftware();
return this;
}
private void onAnimationEnd(Animator anim) {
mActiveAnimations.remove(anim);
}
@NonNull RippleAnimationSession setOnSessionEnd(
@Nullable Consumer<RippleAnimationSession> onSessionEnd) {
mOnSessionEnd = onSessionEnd;
return this;
}
RippleAnimationSession setOnAnimationUpdated(@Nullable Runnable run) {
mOnUpdate = run;
mProperties.setOnChange(mOnUpdate);
return this;
}
private boolean isHwAccelerated(Canvas canvas) {
return canvas.isHardwareAccelerated() && !mForceSoftware;
}
private void exitSoftware() {
ValueAnimator expand = ValueAnimator.ofFloat(.5f, 1f);
expand.setDuration(EXIT_ANIM_DURATION);
expand.setStartDelay(computeDelay());
expand.addUpdateListener(updatedAnimation -> {
notifyUpdate();
mProperties.getShader().setProgress((Float) expand.getAnimatedValue());
});
expand.addListener(new AnimatorListener(this) {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
Consumer<RippleAnimationSession> onEnd = mOnSessionEnd;
if (onEnd != null) onEnd.accept(RippleAnimationSession.this);
}
});
expand.setInterpolator(LINEAR_INTERPOLATOR);
expand.start();
mActiveAnimations.add(expand);
}
private long computeDelay() {
long currentTime = System.nanoTime();
long timePassed = (currentTime - mStartTime) / 1_000_000;
long difference = EXIT_ANIM_OFFSET;
return Math.max(difference - timePassed, 0);
}
private void notifyUpdate() {
Runnable onUpdate = mOnUpdate;
if (onUpdate != null) onUpdate.run();
}
RippleAnimationSession setForceSoftwareAnimation(boolean forceSw) {
mForceSoftware = forceSw;
return this;
}
private void exitHardware(RecordingCanvas canvas) {
AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>>
props = getCanvasProperties();
RenderNodeAnimator exit =
new RenderNodeAnimator(props.getProgress(), 1f);
exit.setDuration(EXIT_ANIM_DURATION);
exit.addListener(new AnimatorListener(this) {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
Consumer<RippleAnimationSession> onEnd = mOnSessionEnd;
if (onEnd != null) onEnd.accept(RippleAnimationSession.this);
}
});
exit.setTarget(canvas);
exit.setInterpolator(DECELERATE_INTERPOLATOR);
long delay = computeDelay();
exit.setStartDelay(delay);
exit.start();
mActiveAnimations.add(exit);
}
private void enterHardware(RecordingCanvas can) {
AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>>
props = getCanvasProperties();
RenderNodeAnimator expand =
new RenderNodeAnimator(props.getProgress(), .5f);
expand.setTarget(can);
expand.setDuration(ENTER_ANIM_DURATION);
expand.addListener(new AnimatorListener(this));
expand.setInterpolator(LINEAR_INTERPOLATOR);
expand.start();
mActiveAnimations.add(expand);
}
private void enterSoftware() {
ValueAnimator expand = ValueAnimator.ofFloat(0f, 0.5f);
expand.addUpdateListener(updatedAnimation -> {
notifyUpdate();
mProperties.getShader().setProgress((Float) expand.getAnimatedValue());
});
expand.addListener(new AnimatorListener(this));
expand.setInterpolator(LINEAR_INTERPOLATOR);
expand.start();
mActiveAnimations.add(expand);
}
@NonNull AnimationProperties<Float, Paint> getProperties() {
return mProperties;
}
@NonNull AnimationProperties getCanvasProperties() {
if (mCanvasProperties == null) {
mCanvasProperties = new AnimationProperties<>(
CanvasProperty.createFloat(mProperties.getX()),
CanvasProperty.createFloat(mProperties.getY()),
CanvasProperty.createFloat(mProperties.getMaxRadius()),
CanvasProperty.createPaint(mProperties.getPaint()),
CanvasProperty.createFloat(mProperties.getProgress()),
mProperties.getShader());
}
return mCanvasProperties;
}
private static class AnimatorListener implements Animator.AnimatorListener {
private final RippleAnimationSession mSession;
AnimatorListener(RippleAnimationSession session) {
mSession = session;
}
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
mSession.onAnimationEnd(animation);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
}
static class AnimationProperties<FloatType, PaintType> {
private final FloatType mY;
private FloatType mProgress;
private FloatType mMaxRadius;
private final PaintType mPaint;
private final FloatType mX;
private final RippleShader mShader;
private Runnable mOnChange;
private void onChange() {
if (mOnChange != null) mOnChange.run();
}
private void setOnChange(Runnable onChange) {
mOnChange = onChange;
}
AnimationProperties(FloatType x, FloatType y, FloatType maxRadius,
PaintType paint, FloatType progress, RippleShader shader) {
mY = y;
mX = x;
mMaxRadius = maxRadius;
mPaint = paint;
mShader = shader;
mProgress = progress;
}
FloatType getProgress() {
return mProgress;
}
FloatType getX() {
return mX;
}
FloatType getY() {
return mY;
}
FloatType getMaxRadius() {
return mMaxRadius;
}
PaintType getPaint() {
return mPaint;
}
RippleShader getShader() {
return mShader;
}
}
}

View File

@@ -16,6 +16,14 @@
package android.graphics.drawable;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
@@ -27,17 +35,21 @@ import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.CanvasProperty;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.RecordingCanvas;
import android.graphics.Rect;
import android.graphics.Shader;
import android.os.Build;
import android.util.AttributeSet;
import android.view.animation.LinearInterpolator;
import com.android.internal.R;
@@ -45,6 +57,9 @@ import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Arrays;
/**
@@ -94,6 +109,17 @@ import java.util.Arrays;
* </pre>
*
* @attr ref android.R.styleable#RippleDrawable_color
*
* To change the ripple style, assign the value of "solid" or "patterned" to the android:rippleStyle
* attribute.
*
* <pre>
* <code>&lt;!-- A red ripple masked against an opaque rectangle. --/>
* &lt;ripple android:rippleStyle="patterned">
* &lt;/ripple></code>
* </pre>
*
* @attr ref android.R.styleable#RippleDrawable_rippleStyle
*/
public class RippleDrawable extends LayerDrawable {
/**
@@ -102,6 +128,29 @@ public class RippleDrawable extends LayerDrawable {
*/
public static final int RADIUS_AUTO = -1;
/**
* Ripple style where a solid circle is drawn. This is also the default style
* @see #setRippleStyle(int)
*/
public static final int STYLE_SOLID = 0;
/**
* Ripple style where a circle shape with a patterned,
* noisy interior expands from the hotspot to the bounds".
* @see #setRippleStyle(int)
*/
public static final int STYLE_PATTERNED = 1;
/**
* Ripple drawing style
* @hide
*/
@Retention(SOURCE)
@Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD})
@IntDef({STYLE_SOLID, STYLE_PATTERNED})
public @interface RippleStyle {
}
private static final int BACKGROUND_OPACITY_DURATION = 80;
private static final int MASK_UNKNOWN = -1;
private static final int MASK_NONE = 0;
private static final int MASK_CONTENT = 1;
@@ -109,6 +158,7 @@ public class RippleDrawable extends LayerDrawable {
/** The maximum number of ripples supported. */
private static final int MAX_RIPPLES = 10;
private static final LinearInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
private final Rect mTempRect = new Rect();
@@ -172,6 +222,14 @@ public class RippleDrawable extends LayerDrawable {
*/
private boolean mForceSoftware;
// Patterned
private float mTargetBackgroundOpacity;
private ValueAnimator mBackgroundAnimation;
private float mBackgroundOpacity;
private boolean mRunBackgroundAnimation;
private boolean mExitingAnimation;
private ArrayList<RippleAnimationSession> mRunningAnimations = new ArrayList<>();
/**
* Constructor used for drawable inflation.
*/
@@ -235,7 +293,7 @@ public class RippleDrawable extends LayerDrawable {
Arrays.fill(ripples, 0, count, null);
}
mExitingRipplesCount = 0;
mExitingAnimation = true;
// Always draw an additional "clean" frame after canceling animations.
invalidateSelf(false);
}
@@ -276,21 +334,37 @@ public class RippleDrawable extends LayerDrawable {
private void setRippleActive(boolean active) {
if (mRippleActive != active) {
mRippleActive = active;
}
if (mState.mRippleStyle == STYLE_SOLID) {
if (active) {
tryRippleEnter();
} else {
tryRippleExit();
}
} else {
if (active) {
startPatternedAnimation();
} else {
exitPatternedAnimation();
}
}
}
private void setBackgroundActive(boolean hovered, boolean focused, boolean pressed) {
if (mBackground == null && (hovered || focused)) {
mBackground = new RippleBackground(this, mHotspotBounds, isBounded());
mBackground.setup(mState.mMaxRadius, mDensity);
}
if (mBackground != null) {
mBackground.setState(focused, hovered, pressed);
if (mState.mRippleStyle == STYLE_SOLID) {
if (mBackground == null && (hovered || focused)) {
mBackground = new RippleBackground(this, mHotspotBounds, isBounded());
mBackground.setup(mState.mMaxRadius, mDensity);
}
if (mBackground != null) {
mBackground.setState(focused, hovered, pressed);
}
} else {
if (focused || hovered) {
enterPatternedBackgroundAnimation(focused, hovered);
} else {
exitPatternedBackgroundAnimation();
}
}
}
@@ -317,6 +391,9 @@ public class RippleDrawable extends LayerDrawable {
mRipple.onBoundsChange();
}
mState.mMaxRadius = mState.mMaxRadius <= 0 && mState.mRippleStyle != STYLE_SOLID
? (int) computeRadius()
: mState.mMaxRadius;
invalidateSelf();
}
@@ -330,7 +407,11 @@ public class RippleDrawable extends LayerDrawable {
// If we just became visible, ensure the background and ripple
// visibilities are consistent with their internal states.
if (mRippleActive) {
tryRippleEnter();
if (mState.mRippleStyle == STYLE_SOLID) {
tryRippleEnter();
} else {
invalidateSelf();
}
}
// Skip animations, just show the correct final states.
@@ -489,6 +570,8 @@ public class RippleDrawable extends LayerDrawable {
mState.mMaxRadius = a.getDimensionPixelSize(
R.styleable.RippleDrawable_radius, mState.mMaxRadius);
mState.mRippleStyle = a.getInteger(R.styleable.RippleDrawable_rippleStyle, STYLE_SOLID);
}
private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
@@ -535,9 +618,9 @@ public class RippleDrawable extends LayerDrawable {
@Override
public void setHotspot(float x, float y) {
mPendingX = x;
mPendingY = y;
if (mRipple == null || mBackground == null) {
mPendingX = x;
mPendingY = y;
mHasPending = true;
}
@@ -665,6 +748,14 @@ public class RippleDrawable extends LayerDrawable {
*/
@Override
public void draw(@NonNull Canvas canvas) {
if (mState.mRippleStyle == STYLE_SOLID) {
drawSolid(canvas);
} else {
drawPatterned(canvas);
}
}
private void drawSolid(Canvas canvas) {
pruneRipples();
// Clip to the dirty bounds, which will be the drawable bounds if we
@@ -681,6 +772,178 @@ public class RippleDrawable extends LayerDrawable {
canvas.restoreToCount(saveCount);
}
private void exitPatternedBackgroundAnimation() {
mTargetBackgroundOpacity = 0;
if (mBackgroundAnimation != null) mBackgroundAnimation.cancel();
// after cancel
mRunBackgroundAnimation = true;
invalidateSelf(false);
}
private void startPatternedAnimation() {
mRippleActive = true;
invalidateSelf(false);
}
private void exitPatternedAnimation() {
mExitingAnimation = true;
invalidateSelf(false);
}
private void enterPatternedBackgroundAnimation(boolean focused, boolean hovered) {
mBackgroundOpacity = 0;
mTargetBackgroundOpacity = focused ? .6f : hovered ? .2f : 0f;
if (mBackgroundAnimation != null) mBackgroundAnimation.cancel();
// after cancel
mRunBackgroundAnimation = true;
invalidateSelf(false);
}
private void startBackgroundAnimation() {
mRunBackgroundAnimation = false;
mBackgroundAnimation = ValueAnimator.ofFloat(mBackgroundOpacity, mTargetBackgroundOpacity);
mBackgroundAnimation.setInterpolator(LINEAR_INTERPOLATOR);
mBackgroundAnimation.setDuration(BACKGROUND_OPACITY_DURATION);
mBackgroundAnimation.addUpdateListener(update -> {
mBackgroundOpacity = (float) update.getAnimatedValue();
invalidateSelf(false);
});
mBackgroundAnimation.start();
}
private void drawPatterned(@NonNull Canvas canvas) {
final Rect bounds = getBounds();
final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
boolean useCanvasProps = shouldUseCanvasProps(canvas);
boolean changedHotspotBounds = !bounds.equals(mHotspotBounds);
if (isBounded()) {
canvas.clipRect(mHotspotBounds);
}
float x, y;
if (changedHotspotBounds) {
x = mHotspotBounds.exactCenterX();
y = mHotspotBounds.exactCenterY();
useCanvasProps = false;
} else {
x = mPendingX;
y = mPendingY;
}
boolean shouldAnimate = mRippleActive;
boolean shouldExit = mExitingAnimation;
mRippleActive = false;
mExitingAnimation = false;
getRipplePaint();
drawContent(canvas);
drawPatternedBackground(canvas);
if (shouldAnimate && mRunningAnimations.size() <= MAX_RIPPLES) {
RippleAnimationSession.AnimationProperties<Float, Paint> properties =
createAnimationProperties(x, y);
mRunningAnimations.add(new RippleAnimationSession(properties, !useCanvasProps)
.setOnAnimationUpdated(useCanvasProps ? null : () -> invalidateSelf(false))
.setOnSessionEnd(session -> {
mRunningAnimations.remove(session);
})
.setForceSoftwareAnimation(!useCanvasProps)
.enter(canvas));
}
if (shouldExit) {
for (int i = 0; i < mRunningAnimations.size(); i++) {
RippleAnimationSession s = mRunningAnimations.get(i);
s.exit(canvas);
}
}
for (int i = 0; i < mRunningAnimations.size(); i++) {
RippleAnimationSession s = mRunningAnimations.get(i);
if (useCanvasProps) {
RippleAnimationSession.AnimationProperties<CanvasProperty<Float>,
CanvasProperty<Paint>>
p = s.getCanvasProperties();
RecordingCanvas can = (RecordingCanvas) canvas;
can.drawRipple(p.getX(), p.getY(), p.getMaxRadius(), p.getPaint(),
p.getProgress(), p.getShader());
} else {
RippleAnimationSession.AnimationProperties<Float, Paint> p =
s.getProperties();
float posX, posY;
if (changedHotspotBounds) {
posX = x;
posY = y;
if (p.getPaint().getShader() instanceof RippleShader) {
RippleShader shader = (RippleShader) p.getPaint().getShader();
shader.setOrigin(posX, posY);
}
} else {
posX = p.getX();
posY = p.getY();
}
float radius = p.getMaxRadius();
canvas.drawCircle(posX, posY, radius, p.getPaint());
}
}
canvas.restoreToCount(saveCount);
}
private void drawPatternedBackground(Canvas c) {
if (mRunBackgroundAnimation) {
startBackgroundAnimation();
}
if (mBackgroundOpacity == 0) return;
Paint p = mRipplePaint;
float newOpacity = mBackgroundOpacity;
final int origAlpha = p.getAlpha();
final int alpha = Math.min((int) (origAlpha * newOpacity + 0.5f), 255);
if (alpha > 0) {
ColorFilter origFilter = p.getColorFilter();
p.setColorFilter(mMaskColorFilter);
p.setAlpha(alpha);
Rect b = mHotspotBounds;
c.drawCircle(b.centerX(), b.centerY(), mState.mMaxRadius, p);
p.setAlpha(origAlpha);
p.setColorFilter(origFilter);
}
}
private float computeRadius() {
Rect b = getDirtyBounds();
float gap = 0;
float radius = (float) Math.sqrt(b.width() * b.width() + b.height() * b.height()) / 2 + gap;
return radius;
}
@NonNull
private RippleAnimationSession.AnimationProperties<Float, Paint> createAnimationProperties(
float x, float y) {
Paint p = new Paint(mRipplePaint);
float radius = mState.mMaxRadius;
RippleAnimationSession.AnimationProperties<Float, Paint> properties;
RippleShader shader = new RippleShader();
int color = mMaskColorFilter == null
? mState.mColor.getColorForState(getState(), Color.BLACK)
: mMaskColorFilter.getColor();
color = color | 0xFF000000;
shader.setColor(color);
shader.setOrigin(x, y);
shader.setRadius(radius);
shader.setProgress(.0f);
properties = new RippleAnimationSession.AnimationProperties<>(
x, y, radius, p, 0f,
shader);
if (mMaskShader == null) {
shader.setHasMask(false);
} else {
shader.setShader(mMaskShader);
shader.setHasMask(true);
}
p.setShader(shader);
p.setColorFilter(null);
p.setColor(color);
return properties;
}
private boolean shouldUseCanvasProps(Canvas c) {
return !mForceSoftware && c.isHardwareAccelerated();
}
@Override
public void invalidateSelf() {
invalidateSelf(true);
@@ -774,18 +1037,23 @@ public class RippleDrawable extends LayerDrawable {
// Draw the appropriate mask anchored to (0,0).
final int left = bounds.left;
final int top = bounds.top;
mMaskCanvas.translate(-left, -top);
if (mState.mRippleStyle == STYLE_SOLID) {
mMaskCanvas.translate(-left, -top);
}
if (maskType == MASK_EXPLICIT) {
drawMask(mMaskCanvas);
} else if (maskType == MASK_CONTENT) {
drawContent(mMaskCanvas);
}
mMaskCanvas.translate(left, top);
if (mState.mRippleStyle == STYLE_SOLID) {
mMaskCanvas.translate(left, top);
}
}
private int getMaskType() {
if (mRipple == null && mExitingRipplesCount <= 0
&& (mBackground == null || !mBackground.isVisible())) {
&& (mBackground == null || !mBackground.isVisible())
&& mState.mRippleStyle == STYLE_SOLID) {
// We might need a mask later.
return MASK_UNKNOWN;
}
@@ -874,7 +1142,7 @@ public class RippleDrawable extends LayerDrawable {
updateMaskShaderIfNeeded();
// Position the shader to account for canvas translation.
if (mMaskShader != null) {
if (mMaskShader != null && mState.mRippleStyle == STYLE_SOLID) {
final Rect bounds = getBounds();
mMaskMatrix.setTranslate(bounds.left - x, bounds.top - y);
mMaskShader.setLocalMatrix(mMaskMatrix);
@@ -973,6 +1241,31 @@ public class RippleDrawable extends LayerDrawable {
return this;
}
/**
* Sets the visual style of the ripple.
*
* @see #STYLE_SOLID
* @see #STYLE_PATTERNED
*
* @param style The style of the ripple
*/
public void setRippleStyle(@RippleStyle int style) throws IllegalArgumentException {
if (style == STYLE_SOLID || style == STYLE_PATTERNED) {
mState.mRippleStyle = style;
} else {
throw new IllegalArgumentException("Invalid style value " + style);
}
}
/**
* Get the current ripple style
* @return Ripple style
*/
public @RippleStyle int getRippleStyle() {
return mState.mRippleStyle;
}
@Override
RippleState createConstantState(LayerState state, Resources res) {
return new RippleState(state, this, res);
@@ -983,6 +1276,7 @@ public class RippleDrawable extends LayerDrawable {
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
int mMaxRadius = RADIUS_AUTO;
int mRippleStyle = STYLE_SOLID;
public RippleState(LayerState orig, RippleDrawable owner, Resources res) {
super(orig, owner, res);
@@ -992,6 +1286,7 @@ public class RippleDrawable extends LayerDrawable {
mTouchThemeAttrs = origs.mTouchThemeAttrs;
mColor = origs.mColor;
mMaxRadius = origs.mMaxRadius;
mRippleStyle = origs.mRippleStyle;
if (origs.mDensity != mDensity) {
applyDensityScaling(orig.mDensity, mDensity);

View File

@@ -0,0 +1,89 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.graphics.drawable;
import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.graphics.Color;
import android.graphics.RuntimeShader;
import android.graphics.Shader;
final class RippleShader extends RuntimeShader {
private static final String SHADER = "uniform float2 in_origin;\n"
+ "uniform float in_maxRadius;\n"
+ "uniform float in_progress;\n"
+ "uniform float in_hasMask;\n"
+ "uniform float4 in_color;\n"
+ "uniform shader in_shader;\n"
+ "float dist2(float2 p0, float2 pf) { return sqrt((pf.x - p0.x) * (pf.x - p0.x) + "
+ "(pf.y - p0.y) * (pf.y - p0.y)); }\n"
+ "float mod2(float a, float b) { return a - (b * floor(a / b)); }\n"
+ "float rand(float2 src) { return fract(sin(dot(src.xy, float2(12.9898, 78.233))) * "
+ "43758.5453123); }\n"
+ "float4 main(float2 p)\n"
+ "{\n"
+ " float fraction = in_progress;\n"
+ " float2 fragCoord = p;//sk_FragCoord.xy;\n"
+ " float maxDist = in_maxRadius;\n"
+ " float fragDist = dist2(in_origin, fragCoord.xy);\n"
+ " float circleRadius = maxDist * fraction;\n"
+ " float colorVal = (fragDist - circleRadius) / maxDist;\n"
+ " float d = fragDist < circleRadius \n"
+ " ? 1. - abs(colorVal * 3. * smoothstep(0., 1., fraction)) \n"
+ " : 1. - abs(colorVal * 5.);\n"
+ " d = smoothstep(0., 1., d);\n"
+ " float divider = 2.;\n"
+ " float x = floor(fragCoord.x / divider);\n"
+ " float y = floor(fragCoord.y / divider);\n"
+ " float density = .95;\n"
+ " d = rand(float2(x, y)) > density ? d : d * .2;\n"
+ " d = d * rand(float2(fraction, x * y));\n"
+ " float alpha = 1. - pow(fraction, 2.);\n"
+ " if (in_hasMask != 0.) {return sample(in_shader).a * in_color * d * alpha;}\n"
+ " return in_color * d * alpha;\n"
+ "}\n";
RippleShader() {
super(SHADER, false);
}
public void setShader(@NonNull Shader s) {
setInputShader("in_shader", s);
}
public void setRadius(float radius) {
setUniform("in_maxRadius", radius);
}
public void setOrigin(float x, float y) {
setUniform("in_origin", new float[] {x, y});
}
public void setProgress(float progress) {
setUniform("in_progress", progress);
}
public void setHasMask(boolean hasMask) {
setUniform("in_hasMask", hasMask ? 1 : 0);
}
public void setColor(@ColorInt int colorIn) {
Color color = Color.valueOf(colorIn);
this.setUniform("in_color", new float[] {color.red(),
color.green(), color.blue(), color.alpha()});
}
}

View File

@@ -160,10 +160,6 @@ void BaseRenderNodeAnimator::pushStaging(AnimationContext& context) {
}
}
if (!mHasStartValue) {
doSetStartValue(getValue(mTarget));
}
if (!mStagingRequests.empty()) {
// No interpolator was set, use the default
if (mPlayState == PlayState::NotStarted && !mInterpolator) {
@@ -270,9 +266,11 @@ bool BaseRenderNodeAnimator::updatePlayTime(nsecs_t playTime) {
// to call setValue even if the animation isn't yet running or is still
// being delayed as we need to override the staging value
if (playTime < 0) {
setValue(mTarget, mFromValue);
return false;
}
if (!this->mHasStartValue) {
doSetStartValue(getValue(mTarget));
}
float fraction = 1.0f;
if ((mPlayState == PlayState::Running || mPlayState == PlayState::Reversing) && mDuration > 0) {