All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.wm.shell.startingsurface.SplashscreenIconDrawableFactory Maven / Gradle / Ivy

/*
 * 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.AnimatorListenerAdapter;
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.AnimatedVectorDrawable;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Trace;
import android.util.Log;
import android.util.PathParser;
import android.window.SplashScreenView;

import com.android.internal.R;

import java.util.function.LongConsumer;

/**
 * Creating a lightweight Drawable object used for splash screen.
 *
 * @hide
 */
public class SplashscreenIconDrawableFactory {

    private static final String TAG = StartingWindowController.TAG;

    /**
     * @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,
            boolean loadInDetail, 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, loadInDetail, 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, loadInDetail, splashscreenWorkerHandler);
        }

        if (drawBackground) {
            background = new MaskBackgroundDrawable(backgroundColor);
        }

        return new Drawable[]{foreground, background};
    }

    static Drawable[] makeLegacyIconDrawable(@NonNull Drawable iconDrawable, int srcIconSize,
            int iconSize, boolean loadInDetail, Handler splashscreenWorkerHandler) {
        return new Drawable[]{new ImmobileIconDrawable(iconDrawable, srcIconSize, iconSize,
                loadInDetail, 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, boolean loadInDetail,
                Handler splashscreenWorkerHandler) {
            // This icon has lower density, don't scale it.
            if (loadInDetail) {
                splashscreenWorkerHandler.post(() -> preDrawIcon(drawable, iconSize));
            } else {
                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.
     */
    public static class AnimatableIconAnimateListener extends AdaptiveForegroundDrawable
            implements SplashScreenView.IconAnimateListener {
        private final Animatable mAnimatableIcon;
        private boolean mAnimationTriggered;
        private AnimatorListenerAdapter mJankMonitoringListener;
        private boolean mRunning;
        private LongConsumer mStartListener;

        AnimatableIconAnimateListener(@NonNull Drawable foregroundDrawable) {
            super(foregroundDrawable);
            Callback callback = 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);
                }
            };
            mForegroundDrawable.setCallback(callback);
            mAnimatableIcon = (Animatable) mForegroundDrawable;
        }

        @Override
        public void setAnimationJankMonitoring(AnimatorListenerAdapter listener) {
            mJankMonitoringListener = listener;
        }

        @Override
        public void prepareAnimate(LongConsumer startListener) {
            stopAnimation();
            mStartListener = startListener;
        }

        private void startAnimation() {
            if (mJankMonitoringListener != null) {
                mJankMonitoringListener.onAnimationStart(null);
            }
            try {
                mAnimatableIcon.start();
            } catch (Exception ex) {
                Log.e(TAG, "Error while running the splash screen animated icon", ex);
                mRunning = false;
                if (mJankMonitoringListener != null) {
                    mJankMonitoringListener.onAnimationCancel(null);
                }
                if (mStartListener != null) {
                    mStartListener.accept(0);
                }
                return;
            }
            long animDuration = 0;
            if (mAnimatableIcon instanceof AnimatedVectorDrawable
                    && ((AnimatedVectorDrawable) mAnimatableIcon).getTotalDuration() > 0) {
                animDuration = ((AnimatedVectorDrawable) mAnimatableIcon).getTotalDuration();
            } else if (mAnimatableIcon instanceof AnimationDrawable
                    && ((AnimationDrawable) mAnimatableIcon).getTotalDuration() > 0) {
                animDuration = ((AnimationDrawable) mAnimatableIcon).getTotalDuration();
            }
            mRunning = true;
            if (mStartListener != null) {
                mStartListener.accept(animDuration);
            }
        }

        private void onAnimationEnd() {
            mAnimatableIcon.stop();
            if (mJankMonitoringListener != null) {
                mJankMonitoringListener.onAnimationEnd(null);
            }
            mStartListener = null;
            mRunning = false;
        }

        @Override
        public void stopAnimation() {
            if (mRunning) {
                onAnimationEnd();
                mJankMonitoringListener = null;
            }
        }

        private void ensureAnimationStarted() {
            if (mAnimationTriggered) {
                return;
            }
            if (!mRunning) {
                startAnimation();
            }
            mAnimationTriggered = true;
        }

        @Override
        public void draw(Canvas canvas) {
            ensureAnimationStarted();
            super.draw(canvas);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy