Merge "Improve app launch animation (1/N)"

This commit is contained in:
Wei Sheng Shih
2021-01-07 09:00:13 +00:00
committed by Android (Google) Code Review
5 changed files with 584 additions and 19 deletions

View File

@@ -17,4 +17,6 @@
<resources>
<!-- Bubbles -->
<color name="bubbles_icon_tint">@color/GM2_grey_200</color>
<!-- Splash screen-->
<color name="splash_window_background_default">@color/splash_screen_bg_dark</color>
</resources>

View File

@@ -35,4 +35,9 @@
<color name="GM2_grey_200">#E8EAED</color>
<color name="GM2_grey_700">#5F6368</color>
<color name="GM2_grey_800">#3C4043</color>
<!-- Splash screen -->
<color name="splash_screen_bg_light">#FFFFFF</color>
<color name="splash_screen_bg_dark">#000000</color>
<color name="splash_window_background_default">@color/splash_screen_bg_light</color>
</resources>

View File

@@ -167,4 +167,7 @@
<!-- Size of padding for the user education cling, this should at minimum be larger than
individual_bubble_size + some padding. -->
<dimen name="bubble_stack_user_education_side_inset">72dp</dimen>
<!-- The width/height of the icon view on staring surface. -->
<dimen name="starting_surface_icon_size">108dp</dimen>
</resources>

View File

@@ -0,0 +1,565 @@
/*
* 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 com.android.wm.shell.startingsurface;
import android.annotation.NonNull;
import android.app.ActivityThread;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import android.util.Slog;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.FrameLayout;
import com.android.internal.R;
import com.android.internal.graphics.palette.Palette;
import com.android.internal.graphics.palette.Quantizer;
import com.android.internal.graphics.palette.VariationalKMeansQuantizer;
import com.android.internal.policy.PhoneWindow;
import java.util.List;
/**
* Util class to create the view for a splash screen content.
*/
class SplashscreenContentDrawer {
private static final String TAG = StartingSurfaceDrawer.TAG;
private static final boolean DEBUG = StartingSurfaceDrawer.DEBUG_SPLASH_SCREEN;
// The acceptable area ratio of foreground_icon_area/background_icon_area, if there is an
// icon which it's non-transparent foreground area is similar to it's background area, then
// do not enlarge the foreground drawable.
// For example, an icon with the foreground 108*108 opaque pixels and it's background
// also 108*108 pixels, then do not enlarge this icon if only need to show foreground icon.
private static final float ENLARGE_FOREGROUND_ICON_THRESHOLD = (72f * 72f) / (108f * 108f);
private final Context mContext;
private int mIconSize;
SplashscreenContentDrawer(Context context) {
mContext = context;
}
private void updateDensity() {
mIconSize = mContext.getResources().getDimensionPixelSize(
com.android.wm.shell.R.dimen.starting_surface_icon_size);
}
private int getSystemBGColor() {
final Context systemContext = ActivityThread.currentApplication();
if (systemContext == null) {
Slog.e(TAG, "System context does not exist!");
return Color.BLACK;
}
final Resources res = systemContext.getResources();
return res.getColor(com.android.wm.shell.R.color.splash_window_background_default);
}
private Drawable createDefaultBackgroundDrawable() {
return new ColorDrawable(getSystemBGColor());
}
View makeSplashScreenContentView(PhoneWindow win, Context context, int iconRes,
int splashscreenContentResId) {
updateDensity();
win.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
// splash screen content will be deprecated after S.
final View ssc = makeSplashscreenContentDrawable(win, context, splashscreenContentResId);
if (ssc != null) {
return ssc;
}
final TypedArray typedArray = context.obtainStyledAttributes(
com.android.internal.R.styleable.Window);
final int resId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0);
typedArray.recycle();
final Drawable themeBGDrawable;
if (resId == 0) {
Slog.w(TAG, "Window background not exist!");
themeBGDrawable = createDefaultBackgroundDrawable();
} else {
themeBGDrawable = context.getDrawable(resId);
}
final Drawable iconDrawable = iconRes != 0 ? context.getDrawable(iconRes)
: context.getPackageManager().getDefaultActivityIcon();
// TODO (b/173975965) Tracking the performance on improved splash screen.
final StartingWindowViewBuilder builder = new StartingWindowViewBuilder();
return builder
.setPhoneWindow(win)
.setContext(context)
.setThemeDrawable(themeBGDrawable)
.setIconDrawable(iconDrawable).build();
}
private class StartingWindowViewBuilder {
// materials
private Drawable mThemeBGDrawable;
private Drawable mIconDrawable;
private PhoneWindow mPhoneWindow;
private Context mContext;
// result
private boolean mBuildComplete = false;
private View mCachedResult;
private int mThemeColor;
private Drawable mFinalIconDrawable;
private float mScale = 1f;
StartingWindowViewBuilder setThemeDrawable(Drawable background) {
mThemeBGDrawable = background;
mBuildComplete = false;
return this;
}
StartingWindowViewBuilder setIconDrawable(Drawable iconDrawable) {
mIconDrawable = iconDrawable;
mBuildComplete = false;
return this;
}
StartingWindowViewBuilder setPhoneWindow(PhoneWindow window) {
mPhoneWindow = window;
mBuildComplete = false;
return this;
}
StartingWindowViewBuilder setContext(Context context) {
mContext = context;
mBuildComplete = false;
return this;
}
View build() {
if (mBuildComplete) {
return mCachedResult;
}
if (mPhoneWindow == null || mContext == null) {
Slog.e(TAG, "Unable to create StartingWindowView, lack of materials!");
return null;
}
if (mThemeBGDrawable == null) {
Slog.w(TAG, "Theme Background Drawable is null, forget to set Theme Drawable?");
mThemeBGDrawable = createDefaultBackgroundDrawable();
}
processThemeColor();
if (!processAdaptiveIcon() && mIconDrawable != null) {
if (DEBUG) {
Slog.d(TAG, "The icon is not an AdaptiveIconDrawable");
}
mFinalIconDrawable = mIconDrawable;
}
final int iconSize = mFinalIconDrawable != null ? (int) (mIconSize * mScale) : 0;
mCachedResult = fillViewWithIcon(mPhoneWindow, mContext, iconSize, mFinalIconDrawable);
mBuildComplete = true;
return mCachedResult;
}
private void processThemeColor() {
final DrawableColorTester themeBGTester =
new DrawableColorTester(mThemeBGDrawable, true /* filterTransparent */);
if (themeBGTester.nonTransparentRatio() == 0) {
// the window background is transparent, unable to draw
Slog.w(TAG, "Window background is transparent, fill background with black color");
mThemeColor = getSystemBGColor();
} else {
mThemeColor = themeBGTester.getDominateColor();
}
}
private boolean processAdaptiveIcon() {
if (!(mIconDrawable instanceof AdaptiveIconDrawable)) {
return false;
}
final AdaptiveIconDrawable adaptiveIconDrawable = (AdaptiveIconDrawable) mIconDrawable;
final DrawableColorTester backIconTester =
new DrawableColorTester(adaptiveIconDrawable.getBackground());
final Drawable iconForeground = adaptiveIconDrawable.getForeground();
final DrawableColorTester foreIconTester =
new DrawableColorTester(iconForeground, true /* filterTransparent */);
final boolean foreComplex = foreIconTester.isComplexColor();
final int foreMainColor = foreIconTester.getDominateColor();
if (DEBUG) {
Slog.d(TAG, "foreground complex color? " + foreComplex + " main color: "
+ Integer.toHexString(foreMainColor));
}
final boolean backComplex = backIconTester.isComplexColor();
final int backMainColor = backIconTester.getDominateColor();
if (DEBUG) {
Slog.d(TAG, "background complex color? " + backComplex + " main color: "
+ Integer.toHexString(backMainColor));
Slog.d(TAG, "theme color? " + Integer.toHexString(mThemeColor));
}
// Only draw the foreground of AdaptiveIcon to the splash screen if below condition
// meet:
// A. The background of the adaptive icon is not complicated. If it is complicated,
// it may contain some information, and
// B. The background of the adaptive icon is similar to the theme color, or
// C. The background of the adaptive icon is grayscale, and the foreground of the
// adaptive icon forms a certain contrast with the theme color.
if (!backComplex && (isRgbSimilarInHsv(mThemeColor, backMainColor)
|| (backIconTester.isGrayscale()
&& !isRgbSimilarInHsv(mThemeColor, foreMainColor)))) {
if (DEBUG) {
Slog.d(TAG, "makeSplashScreenContentView: choose fg icon");
}
// Using AdaptiveIconDrawable here can help keep the shape consistent with the
// current settings.
mFinalIconDrawable = new AdaptiveIconDrawable(
new ColorDrawable(mThemeColor), iconForeground);
// Reference AdaptiveIcon description, outer is 108 and inner is 72, so we
// should enlarge the size 108/72 if we only draw adaptiveIcon's foreground.
if (foreIconTester.nonTransparentRatio() < ENLARGE_FOREGROUND_ICON_THRESHOLD) {
mScale = 1.5f;
}
} else {
if (DEBUG) {
Slog.d(TAG, "makeSplashScreenContentView: draw whole icon");
}
mFinalIconDrawable = adaptiveIconDrawable;
}
return true;
}
private View fillViewWithIcon(PhoneWindow win, Context context,
int iconSize, Drawable iconDrawable) {
final StartingSurfaceWindowView surfaceWindowView =
new StartingSurfaceWindowView(context, iconSize);
surfaceWindowView.setBackground(new ColorDrawable(mThemeColor));
if (iconDrawable != null) {
surfaceWindowView.setIconDrawable(iconDrawable);
}
if (DEBUG) {
Slog.d(TAG, "fillViewWithIcon surfaceWindowView " + surfaceWindowView);
}
win.setContentView(surfaceWindowView);
makeSystemUIColorsTransparent(win);
return surfaceWindowView;
}
private void makeSystemUIColorsTransparent(PhoneWindow win) {
win.setStatusBarColor(Color.TRANSPARENT);
win.setNavigationBarColor(Color.TRANSPARENT);
}
}
private static boolean isRgbSimilarInHsv(int a, int b) {
if (a == b) {
return true;
}
final float[] aHsv = new float[3];
final float[] bHsv = new float[3];
Color.colorToHSV(a, aHsv);
Color.colorToHSV(b, bHsv);
// Minimum degree of the hue between two colors, the result range is 0-180.
int minAngle = (int) Math.abs(aHsv[0] - bHsv[0]);
minAngle = (minAngle + 180) % 360 - 180;
// Calculate the difference between two colors based on the HSV dimensions.
final float normalizeH = minAngle / 180f;
final double square = Math.pow(normalizeH, 2)
+ Math.pow(aHsv[1] - bHsv[1], 2)
+ Math.pow(aHsv[2] - bHsv[2], 2);
final double mean = square / 3;
final double root = Math.sqrt(mean);
if (DEBUG) {
Slog.d(TAG, "hsvDiff " + minAngle + " a: " + Integer.toHexString(a)
+ " b " + Integer.toHexString(b) + " ah " + aHsv[0] + " bh " + bHsv[0]
+ " root " + root);
}
return root < 0.1;
}
private static View makeSplashscreenContentDrawable(PhoneWindow win, Context ctx,
int splashscreenContentResId) {
// doesn't support windowSplashscreenContent after S
// TODO add an allowlist to skip some packages if needed
final int targetSdkVersion = ctx.getApplicationInfo().targetSdkVersion;
if (DEBUG) {
Slog.d(TAG, "target sdk for package: " + targetSdkVersion);
}
if (targetSdkVersion >= Build.VERSION_CODES.S) {
return null;
}
if (splashscreenContentResId == 0) {
return null;
}
final Drawable drawable = ctx.getDrawable(splashscreenContentResId);
if (drawable == null) {
return null;
}
View view = new View(ctx);
view.setBackground(drawable);
win.setContentView(view);
return view;
}
private static class DrawableColorTester {
private final ColorTester mColorChecker;
DrawableColorTester(Drawable drawable) {
this(drawable, false /* filterTransparent */);
}
DrawableColorTester(Drawable drawable, boolean filterTransparent) {
// Some applications use LayerDrawable for their windowBackground. To ensure that we
// only get the real background, so that the color is not affected by the alpha of the
// upper layer, try to get the lower layer here. This can also speed up the calculation.
if (drawable instanceof LayerDrawable) {
LayerDrawable layerDrawable = (LayerDrawable) drawable;
if (layerDrawable.getNumberOfLayers() > 0) {
if (DEBUG) {
Slog.d(TAG, "replace drawable with bottom layer drawable");
}
drawable = layerDrawable.getDrawable(0);
}
}
mColorChecker = drawable instanceof ColorDrawable
? new SingleColorTester((ColorDrawable) drawable)
: new ComplexDrawableTester(drawable, filterTransparent);
}
public float nonTransparentRatio() {
return mColorChecker.nonTransparentRatio();
}
public boolean isComplexColor() {
return mColorChecker.isComplexColor();
}
public int getDominateColor() {
return mColorChecker.getDominantColor();
}
public boolean isGrayscale() {
return mColorChecker.isGrayscale();
}
/**
* A help class to check the color information from a Drawable.
*/
private interface ColorTester {
float nonTransparentRatio();
boolean isComplexColor();
int getDominantColor();
boolean isGrayscale();
}
private static boolean isGrayscaleColor(int color) {
final int red = Color.red(color);
final int green = Color.green(color);
final int blue = Color.blue(color);
return red == green && green == blue;
}
/**
* For ColorDrawable only.
* There will be only one color so don't spend too much resource for it.
*/
private static class SingleColorTester implements ColorTester {
private final ColorDrawable mColorDrawable;
SingleColorTester(@NonNull ColorDrawable drawable) {
mColorDrawable = drawable;
}
@Override
public float nonTransparentRatio() {
final int alpha = mColorDrawable.getAlpha();
return (float) (alpha / 255);
}
@Override
public boolean isComplexColor() {
return false;
}
@Override
public int getDominantColor() {
return mColorDrawable.getColor();
}
@Override
public boolean isGrayscale() {
return isGrayscaleColor(mColorDrawable.getColor());
}
}
/**
* For any other Drawable except ColorDrawable.
* This will use the Palette API to check the color information and use a quantizer to
* filter out transparent colors when needed.
*/
private static class ComplexDrawableTester implements ColorTester {
private static final int MAX_BITMAP_SIZE = 40;
private final Palette mPalette;
private final boolean mFilterTransparent;
private static final TransparentFilterQuantizer TRANSPARENT_FILTER_QUANTIZER =
new TransparentFilterQuantizer();
ComplexDrawableTester(Drawable drawable, boolean filterTransparent) {
final Rect initialBounds = drawable.copyBounds();
int width = drawable.getIntrinsicWidth();
int height = drawable.getIntrinsicHeight();
// Some drawables do not have intrinsic dimensions
if (width <= 0 || height <= 0) {
width = MAX_BITMAP_SIZE;
height = MAX_BITMAP_SIZE;
} else {
width = Math.min(width, MAX_BITMAP_SIZE);
height = Math.min(height, MAX_BITMAP_SIZE);
}
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
final Canvas bmpCanvas = new Canvas(bitmap);
drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
drawable.draw(bmpCanvas);
// restore to original bounds
drawable.setBounds(initialBounds);
final Palette.Builder builder = new Palette.Builder(bitmap)
.maximumColorCount(5).clearFilters();
// The Palette API will ignore Alpha, so it cannot handle transparent pixels, but
// sometimes we will need this information to know if this Drawable object is
// transparent.
mFilterTransparent = filterTransparent;
if (mFilterTransparent) {
builder.setQuantizer(TRANSPARENT_FILTER_QUANTIZER);
}
mPalette = builder.generate();
bitmap.recycle();
}
@Override
public float nonTransparentRatio() {
return mFilterTransparent ? TRANSPARENT_FILTER_QUANTIZER.mNonTransparentRatio : 1;
}
@Override
public boolean isComplexColor() {
return mPalette.getSwatches().size() > 1;
}
@Override
public int getDominantColor() {
final Palette.Swatch mainSwatch = mPalette.getDominantSwatch();
if (mainSwatch != null) {
return mainSwatch.getRgb();
}
return Color.BLACK;
}
@Override
public boolean isGrayscale() {
final List<Palette.Swatch> swatches = mPalette.getSwatches();
if (swatches != null) {
for (int i = swatches.size() - 1; i >= 0; i--) {
Palette.Swatch swatch = swatches.get(i);
if (!isGrayscaleColor(swatch.getRgb())) {
return false;
}
}
}
return true;
}
private static class TransparentFilterQuantizer implements Quantizer {
private static final int NON_TRANSPARENT = 0xFF000000;
private final Quantizer mInnerQuantizer = new VariationalKMeansQuantizer();
private float mNonTransparentRatio;
@Override
public void quantize(final int[] pixels, final int maxColors,
final Palette.Filter[] filters) {
mNonTransparentRatio = 0;
int realSize = 0;
for (int i = pixels.length - 1; i > 0; i--) {
if ((pixels[i] & NON_TRANSPARENT) != 0) {
realSize++;
}
}
if (realSize == 0) {
if (DEBUG) {
Slog.d(TAG, "quantize: this is pure transparent image");
}
mInnerQuantizer.quantize(pixels, maxColors, filters);
return;
}
mNonTransparentRatio = (float) realSize / pixels.length;
final int[] samplePixels = new int[realSize];
int rowIndex = 0;
for (int i = pixels.length - 1; i > 0; i--) {
if ((pixels[i] & NON_TRANSPARENT) == NON_TRANSPARENT) {
samplePixels[rowIndex] = pixels[i];
rowIndex++;
}
}
mInnerQuantizer.quantize(samplePixels, maxColors, filters);
}
@Override
public List<Palette.Swatch> getQuantizedColors() {
return mInnerQuantizer.getQuantizedColors();
}
}
}
}
private static class StartingSurfaceWindowView extends FrameLayout {
// TODO animate the icon view
private final View mIconView;
StartingSurfaceWindowView(Context context, int iconSize) {
super(context);
final boolean emptyIcon = iconSize == 0;
if (emptyIcon) {
mIconView = null;
} else {
mIconView = new View(context);
FrameLayout.LayoutParams params =
new FrameLayout.LayoutParams(iconSize, iconSize);
params.gravity = Gravity.CENTER;
addView(mIconView, params);
}
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
// TODO support animatable icon
void setIconDrawable(Drawable icon) {
if (mIconView != null) {
mIconView.setBackground(icon);
}
}
}
}

View File

@@ -35,7 +35,6 @@ import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.hardware.display.DisplayManager;
import android.os.IBinder;
import android.os.RemoteException;
@@ -64,6 +63,7 @@ import java.util.function.Consumer;
* class to remove the starting window of the Task.
* @hide
*/
public class StartingSurfaceDrawer {
static final String TAG = StartingSurfaceDrawer.class.getSimpleName();
static final boolean DEBUG_SPLASH_SCREEN = false;
@@ -72,6 +72,7 @@ public class StartingSurfaceDrawer {
private final Context mContext;
private final DisplayManager mDisplayManager;
final ShellExecutor mMainExecutor;
private final SplashscreenContentDrawer mSplashscreenContentDrawer;
// TODO(b/131727939) remove this when clearing ActivityRecord
private static final int REMOVE_WHEN_TIMEOUT = 2000;
@@ -80,6 +81,7 @@ public class StartingSurfaceDrawer {
mContext = context;
mDisplayManager = mContext.getSystemService(DisplayManager.class);
mMainExecutor = mainExecutor;
mSplashscreenContentDrawer = new SplashscreenContentDrawer(context);
}
private final SparseArray<StartingWindowRecord> mStartingWindowRecords = new SparseArray<>();
@@ -234,7 +236,6 @@ public class StartingSurfaceDrawer {
return;
}
context = displayContext;
if (theme != context.getThemeResId() || labelRes != 0) {
try {
context = context.createPackageContext(
@@ -347,7 +348,12 @@ public class StartingSurfaceDrawer {
}
params.setTitle("Splash Screen " + activityInfo.packageName);
addSplashscreenContent(win, context, splashscreenContentResId[0]);
final View contentView = mSplashscreenContentDrawer.makeSplashScreenContentView(win,
context, iconRes, splashscreenContentResId[0]);
if (contentView == null) {
Slog.w(TAG, "Adding splash screen window for " + activityInfo.packageName + " failed!");
return;
}
final View view = win.getDecorView();
@@ -459,20 +465,4 @@ public class StartingSurfaceDrawer {
mTaskSnapshotWindow = taskSnapshotWindow;
}
}
private void addSplashscreenContent(PhoneWindow win, Context ctx,
int splashscreenContentResId) {
if (splashscreenContentResId == 0) {
return;
}
final Drawable drawable = ctx.getDrawable(splashscreenContentResId);
if (drawable == null) {
return;
}
// We wrap this into a view so the system insets get applied to the drawable.
final View v = new View(ctx);
v.setBackground(drawable);
win.setContentView(v);
}
}