blob: 951b97e791c9f973327ddfe6d5132f4b97cf0370 [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.wm.shell.startingsurface;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Trace;
import android.util.PathParser;
import android.window.SplashScreenView;
import com.android.internal.R;
/**
* Creating a lightweight Drawable object used for splash screen.
*
* @hide
*/
public class SplashscreenIconDrawableFactory {
/**
* @return An array containing the foreground drawable at index 0 and if needed a background
* drawable at index 1.
*/
static Drawable[] makeIconDrawable(@ColorInt int backgroundColor, @ColorInt int themeColor,
@NonNull Drawable foregroundDrawable, int srcIconSize, int iconSize,
Handler splashscreenWorkerHandler) {
Drawable foreground;
Drawable background = null;
boolean drawBackground =
backgroundColor != Color.TRANSPARENT && backgroundColor != themeColor;
if (foregroundDrawable instanceof Animatable) {
foreground = new AnimatableIconAnimateListener(foregroundDrawable);
} else if (foregroundDrawable instanceof AdaptiveIconDrawable) {
// If the icon is Adaptive, we already use the icon background.
drawBackground = false;
foreground = new ImmobileIconDrawable(foregroundDrawable,
srcIconSize, iconSize, splashscreenWorkerHandler);
} else {
// Adaptive icon don't handle transparency so we draw the background of the adaptive
// icon with the same color as the window background color instead of using two layers
foreground = new ImmobileIconDrawable(
new AdaptiveForegroundDrawable(foregroundDrawable),
srcIconSize, iconSize, splashscreenWorkerHandler);
}
if (drawBackground) {
background = new MaskBackgroundDrawable(backgroundColor);
}
return new Drawable[]{foreground, background};
}
static Drawable[] makeLegacyIconDrawable(@NonNull Drawable iconDrawable, int srcIconSize,
int iconSize, Handler splashscreenWorkerHandler) {
return new Drawable[]{new ImmobileIconDrawable(iconDrawable, srcIconSize, iconSize,
splashscreenWorkerHandler)};
}
/**
* Drawable pre-drawing the scaled icon in a separate thread to increase the speed of the
* final drawing.
*/
private static class ImmobileIconDrawable extends Drawable {
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG
| Paint.FILTER_BITMAP_FLAG);
private final Matrix mMatrix = new Matrix();
private Bitmap mIconBitmap;
ImmobileIconDrawable(Drawable drawable, int srcIconSize, int iconSize,
Handler splashscreenWorkerHandler) {
final float scale = (float) iconSize / srcIconSize;
mMatrix.setScale(scale, scale);
splashscreenWorkerHandler.post(() -> preDrawIcon(drawable, srcIconSize));
}
private void preDrawIcon(Drawable drawable, int size) {
synchronized (mPaint) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "preDrawIcon");
mIconBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(mIconBitmap);
drawable.setBounds(0, 0, size, size);
drawable.draw(canvas);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
}
@Override
public void draw(Canvas canvas) {
synchronized (mPaint) {
if (mIconBitmap != null) {
canvas.drawBitmap(mIconBitmap, mMatrix, mPaint);
} else {
// this shouldn't happen, but if it really happen, invalidate self to wait
// for bitmap to be ready.
invalidateSelf();
}
}
}
@Override
public void setAlpha(int alpha) {
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
}
@Override
public int getOpacity() {
return 1;
}
}
/**
* Base class the draw a background clipped by the system mask.
*/
public static class MaskBackgroundDrawable extends Drawable {
private static final float MASK_SIZE = AdaptiveIconDrawable.MASK_SIZE;
private static final float EXTRA_INSET_PERCENTAGE = 1 / 4f;
static final float DEFAULT_VIEW_PORT_SCALE = 1f / (1 + 2 * EXTRA_INSET_PERCENTAGE);
/**
* Clip path defined in R.string.config_icon_mask.
*/
private static Path sMask;
private final Path mMaskScaleOnly;
private final Matrix mMaskMatrix;
@Nullable
private final Paint mBackgroundPaint;
public MaskBackgroundDrawable(@ColorInt int backgroundColor) {
final Resources r = Resources.getSystem();
sMask = PathParser.createPathFromPathData(r.getString(R.string.config_icon_mask));
Path mask = new Path(sMask);
mMaskScaleOnly = new Path(mask);
mMaskMatrix = new Matrix();
if (backgroundColor != Color.TRANSPARENT) {
mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG
| Paint.FILTER_BITMAP_FLAG);
mBackgroundPaint.setColor(backgroundColor);
mBackgroundPaint.setStyle(Paint.Style.FILL);
} else {
mBackgroundPaint = null;
}
}
@Override
protected void onBoundsChange(Rect bounds) {
if (bounds.isEmpty()) {
return;
}
updateLayerBounds(bounds);
}
protected void updateLayerBounds(Rect bounds) {
// reset everything that depends on the view bounds
mMaskMatrix.setScale(bounds.width() / MASK_SIZE, bounds.height() / MASK_SIZE);
sMask.transform(mMaskMatrix, mMaskScaleOnly);
}
@Override
public void draw(Canvas canvas) {
canvas.clipPath(mMaskScaleOnly);
if (mBackgroundPaint != null) {
canvas.drawPath(mMaskScaleOnly, mBackgroundPaint);
}
}
@Override
public void setAlpha(int alpha) {
if (mBackgroundPaint != null) {
mBackgroundPaint.setAlpha(alpha);
}
}
@Override
public int getOpacity() {
return PixelFormat.RGBA_8888;
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
}
}
private static class AdaptiveForegroundDrawable extends MaskBackgroundDrawable {
@NonNull
protected final Drawable mForegroundDrawable;
private final Rect mTmpOutRect = new Rect();
AdaptiveForegroundDrawable(@NonNull Drawable foregroundDrawable) {
super(Color.TRANSPARENT);
mForegroundDrawable = foregroundDrawable;
}
@Override
protected void updateLayerBounds(Rect bounds) {
super.updateLayerBounds(bounds);
int cX = bounds.width() / 2;
int cY = bounds.height() / 2;
int insetWidth = (int) (bounds.width() / (DEFAULT_VIEW_PORT_SCALE * 2));
int insetHeight = (int) (bounds.height() / (DEFAULT_VIEW_PORT_SCALE * 2));
final Rect outRect = mTmpOutRect;
outRect.set(cX - insetWidth, cY - insetHeight, cX + insetWidth, cY + insetHeight);
if (mForegroundDrawable != null) {
mForegroundDrawable.setBounds(outRect);
}
invalidateSelf();
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
mForegroundDrawable.draw(canvas);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
mForegroundDrawable.setColorFilter(colorFilter);
}
}
/**
* A lightweight AdaptiveIconDrawable which support foreground to be Animatable, and keep this
* drawable masked by config_icon_mask.
*/
private static class AnimatableIconAnimateListener extends AdaptiveForegroundDrawable
implements SplashScreenView.IconAnimateListener {
private Animatable mAnimatableIcon;
private Animator mIconAnimator;
private boolean mAnimationTriggered;
AnimatableIconAnimateListener(@NonNull Drawable foregroundDrawable) {
super(foregroundDrawable);
mForegroundDrawable.setCallback(mCallback);
}
@Override
public boolean prepareAnimate(long duration, Runnable startListener) {
mAnimatableIcon = (Animatable) mForegroundDrawable;
mIconAnimator = ValueAnimator.ofInt(0, 1);
mIconAnimator.setDuration(duration);
mIconAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
if (startListener != null) {
startListener.run();
}
mAnimatableIcon.start();
}
@Override
public void onAnimationEnd(Animator animation) {
mAnimatableIcon.stop();
}
@Override
public void onAnimationCancel(Animator animation) {
mAnimatableIcon.stop();
}
@Override
public void onAnimationRepeat(Animator animation) {
// do not repeat
mAnimatableIcon.stop();
}
});
return true;
}
private final Callback mCallback = new Callback() {
@Override
public void invalidateDrawable(@NonNull Drawable who) {
invalidateSelf();
}
@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
scheduleSelf(what, when);
}
@Override
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
unscheduleSelf(what);
}
};
private void ensureAnimationStarted() {
if (mAnimationTriggered) {
return;
}
if (mIconAnimator != null && !mIconAnimator.isRunning()) {
mIconAnimator.start();
}
mAnimationTriggered = true;
}
@Override
public void draw(Canvas canvas) {
ensureAnimationStarted();
super.draw(canvas);
}
}
}