diff options
Diffstat (limited to 'graphics/java')
5 files changed, 707 insertions, 14 deletions
diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java index b70fa0e693c2..88cf96a9eb4e 100644 --- a/graphics/java/android/graphics/HardwareRenderer.java +++ b/graphics/java/android/graphics/HardwareRenderer.java @@ -39,6 +39,7 @@ import android.view.IGraphicsStatsCallback; import android.view.NativeVectorDrawableAnimator; import android.view.PixelCopy; import android.view.Surface; +import android.view.SurfaceControl; import android.view.SurfaceHolder; import android.view.animation.AnimationUtils; @@ -314,6 +315,16 @@ public class HardwareRenderer { } /** + * Sets the SurfaceControl to be used internally inside render thread + * @hide + * @param surfaceControl The surface control to pass to render thread in hwui. + * If null, any previous references held in render thread will be discarded. + */ + public void setSurfaceControl(@Nullable SurfaceControl surfaceControl) { + nSetSurfaceControl(mNativeProxy, surfaceControl != null ? surfaceControl.mNativeObject : 0); + } + + /** * Sets the parameters that can be used to control a render request for a * {@link HardwareRenderer}. This is not thread-safe and must not be held on to for longer * than a single frame request. @@ -1216,6 +1227,8 @@ public class HardwareRenderer { private static native void nSetSurface(long nativeProxy, Surface window, boolean discardBuffer); + private static native void nSetSurfaceControl(long nativeProxy, long nativeSurfaceControl); + private static native boolean nPause(long nativeProxy); private static native void nSetStopped(long nativeProxy, boolean stopped); diff --git a/graphics/java/android/graphics/drawable/RippleAnimationSession.java b/graphics/java/android/graphics/drawable/RippleAnimationSession.java new file mode 100644 index 000000000000..80f65f919fa6 --- /dev/null +++ b/graphics/java/android/graphics/drawable/RippleAnimationSession.java @@ -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; + } + } +} diff --git a/graphics/java/android/graphics/drawable/RippleDrawable.java b/graphics/java/android/graphics/drawable/RippleDrawable.java index bab80cee2b26..5024875aab3c 100644 --- a/graphics/java/android/graphics/drawable/RippleDrawable.java +++ b/graphics/java/android/graphics/drawable/RippleDrawable.java @@ -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); diff --git a/graphics/java/android/graphics/drawable/RippleShader.java b/graphics/java/android/graphics/drawable/RippleShader.java new file mode 100644 index 000000000000..500efdd84854 --- /dev/null +++ b/graphics/java/android/graphics/drawable/RippleShader.java @@ -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()}); + } +} diff --git a/graphics/java/android/graphics/fonts/FontVariationAxis.java b/graphics/java/android/graphics/fonts/FontVariationAxis.java index 7bd581723d18..d1fe2cdbcd77 100644 --- a/graphics/java/android/graphics/fonts/FontVariationAxis.java +++ b/graphics/java/android/graphics/fonts/FontVariationAxis.java @@ -23,6 +23,7 @@ import android.os.Build; import android.text.TextUtils; import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.regex.Pattern; @@ -189,6 +190,17 @@ public final class FontVariationAxis { return TextUtils.join(",", axes); } + /** + * Stringify the array of FontVariationAxis. + * @hide + */ + public static @NonNull String toFontVariationSettings(@Nullable List<FontVariationAxis> axes) { + if (axes == null) { + return ""; + } + return TextUtils.join(",", axes); + } + @Override public boolean equals(@Nullable Object o) { if (o == this) { |