/* * Copyright (C) 2017 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.launcher3.dragndrop; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import android.annotation.TargetApi; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Path.Direction; import android.graphics.Picture; import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.Log; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.folder.PreviewBackground; import com.android.launcher3.icons.BitmapRenderer; import com.android.launcher3.util.Preconditions; import com.android.launcher3.views.ActivityContext; /** * {@link AdaptiveIconDrawable} representation of a {@link FolderIcon} */ @TargetApi(Build.VERSION_CODES.O) public class FolderAdaptiveIcon extends AdaptiveIconDrawable { private static final String TAG = "FolderAdaptiveIcon"; private final Drawable mBadge; private final Path mMask; private final ConstantState mConstantState; private static final Rect sTmpRect = new Rect(); private FolderAdaptiveIcon(Drawable bg, Drawable fg, Drawable badge, Path mask) { super(bg, fg); mBadge = badge; mMask = mask; mConstantState = new MyConstantState(bg.getConstantState(), fg.getConstantState(), badge.getConstantState(), mask); } @Override public Path getIconMask() { return mMask; } public Drawable getBadge() { return mBadge; } public static @Nullable FolderAdaptiveIcon createFolderAdaptiveIcon( ActivityContext activity, int folderId, Point size) { Preconditions.assertNonUiThread(); // assume square if (size.x != size.y) { return null; } int requestedSize = size.x; // Only use the size actually needed for drawing the folder icon int drawingSize = activity.getDeviceProfile().folderIconSizePx; int foregroundSize = Math.max(requestedSize, drawingSize); float shift = foregroundSize - requestedSize; Picture background = new Picture(); Picture foreground = new Picture(); Picture badge = new Picture(); Canvas bgCanvas = background.beginRecording(requestedSize, requestedSize); Canvas badgeCanvas = badge.beginRecording(requestedSize, requestedSize); Canvas fgCanvas = foreground.beginRecording(foregroundSize, foregroundSize); fgCanvas.translate(shift, shift); // Do not clip the folder drawing since the icon previews extend outside the background. Path mask = new Path(); mask.addRect(-shift, -shift, requestedSize + shift, requestedSize + shift, Direction.CCW); // Initialize the actual draw commands on the UI thread to avoid race conditions with // FolderIcon draw pass try { MAIN_EXECUTOR.submit(() -> { FolderIcon icon = activity.findFolderIcon(folderId); if (icon == null) { throw new IllegalArgumentException("Folder not found with id: " + folderId); } initLayersOnUiThread(icon, requestedSize, bgCanvas, fgCanvas, badgeCanvas); }).get(); } catch (Exception e) { Log.e(TAG, "Unable to create folder icon", e); return null; } finally { background.endRecording(); foreground.endRecording(); badge.endRecording(); } // Only convert foreground to a bitmap as it can contain multiple draw commands. Other // layers either draw a nothing or a single draw call. Bitmap fgBitmap = Bitmap.createBitmap(foreground); Paint foregroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // Do not use PictureDrawable as it moves the picture to the canvas bounds, whereas we want // to draw it at (0,0) return new FolderAdaptiveIcon( new BitmapRendererDrawable(c -> c.drawPicture(background)), new BitmapRendererDrawable( c -> c.drawBitmap(fgBitmap, -shift, -shift, foregroundPaint)), new BitmapRendererDrawable(c -> c.drawPicture(badge)), mask); } @UiThread private static void initLayersOnUiThread(FolderIcon icon, int size, Canvas backgroundCanvas, Canvas foregroundCanvas, Canvas badgeCanvas) { icon.getPreviewBounds(sTmpRect); final int previewSize = sTmpRect.width(); PreviewBackground bg = icon.getFolderBackground(); final int margin = (size - previewSize) / 2; final float previewShiftX = -sTmpRect.left + margin; final float previewShiftY = -sTmpRect.top + margin; // Initialize badge, which consists of the outline stroke, shadow and dot; these // must be rendered above the foreground badgeCanvas.save(); badgeCanvas.translate(previewShiftX, previewShiftY); icon.drawDot(badgeCanvas); badgeCanvas.restore(); // Draw foreground foregroundCanvas.save(); foregroundCanvas.translate(previewShiftX, previewShiftY); icon.getPreviewItemManager().draw(foregroundCanvas); foregroundCanvas.restore(); // Draw background Paint backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); backgroundPaint.setColor(bg.getBgColor()); bg.drawShadow(backgroundCanvas); backgroundCanvas.drawCircle(size / 2f, size / 2f, bg.getRadius(), backgroundPaint); bg.drawBackgroundStroke(backgroundCanvas); } @Override public ConstantState getConstantState() { return mConstantState; } private static class MyConstantState extends ConstantState { private final ConstantState mBg; private final ConstantState mFg; private final ConstantState mBadge; private final Path mMask; MyConstantState(ConstantState bg, ConstantState fg, ConstantState badge, Path mask) { mBg = bg; mFg = fg; mBadge = badge; mMask = mask; } @Override public Drawable newDrawable() { return new FolderAdaptiveIcon(mBg.newDrawable(), mFg.newDrawable(), mBadge.newDrawable(), mMask); } @Override public int getChangingConfigurations() { return mBg.getChangingConfigurations() & mFg.getChangingConfigurations() & mBadge.getChangingConfigurations(); } } private static class BitmapRendererDrawable extends Drawable { private final BitmapRenderer mRenderer; BitmapRendererDrawable(BitmapRenderer renderer) { mRenderer = renderer; } @Override public void draw(Canvas canvas) { mRenderer.draw(canvas); } @Override public void setAlpha(int i) { } @Override public void setColorFilter(ColorFilter colorFilter) { } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } @Override public ConstantState getConstantState() { return new MyConstantState(mRenderer); } private static class MyConstantState extends ConstantState { private final BitmapRenderer mRenderer; MyConstantState(BitmapRenderer renderer) { mRenderer = renderer; } @Override public Drawable newDrawable() { return new BitmapRendererDrawable(mRenderer); } @Override public int getChangingConfigurations() { return 0; } } } }