blob: 10e2afe0baa91331458c2bccde32421a2caec1b5 [file] [log] [blame]
/*
* 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;
}
}
}