Merge "Implementing the new material ripple" into sc-dev
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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><!-- A red ripple masked against an opaque rectangle. --/>
|
||||
* <ripple android:rippleStyle="patterned">
|
||||
* </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);
|
||||
|
||||
89
graphics/java/android/graphics/drawable/RippleShader.java
Normal file
89
graphics/java/android/graphics/drawable/RippleShader.java
Normal 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()});
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user