| /* |
| * 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 com.android.systemui.scrim; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.ValueAnimator; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.graphics.Canvas; |
| import android.graphics.ColorFilter; |
| import android.graphics.Paint; |
| import android.graphics.Path; |
| import android.graphics.PixelFormat; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.graphics.Xfermode; |
| import android.graphics.drawable.Drawable; |
| import android.view.animation.DecelerateInterpolator; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.graphics.ColorUtils; |
| import com.android.systemui.statusbar.notification.stack.StackStateAnimator; |
| |
| /** |
| * Drawable used on SysUI scrims. |
| */ |
| public class ScrimDrawable extends Drawable { |
| private static final String TAG = "ScrimDrawable"; |
| |
| private boolean mShouldUseLargeScreenSize; |
| private final Paint mPaint; |
| private final Path mPath = new Path(); |
| private final RectF mBoundsRectF = new RectF(); |
| |
| private int mAlpha = 255; |
| private int mMainColor; |
| private ValueAnimator mColorAnimation; |
| private int mMainColorTo; |
| private float mCornerRadius; |
| private ConcaveInfo mConcaveInfo; |
| private int mBottomEdgePosition; |
| private float mBottomEdgeRadius = -1; |
| private boolean mCornerRadiusEnabled; |
| |
| public ScrimDrawable() { |
| mPaint = new Paint(); |
| mPaint.setStyle(Paint.Style.FILL); |
| mShouldUseLargeScreenSize = false; |
| } |
| |
| /** |
| * Sets the background color. |
| * @param mainColor the color. |
| * @param animated if transition should be interpolated. |
| */ |
| public void setColor(int mainColor, boolean animated) { |
| if (mainColor == mMainColorTo) { |
| return; |
| } |
| |
| if (mColorAnimation != null && mColorAnimation.isRunning()) { |
| mColorAnimation.cancel(); |
| } |
| |
| mMainColorTo = mainColor; |
| |
| if (animated) { |
| final int mainFrom = mMainColor; |
| |
| ValueAnimator anim = ValueAnimator.ofFloat(0, 1); |
| anim.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); |
| anim.addUpdateListener(animation -> { |
| float ratio = (float) animation.getAnimatedValue(); |
| mMainColor = ColorUtils.blendARGB(mainFrom, mainColor, ratio); |
| invalidateSelf(); |
| }); |
| anim.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation, boolean isReverse) { |
| if (mColorAnimation == animation) { |
| mColorAnimation = null; |
| } |
| } |
| }); |
| anim.setInterpolator(new DecelerateInterpolator()); |
| anim.start(); |
| mColorAnimation = anim; |
| } else { |
| mMainColor = mainColor; |
| invalidateSelf(); |
| } |
| } |
| |
| @Override |
| public void setAlpha(int alpha) { |
| if (alpha != mAlpha) { |
| mAlpha = alpha; |
| invalidateSelf(); |
| } |
| } |
| |
| @Override |
| public int getAlpha() { |
| return mAlpha; |
| } |
| |
| @Override |
| public void setXfermode(@Nullable Xfermode mode) { |
| mPaint.setXfermode(mode); |
| invalidateSelf(); |
| } |
| |
| @Override |
| public void setColorFilter(ColorFilter colorFilter) { |
| mPaint.setColorFilter(colorFilter); |
| } |
| |
| @Override |
| public ColorFilter getColorFilter() { |
| return mPaint.getColorFilter(); |
| } |
| |
| @Override |
| public int getOpacity() { |
| return PixelFormat.TRANSLUCENT; |
| } |
| |
| public void setShouldUseLargeScreenSize(boolean v) { |
| mShouldUseLargeScreenSize = v; |
| } |
| |
| /** |
| * Corner radius used by either concave or convex corners. |
| */ |
| public void setRoundedCorners(float radius) { |
| if (radius == mCornerRadius) { |
| return; |
| } |
| mCornerRadius = radius; |
| if (mConcaveInfo != null) { |
| mConcaveInfo.setCornerRadius(radius); |
| updatePath(); |
| } |
| invalidateSelf(); |
| } |
| |
| /** |
| * If we should draw a rounded rect instead of a rect. |
| */ |
| public void setRoundedCornersEnabled(boolean enabled) { |
| if (mCornerRadiusEnabled == enabled) { |
| return; |
| } |
| mCornerRadiusEnabled = enabled; |
| invalidateSelf(); |
| } |
| |
| /** |
| * If we should draw a concave rounded rect instead of a rect. |
| */ |
| public void setBottomEdgeConcave(boolean enabled) { |
| if (enabled && mConcaveInfo != null) { |
| return; |
| } |
| if (!enabled) { |
| mConcaveInfo = null; |
| } else { |
| mConcaveInfo = new ConcaveInfo(); |
| mConcaveInfo.setCornerRadius(mCornerRadius); |
| } |
| invalidateSelf(); |
| } |
| |
| /** |
| * Location of concave edge. |
| * @see #setBottomEdgeConcave(boolean) |
| */ |
| public void setBottomEdgePosition(int y) { |
| if (mBottomEdgePosition == y) { |
| return; |
| } |
| mBottomEdgePosition = y; |
| if (mConcaveInfo == null) { |
| return; |
| } |
| updatePath(); |
| invalidateSelf(); |
| } |
| |
| public void setBottomEdgeRadius(float radius) { |
| mBottomEdgeRadius = radius; |
| } |
| |
| @Override |
| public void draw(@NonNull Canvas canvas) { |
| mPaint.setColor(mMainColor); |
| mPaint.setAlpha(mAlpha); |
| if (mConcaveInfo != null) { |
| drawConcave(canvas); |
| } else if (mCornerRadiusEnabled && mCornerRadius > 0) { |
| float topEdgeRadius = mCornerRadius; |
| float bottomEdgeRadius = mBottomEdgeRadius == -1.0 ? mCornerRadius : mBottomEdgeRadius; |
| |
| mBoundsRectF.set(getBounds()); |
| |
| // When the back gesture causes the notification scrim to be scaled down, |
| // this offset "reveals" the rounded bottom edge as it "pulls away". |
| // We must *not* make this adjustment on largescreen shades (where the corner is sharp). |
| if (!mShouldUseLargeScreenSize && mBottomEdgeRadius != -1) { |
| mBoundsRectF.bottom -= bottomEdgeRadius; |
| } |
| |
| // We need a box with rounded corners but its lower corners are not rounded on large |
| // screen devices in "portrait" orientation. |
| // Thus, we cannot draw a symmetric rounded rectangle via canvas.drawRoundRect() |
| // and must build a box with different corner radii at the top and at the bottom. |
| // Additionally, when the scrim is pushed to the very bottom of the screen, do not draw |
| // anything (drawing a rounded box with these specifications is not possible). |
| // TODO(b/271030611) perhaps this could be accomplished via Path.addRoundRect instead? |
| if (mBoundsRectF.bottom - mBoundsRectF.top > bottomEdgeRadius) { |
| mPath.reset(); |
| mPath.moveTo(mBoundsRectF.right, mBoundsRectF.top + topEdgeRadius); |
| mPath.cubicTo(mBoundsRectF.right, mBoundsRectF.top + topEdgeRadius, |
| mBoundsRectF.right, mBoundsRectF.top, |
| mBoundsRectF.right - topEdgeRadius, mBoundsRectF.top); |
| mPath.lineTo(mBoundsRectF.left + topEdgeRadius, mBoundsRectF.top); |
| mPath.cubicTo(mBoundsRectF.left + topEdgeRadius, mBoundsRectF.top, |
| mBoundsRectF.left, mBoundsRectF.top, |
| mBoundsRectF.left, mBoundsRectF.top + topEdgeRadius); |
| mPath.lineTo(mBoundsRectF.left, mBoundsRectF.bottom - bottomEdgeRadius); |
| mPath.cubicTo(mBoundsRectF.left, mBoundsRectF.bottom - bottomEdgeRadius, |
| mBoundsRectF.left, mBoundsRectF.bottom, |
| mBoundsRectF.left + bottomEdgeRadius, mBoundsRectF.bottom); |
| mPath.lineTo(mBoundsRectF.right - bottomEdgeRadius, mBoundsRectF.bottom); |
| mPath.cubicTo(mBoundsRectF.right - bottomEdgeRadius, mBoundsRectF.bottom, |
| mBoundsRectF.right, mBoundsRectF.bottom, |
| mBoundsRectF.right, mBoundsRectF.bottom - bottomEdgeRadius); |
| mPath.close(); |
| canvas.drawPath(mPath, mPaint); |
| } |
| } else { |
| canvas.drawRect(getBounds().left, getBounds().top, getBounds().right, |
| getBounds().bottom, mPaint); |
| } |
| } |
| |
| @Override |
| protected void onBoundsChange(Rect bounds) { |
| updatePath(); |
| } |
| |
| private void drawConcave(Canvas canvas) { |
| canvas.clipOutPath(mConcaveInfo.mPath); |
| canvas.drawRect(getBounds().left, getBounds().top, getBounds().right, |
| mBottomEdgePosition + mConcaveInfo.mPathOverlap, mPaint); |
| } |
| |
| private void updatePath() { |
| if (mConcaveInfo == null) { |
| return; |
| } |
| mConcaveInfo.mPath.reset(); |
| float top = mBottomEdgePosition; |
| float bottom = mBottomEdgePosition + mConcaveInfo.mPathOverlap; |
| mConcaveInfo.mPath.addRoundRect(getBounds().left, top, getBounds().right, bottom, |
| mConcaveInfo.mCornerRadii, Path.Direction.CW); |
| } |
| |
| @VisibleForTesting |
| public int getMainColor() { |
| return mMainColor; |
| } |
| |
| private static class ConcaveInfo { |
| private float mPathOverlap; |
| private final float[] mCornerRadii; |
| private final Path mPath = new Path(); |
| |
| ConcaveInfo() { |
| mCornerRadii = new float[] {0, 0, 0, 0, 0, 0, 0, 0}; |
| } |
| |
| public void setCornerRadius(float radius) { |
| mPathOverlap = radius; |
| mCornerRadii[0] = radius; |
| mCornerRadii[1] = radius; |
| mCornerRadii[2] = radius; |
| mCornerRadii[3] = radius; |
| } |
| } |
| } |