From b37faec28713934ec5bea41935a6bf83c29c011c Mon Sep 17 00:00:00 2001 From: Jeremy Sim Date: Fri, 22 Mar 2024 16:05:09 -0700 Subject: [PATCH] Refactor how app pair icons draw This changes (and cleans up) the way app pair icons are composed. Previously, the background and 2 icons were drawn individually and separately onto the canvas. Now, they are composed into a combined drawable first. This also allows the full icon drawable to be requested by external functions (which will be needed for display app pairs in folder previews). Bug: 315731527 Flag: ACONFIG com.android.wm.shell.enable_app_pairs TRUNKFOOD Test: Visually confirmed that app pairs loooks the same in all scenarios: rotation, disabled, themed, taskbar, pinned taskbar. Screenshot test to follow. Change-Id: I7242e0c525ef578a54a06fb9137fcfc42c6f0e86 --- .../launcher3/taskbar/TaskbarView.java | 3 +- src/com/android/launcher3/BubbleTextView.java | 7 +- .../launcher3/apppairs/AppPairIcon.java | 64 ++++-- .../apppairs/AppPairIconBackground.java | 167 -------------- .../apppairs/AppPairIconDrawable.java | 208 ++++++++++++++++++ .../apppairs/AppPairIconDrawingParams.kt | 98 +++++++++ .../launcher3/apppairs/AppPairIconGraphic.kt | 186 ++++++---------- .../graphics/LauncherPreviewRenderer.java | 8 +- .../android/launcher3/util/ItemInflater.kt | 3 +- 9 files changed, 427 insertions(+), 317 deletions(-) delete mode 100644 src/com/android/launcher3/apppairs/AppPairIconBackground.java create mode 100644 src/com/android/launcher3/apppairs/AppPairIconDrawable.java create mode 100644 src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java index 367bf6cfb2..c81bf7aacc 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -354,7 +354,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar break; case ITEM_TYPE_APP_PAIR: hotseatView = AppPairIcon.inflateIcon( - expectedLayoutResId, mActivityContext, this, folderInfo); + expectedLayoutResId, mActivityContext, this, folderInfo, + BubbleTextView.DISPLAY_TASKBAR); ((AppPairIcon) hotseatView).setTextVisible(false); break; default: diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 3ee1c614f2..1285aca8b2 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -58,7 +58,6 @@ import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; import com.android.launcher3.accessibility.BaseAccessibilityDelegate; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dot.DotInfo; import com.android.launcher3.dragndrop.DragOptions.PreDragCondition; import com.android.launcher3.dragndrop.DraggableView; @@ -96,10 +95,10 @@ import java.util.Locale; public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, IconLabelDotView, DraggableView, Reorderable { - private static final int DISPLAY_WORKSPACE = 0; + public static final int DISPLAY_WORKSPACE = 0; public static final int DISPLAY_ALL_APPS = 1; - private static final int DISPLAY_FOLDER = 2; - protected static final int DISPLAY_TASKBAR = 5; + public static final int DISPLAY_FOLDER = 2; + public static final int DISPLAY_TASKBAR = 5; public static final int DISPLAY_SEARCH_RESULT = 6; public static final int DISPLAY_SEARCH_RESULT_SMALL = 7; public static final int DISPLAY_PREDICTION_ROW = 8; diff --git a/src/com/android/launcher3/apppairs/AppPairIcon.java b/src/com/android/launcher3/apppairs/AppPairIcon.java index 13fefc4b5b..bbeb34199f 100644 --- a/src/com/android/launcher3/apppairs/AppPairIcon.java +++ b/src/com/android/launcher3/apppairs/AppPairIcon.java @@ -16,12 +16,13 @@ package com.android.launcher3.apppairs; +import static com.android.launcher3.BubbleTextView.DISPLAY_FOLDER; + import android.content.Context; +import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; -import android.util.Log; import android.view.LayoutInflater; -import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -37,7 +38,6 @@ import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.util.MultiTranslateDelegate; import com.android.launcher3.views.ActivityContext; -import java.util.Collections; import java.util.Comparator; import java.util.function.Predicate; @@ -61,6 +61,9 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab private BubbleTextView mAppPairName; // The underlying ItemInfo that stores info about the app pair members, etc. private FolderInfo mInfo; + // The containing element that holds this icon: workspace, taskbar, folder, etc. Affects certain + // aspects of how the icon is drawn. + private int mContainer; // Required for Reorderable -- handles translation and bouncing movements private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this); @@ -78,7 +81,7 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab * Builds an AppPairIcon to be added to the Launcher. */ public static AppPairIcon inflateIcon(int resId, ActivityContext activity, - @Nullable ViewGroup group, FolderInfo appPairInfo) { + @Nullable ViewGroup group, FolderInfo appPairInfo, int container) { DeviceProfile grid = activity.getDeviceProfile(); LayoutInflater inflater = (group != null) ? LayoutInflater.from(group.getContext()) @@ -86,31 +89,32 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab AppPairIcon icon = (AppPairIcon) inflater.inflate(resId, group, false); // Sort contents, so that left-hand app comes first - Collections.sort(appPairInfo.contents, Comparator.comparingInt(a -> a.rank)); + appPairInfo.contents.sort(Comparator.comparingInt(a -> a.rank)); - icon.setClipToPadding(false); icon.setTag(appPairInfo); icon.setOnClickListener(activity.getItemOnClickListener()); icon.mInfo = appPairInfo; - - // TODO (b/326664798): Delete this check, instead check at launcher load time - if (icon.mInfo.contents.size() != 2) { - Log.wtf(TAG, "AppPair contents not 2, size: " + icon.mInfo.contents.size()); - return icon; - } + icon.mContainer = container; // Set up icon drawable area icon.mIconGraphic = icon.findViewById(R.id.app_pair_icon_graphic); - icon.mIconGraphic.init(activity, icon); + icon.mIconGraphic.init(icon, container); icon.checkDisabledState(); // Set up app pair title icon.mAppPairName = icon.findViewById(R.id.app_pair_icon_name); - icon.mAppPairName.setCompoundDrawablePadding(0); FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mAppPairName.getLayoutParams(); - lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx; + // Shift the title text down to leave room for the icon graphic. Since the icon graphic is + // a separate element (and not set as a CompoundDrawable on the BubbleTextView), we need to + // shift the text down manually. + lp.topMargin = container == DISPLAY_FOLDER + ? grid.folderChildIconSizePx + grid.folderChildDrawablePaddingPx + : grid.iconSizePx + grid.iconDrawablePaddingPx; + // For some reason, app icons have setIncludeFontPadding(false) inside folders, so we set it + // here to match that. + icon.mAppPairName.setIncludeFontPadding(container != DISPLAY_FOLDER); icon.mAppPairName.setText(appPairInfo.title); // Set up accessibility @@ -174,7 +178,11 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab return mInfo; } - public View getIconDrawableArea() { + public BubbleTextView getTitleTextView() { + return mAppPairName; + } + + public AppPairIconGraphic getIconDrawableArea() { return mIconGraphic; } @@ -195,8 +203,8 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab mIsLaunchableAtScreenSize = dp.isTablet || getInfo().contents.stream().noneMatch( wii -> wii.hasStatusFlag(WorkspaceItemInfo.FLAG_NON_RESIZEABLE)); - // Call applyIcons to check and update icons - mIconGraphic.applyIcons(); + // Invalidate to update icons + mIconGraphic.redraw(); } /** @@ -207,7 +215,25 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab // updated apps), redraw the icon graphic (icon background and both icons). if (getInfo().contents.stream().anyMatch(itemCheck)) { checkDisabledState(); - mIconGraphic.invalidate(); } } + + /** + * Inside folders, icons are vertically centered in their rows. See + * {@link BubbleTextView#onMeasure(int, int)} for comparison. + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mContainer == DISPLAY_FOLDER) { + int height = MeasureSpec.getSize(heightMeasureSpec); + ActivityContext activity = ActivityContext.lookupContext(getContext()); + Paint.FontMetrics fm = mAppPairName.getPaint().getFontMetrics(); + int cellHeightPx = activity.getDeviceProfile().folderChildIconSizePx + + activity.getDeviceProfile().folderChildDrawablePaddingPx + + (int) Math.ceil(fm.bottom - fm.top); + setPadding(getPaddingLeft(), (height - cellHeightPx) / 2, getPaddingRight(), + getPaddingBottom()); + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } } diff --git a/src/com/android/launcher3/apppairs/AppPairIconBackground.java b/src/com/android/launcher3/apppairs/AppPairIconBackground.java deleted file mode 100644 index 187541ff93..0000000000 --- a/src/com/android/launcher3/apppairs/AppPairIconBackground.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright (C) 2023 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.apppairs; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.ColorFilter; -import android.graphics.Paint; -import android.graphics.PixelFormat; -import android.graphics.RectF; -import android.graphics.drawable.Drawable; -import android.os.Build; - -import com.android.launcher3.R; - -/** - * A Drawable for the background behind the twin app icons (looks like two rectangles). - */ -class AppPairIconBackground extends Drawable { - // The underlying view that we are drawing this background on. - private final AppPairIconGraphic icon; - private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - - /** - * Null values to use with - * {@link Canvas#drawDoubleRoundRect(RectF, float[], RectF, float[], Paint)}, since there - * doesn't seem to be any other API for drawing rectangles with 4 different corner radii. - */ - private static final RectF EMPTY_RECT = new RectF(); - private static final float[] ARRAY_OF_ZEROES = new float[8]; - - AppPairIconBackground(Context context, AppPairIconGraphic iconGraphic) { - icon = iconGraphic; - // Set up background paint color - TypedArray ta = context.getTheme().obtainStyledAttributes(R.styleable.FolderIconPreview); - mBackgroundPaint.setStyle(Paint.Style.FILL); - mBackgroundPaint.setColor( - ta.getColor(R.styleable.FolderIconPreview_folderPreviewColor, 0)); - ta.recycle(); - } - - @Override - public void draw(Canvas canvas) { - if (icon.isLeftRightSplit()) { - drawLeftRightSplit(canvas); - } else { - drawTopBottomSplit(canvas); - } - } - - /** - * When device is in landscape, we draw the rectangles with a left-right split. - */ - private void drawLeftRightSplit(Canvas canvas) { - // Get the bounds where we will draw the background image - int width = getBounds().width(); - int height = getBounds().height(); - - // The left half of the background image, excluding center channel - RectF leftSide = new RectF( - 0, - 0, - (width / 2f) - (icon.getCenterChannelSize() / 2f), - height - ); - // The right half of the background image, excluding center channel - RectF rightSide = new RectF( - (width / 2f) + (icon.getCenterChannelSize() / 2f), - 0, - width, - height - ); - - drawCustomRoundedRect(canvas, leftSide, new float[]{ - icon.getBigRadius(), icon.getBigRadius(), - icon.getSmallRadius(), icon.getSmallRadius(), - icon.getSmallRadius(), icon.getSmallRadius(), - icon.getBigRadius(), icon.getBigRadius()}); - drawCustomRoundedRect(canvas, rightSide, new float[]{ - icon.getSmallRadius(), icon.getSmallRadius(), - icon.getBigRadius(), icon.getBigRadius(), - icon.getBigRadius(), icon.getBigRadius(), - icon.getSmallRadius(), icon.getSmallRadius()}); - } - - /** - * When device is in portrait, we draw the rectangles with a top-bottom split. - */ - private void drawTopBottomSplit(Canvas canvas) { - // Get the bounds where we will draw the background image - int width = getBounds().width(); - int height = getBounds().height(); - - // The top half of the background image, excluding center channel - RectF topSide = new RectF( - 0, - 0, - width, - (height / 2f) - (icon.getCenterChannelSize() / 2f) - ); - // The bottom half of the background image, excluding center channel - RectF bottomSide = new RectF( - 0, - (height / 2f) + (icon.getCenterChannelSize() / 2f), - width, - height - ); - - drawCustomRoundedRect(canvas, topSide, new float[]{ - icon.getBigRadius(), icon.getBigRadius(), - icon.getBigRadius(), icon.getBigRadius(), - icon.getSmallRadius(), icon.getSmallRadius(), - icon.getSmallRadius(), icon.getSmallRadius()}); - drawCustomRoundedRect(canvas, bottomSide, new float[]{ - icon.getSmallRadius(), icon.getSmallRadius(), - icon.getSmallRadius(), icon.getSmallRadius(), - icon.getBigRadius(), icon.getBigRadius(), - icon.getBigRadius(), icon.getBigRadius()}); - } - - /** - * Draws a rectangle with custom rounded corners. - * @param c The Canvas to draw on. - * @param rect The bounds of the rectangle. - * @param radii An array of 8 radii for the corners: top left x, top left y, top right x, top - * right y, bottom right x, and so on. - */ - private void drawCustomRoundedRect(Canvas c, RectF rect, float[] radii) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - // Canvas.drawDoubleRoundRect is supported from Q onward - c.drawDoubleRoundRect(rect, radii, EMPTY_RECT, ARRAY_OF_ZEROES, mBackgroundPaint); - } else { - // Fallback rectangle with uniform rounded corners - c.drawRoundRect(rect, icon.getBigRadius(), icon.getBigRadius(), mBackgroundPaint); - } - } - - @Override - public int getOpacity() { - return PixelFormat.OPAQUE; - } - - @Override - public void setAlpha(int i) { - mBackgroundPaint.setAlpha(i); - } - - @Override - public void setColorFilter(ColorFilter colorFilter) { - mBackgroundPaint.setColorFilter(colorFilter); - } -} diff --git a/src/com/android/launcher3/apppairs/AppPairIconDrawable.java b/src/com/android/launcher3/apppairs/AppPairIconDrawable.java new file mode 100644 index 0000000000..c0ac11a4a1 --- /dev/null +++ b/src/com/android/launcher3/apppairs/AppPairIconDrawable.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2023 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.apppairs; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.os.Build; + +import androidx.annotation.NonNull; + +import com.android.launcher3.icons.FastBitmapDrawable; + +/** + * A composed Drawable consisting of the two app pair icons and the background behind them (looks + * like two rectangles). + */ +class AppPairIconDrawable extends Drawable { + private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final AppPairIconDrawingParams mP; + private final FastBitmapDrawable mIcon1; + private final FastBitmapDrawable mIcon2; + + /** + * Null values to use with + * {@link Canvas#drawDoubleRoundRect(RectF, float[], RectF, float[], Paint)}, since there + * doesn't seem to be any other API for drawing rectangles with 4 different corner radii. + */ + private static final RectF EMPTY_RECT = new RectF(); + private static final float[] ARRAY_OF_ZEROES = new float[8]; + + AppPairIconDrawable( + AppPairIconDrawingParams p, FastBitmapDrawable icon1, FastBitmapDrawable icon2) { + mP = p; + mBackgroundPaint.setStyle(Paint.Style.FILL); + mBackgroundPaint.setColor(p.getBgColor()); + mIcon1 = icon1; + mIcon2 = icon2; + } + + @Override + public void draw(@NonNull Canvas canvas) { + if (mP.isLeftRightSplit()) { + drawLeftRightSplit(canvas); + } else { + drawTopBottomSplit(canvas); + } + + canvas.translate( + mP.getStandardIconPadding() + mP.getOuterPadding(), + mP.getStandardIconPadding() + mP.getOuterPadding() + ); + + // Draw first icon. + canvas.save(); + // The app icons are placed differently depending on device orientation. + if (mP.isLeftRightSplit()) { + canvas.translate( + mP.getInnerPadding(), + mP.getBackgroundSize() / 2f - mP.getMemberIconSize() / 2f + ); + } else { + canvas.translate( + mP.getBackgroundSize() / 2f - mP.getMemberIconSize() / 2f, + mP.getInnerPadding() + ); + } + + mIcon1.draw(canvas); + canvas.restore(); + + // Draw second icon. + canvas.save(); + // The app icons are placed differently depending on device orientation. + if (mP.isLeftRightSplit()) { + canvas.translate( + mP.getBackgroundSize() - (mP.getInnerPadding() + mP.getMemberIconSize()), + mP.getBackgroundSize() / 2f - mP.getMemberIconSize() / 2f + ); + } else { + canvas.translate( + mP.getBackgroundSize() / 2f - mP.getMemberIconSize() / 2f, + mP.getBackgroundSize() - (mP.getInnerPadding() + mP.getMemberIconSize()) + ); + } + + mIcon2.draw(canvas); + } + + /** + * When device is in landscape, we draw the rectangles with a left-right split. + */ + private void drawLeftRightSplit(Canvas canvas) { + // Get the bounds where we will draw the background image + int width = mP.getIconSize(); + int height = mP.getIconSize(); + + // The left half of the background image, excluding center channel + RectF leftSide = new RectF( + mP.getStandardIconPadding() + mP.getOuterPadding(), + mP.getStandardIconPadding() + mP.getOuterPadding(), + (width / 2f) - (mP.getCenterChannelSize() / 2f), + height - (mP.getStandardIconPadding() + mP.getOuterPadding()) + ); + // The right half of the background image, excluding center channel + RectF rightSide = new RectF( + (width / 2f) + (mP.getCenterChannelSize() / 2f), + (mP.getStandardIconPadding() + mP.getOuterPadding()), + width - (mP.getStandardIconPadding() + mP.getOuterPadding()), + height - (mP.getStandardIconPadding() + mP.getOuterPadding()) + ); + + drawCustomRoundedRect(canvas, leftSide, new float[]{ + mP.getBigRadius(), mP.getBigRadius(), + mP.getSmallRadius(), mP.getSmallRadius(), + mP.getSmallRadius(), mP.getSmallRadius(), + mP.getBigRadius(), mP.getBigRadius()}); + drawCustomRoundedRect(canvas, rightSide, new float[]{ + mP.getSmallRadius(), mP.getSmallRadius(), + mP.getBigRadius(), mP.getBigRadius(), + mP.getBigRadius(), mP.getBigRadius(), + mP.getSmallRadius(), mP.getSmallRadius()}); + } + + /** + * When device is in portrait, we draw the rectangles with a top-bottom split. + */ + private void drawTopBottomSplit(Canvas canvas) { + // Get the bounds where we will draw the background image + int width = mP.getIconSize(); + int height = mP.getIconSize(); + + // The top half of the background image, excluding center channel + RectF topSide = new RectF( + (mP.getStandardIconPadding() + mP.getOuterPadding()), + (mP.getStandardIconPadding() + mP.getOuterPadding()), + width - (mP.getStandardIconPadding() + mP.getOuterPadding()), + (height / 2f) - (mP.getCenterChannelSize() / 2f) + ); + // The bottom half of the background image, excluding center channel + RectF bottomSide = new RectF( + (mP.getStandardIconPadding() + mP.getOuterPadding()), + (height / 2f) + (mP.getCenterChannelSize() / 2f), + width - (mP.getStandardIconPadding() + mP.getOuterPadding()), + height - (mP.getStandardIconPadding() + mP.getOuterPadding()) + ); + + drawCustomRoundedRect(canvas, topSide, new float[]{ + mP.getBigRadius(), mP.getBigRadius(), + mP.getBigRadius(), mP.getBigRadius(), + mP.getSmallRadius(), mP.getSmallRadius(), + mP.getSmallRadius(), mP.getSmallRadius()}); + drawCustomRoundedRect(canvas, bottomSide, new float[]{ + mP.getSmallRadius(), mP.getSmallRadius(), + mP.getSmallRadius(), mP.getSmallRadius(), + mP.getBigRadius(), mP.getBigRadius(), + mP.getBigRadius(), mP.getBigRadius()}); + } + + /** + * Draws a rectangle with custom rounded corners. + * @param c The Canvas to draw on. + * @param rect The bounds of the rectangle. + * @param radii An array of 8 radii for the corners: top left x, top left y, top right x, top + * right y, bottom right x, and so on. + */ + private void drawCustomRoundedRect(Canvas c, RectF rect, float[] radii) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // Canvas.drawDoubleRoundRect is supported from Q onward + c.drawDoubleRoundRect(rect, radii, EMPTY_RECT, ARRAY_OF_ZEROES, mBackgroundPaint); + } else { + // Fallback rectangle with uniform rounded corners + c.drawRoundRect(rect, mP.getBigRadius(), mP.getBigRadius(), mBackgroundPaint); + } + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } + + @Override + public void setAlpha(int i) { + mBackgroundPaint.setAlpha(i); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + mBackgroundPaint.setColorFilter(colorFilter); + } +} diff --git a/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt b/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt new file mode 100644 index 0000000000..62e5771776 --- /dev/null +++ b/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2024 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.apppairs + +import android.content.Context +import com.android.launcher3.BubbleTextView.DISPLAY_FOLDER +import com.android.launcher3.DeviceProfile +import com.android.launcher3.R +import com.android.launcher3.views.ActivityContext + +class AppPairIconDrawingParams(val context: Context, container: Int) { + companion object { + // Design specs -- the below ratios are in relation to the size of a standard app icon. + // Note: The standard app icon has two sizes. One is the full size of the drawable (returned + // by dp.iconSizePx), and one is the visual size of the icon on-screen (11/12 of that). + // Hence the calculations below. + const val STANDARD_ICON_PADDING = 1 / 24f + const val STANDARD_ICON_SHRINK = 1 - STANDARD_ICON_PADDING * 2 + // App pairs are slightly smaller than the *visual* size of a standard icon, so all ratios + // are calculated with that in mind. + const val OUTER_PADDING_SCALE = 1 / 30f * STANDARD_ICON_SHRINK + const val INNER_PADDING_SCALE = 1 / 24f * STANDARD_ICON_SHRINK + const val CENTER_CHANNEL_SCALE = 1 / 30f * STANDARD_ICON_SHRINK + const val BIG_RADIUS_SCALE = 1 / 5f * STANDARD_ICON_SHRINK + const val SMALL_RADIUS_SCALE = 1 / 15f * STANDARD_ICON_SHRINK + const val MEMBER_ICON_SCALE = 11 / 30f * STANDARD_ICON_SHRINK + } + + // The size at which this graphic will be drawn. + val iconSize: Int + // Standard app icons are padded by this amount on each side. + val standardIconPadding: Float + // App pair icons are slightly smaller than regular icons, so we pad the icon by this much on + // each side. + val outerPadding: Float + // The colored background (two rectangles in a square area) is this big. + val backgroundSize: Float + // The size of the channel between the two halves of the app pair icon. + val centerChannelSize: Float + // The corner radius of the outside corners. + val bigRadius: Float + // The corner radius of the inside corners, touching the center channel. + val smallRadius: Float + // Inside of the icon, the two member apps are padded by this much. + val innerPadding: Float + // The two member apps have icons that are this big (in diameter). + val memberIconSize: Float + // The app pair icon appears differently in portrait and landscape. + var isLeftRightSplit: Boolean = true + // The background paint color (based on container). + val bgColor: Int + + init { + val activity: ActivityContext = ActivityContext.lookupContext(context) + val dp = activity.deviceProfile + iconSize = if (container == DISPLAY_FOLDER) dp.folderChildIconSizePx else dp.iconSizePx + standardIconPadding = iconSize * STANDARD_ICON_PADDING + outerPadding = iconSize * OUTER_PADDING_SCALE + backgroundSize = iconSize * STANDARD_ICON_SHRINK - (outerPadding * 2) + centerChannelSize = iconSize * CENTER_CHANNEL_SCALE + bigRadius = iconSize * BIG_RADIUS_SCALE + smallRadius = iconSize * SMALL_RADIUS_SCALE + innerPadding = iconSize * INNER_PADDING_SCALE + memberIconSize = iconSize * MEMBER_ICON_SCALE + updateOrientation(dp) + if (container == DISPLAY_FOLDER) { + val ta = + context.theme.obtainStyledAttributes( + intArrayOf(R.attr.materialColorSurfaceContainerLowest) + ) + bgColor = ta.getColor(0, 0) + ta.recycle() + } else { + val ta = context.theme.obtainStyledAttributes(R.styleable.FolderIconPreview) + bgColor = ta.getColor(R.styleable.FolderIconPreview_folderPreviewColor, 0) + ta.recycle() + } + } + + /** Checks the device orientation and updates isLeftRightSplit accordingly. */ + fun updateOrientation(dp: DeviceProfile) { + isLeftRightSplit = dp.isLeftRightSplit + } +} diff --git a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt index 777831bb61..04050b0294 100644 --- a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt +++ b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt @@ -21,14 +21,14 @@ import android.graphics.Canvas import android.graphics.Rect import android.graphics.drawable.Drawable import android.util.AttributeSet -import android.util.Log import android.view.Gravity import android.widget.FrameLayout import com.android.launcher3.DeviceProfile import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener import com.android.launcher3.icons.BitmapInfo -import com.android.launcher3.icons.FastBitmapDrawable import com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter +import com.android.launcher3.model.data.FolderInfo +import com.android.launcher3.model.data.WorkspaceItemInfo import com.android.launcher3.util.Themes import com.android.launcher3.views.ActivityContext @@ -41,161 +41,101 @@ class AppPairIconGraphic @JvmOverloads constructor(context: Context, attrs: Attr private val TAG = "AppPairIconGraphic" companion object { - // Design specs -- the below ratios are in relation to the size of a standard app icon. - private const val OUTER_PADDING_SCALE = 1 / 30f - private const val INNER_PADDING_SCALE = 1 / 24f - private const val MEMBER_ICON_SCALE = 11 / 30f - private const val CENTER_CHANNEL_SCALE = 1 / 30f - private const val BIG_RADIUS_SCALE = 1 / 5f - private const val SMALL_RADIUS_SCALE = 1 / 15f + /** Composes a drawable for this icon, consisting of a background and 2 app icons. */ + @JvmStatic + fun composeDrawable(appPairInfo: FolderInfo, p: AppPairIconDrawingParams): Drawable { + // Generate new icons, using themed flag if needed. + val flags = if (Themes.isThemedIconEnabled(p.context)) BitmapInfo.FLAG_THEMED else 0 + val appIcon1 = appPairInfo.contents[0].newIcon(p.context, flags) + val appIcon2 = appPairInfo.contents[1].newIcon(p.context, flags) + appIcon1.setBounds(0, 0, p.memberIconSize.toInt(), p.memberIconSize.toInt()) + appIcon2.setBounds(0, 0, p.memberIconSize.toInt(), p.memberIconSize.toInt()) + + // Check disabled status. + val activity: ActivityContext = ActivityContext.lookupContext(p.context) + val isLaunchableAtScreenSize = + activity.deviceProfile.isTablet || + appPairInfo.contents.stream().noneMatch { wii: WorkspaceItemInfo -> + wii.hasStatusFlag(WorkspaceItemInfo.FLAG_NON_RESIZEABLE) + } + val shouldDrawAsDisabled = appPairInfo.isDisabled || !isLaunchableAtScreenSize + + // Set disabled status on icons. + appIcon1.setIsDisabled(shouldDrawAsDisabled) + appIcon2.setIsDisabled(shouldDrawAsDisabled) + + // Create icon drawable. + val fullIconDrawable = AppPairIconDrawable(p, appIcon1, appIcon2) + fullIconDrawable.setBounds(0, 0, p.iconSize, p.iconSize) + + // Set disabled color filter on background paint. + fullIconDrawable.colorFilter = + if (shouldDrawAsDisabled) getDisabledColorFilter() else null + + return fullIconDrawable + } } - // App pair icons are slightly smaller than regular icons, so we pad the icon by this much on - // each side. - private var outerPadding = 0f - // Inside of the icon, the two member apps are padded by this much. - private var innerPadding = 0f - // The colored background (two rectangles in a square area) is this big. - private var backgroundSize = 0f - // The two member apps have icons that are this big (in diameter). - private var memberIconSize = 0f - // The size of the center channel. - var centerChannelSize = 0f - // The large outer radius of the background rectangles. - var bigRadius = 0f - // The small inner radius of the background rectangles. - var smallRadius = 0f - // The app pairs icon appears differently in portrait and landscape. - var isLeftRightSplit = false - - private lateinit var activityContext: ActivityContext private lateinit var parentIcon: AppPairIcon - private lateinit var appPairBackground: Drawable - private lateinit var appIcon1: FastBitmapDrawable - private lateinit var appIcon2: FastBitmapDrawable + private lateinit var drawParams: AppPairIconDrawingParams + private lateinit var drawable: Drawable - fun init(activity: ActivityContext, icon: AppPairIcon) { - activityContext = activity - - // Calculate device-specific measurements - val defaultIconSize = activity.deviceProfile.iconSizePx - outerPadding = OUTER_PADDING_SCALE * defaultIconSize - innerPadding = INNER_PADDING_SCALE * defaultIconSize - backgroundSize = defaultIconSize - outerPadding * 2 - memberIconSize = MEMBER_ICON_SCALE * defaultIconSize - centerChannelSize = CENTER_CHANNEL_SCALE * defaultIconSize - bigRadius = BIG_RADIUS_SCALE * defaultIconSize - smallRadius = SMALL_RADIUS_SCALE * defaultIconSize + fun init(icon: AppPairIcon, container: Int) { parentIcon = icon - updateOrientation() - - appPairBackground = AppPairIconBackground(context, this) - appPairBackground.setBounds(0, 0, backgroundSize.toInt(), backgroundSize.toInt()) - - applyIcons() + drawParams = AppPairIconDrawingParams(context, container) + drawable = composeDrawable(icon.info, drawParams) // Center the drawable area in the larger icon canvas val lp: LayoutParams = layoutParams as LayoutParams lp.gravity = Gravity.CENTER_HORIZONTAL - lp.topMargin = outerPadding.toInt() - lp.height = backgroundSize.toInt() - lp.width = backgroundSize.toInt() + lp.height = drawParams.iconSize + lp.width = drawParams.iconSize layoutParams = lp } override fun onAttachedToWindow() { super.onAttachedToWindow() - activityContext.addOnDeviceProfileChangeListener(this) + getActivityContext().addOnDeviceProfileChangeListener(this) } override fun onDetachedFromWindow() { super.onDetachedFromWindow() - activityContext.removeOnDeviceProfileChangeListener(this) + getActivityContext().removeOnDeviceProfileChangeListener(this) } - /** Checks the device orientation and updates isLeftRightSplit accordingly. */ - private fun updateOrientation() { - val activity: ActivityContext = ActivityContext.lookupContext(context) - isLeftRightSplit = activity.deviceProfile.isLeftRightSplit + private fun getActivityContext(): ActivityContext { + return ActivityContext.lookupContext(context) } /** When device profile changes, update orientation */ - override fun onDeviceProfileChanged(dp: DeviceProfile?) { - updateOrientation() + override fun onDeviceProfileChanged(dp: DeviceProfile) { + drawParams.updateOrientation(dp) + redraw() + } + + /** Updates the icon drawable and redraws it */ + fun redraw() { + drawable = composeDrawable(parentIcon.info, drawParams) invalidate() } - /** Sets up app pair member icons for drawing. */ - fun applyIcons() { - val apps = parentIcon.info.contents - - // TODO (b/326664798): Delete this check, instead check at launcher load time - if (apps.size != 2) { - Log.wtf(TAG, "AppPair contents not 2, size: " + apps.size, Throwable()) - return - } - - // Generate new icons, using themed flag if needed - val flags = if (Themes.isThemedIconEnabled(context)) BitmapInfo.FLAG_THEMED else 0 - appIcon1 = apps[0].newIcon(context, flags) - appIcon2 = apps[1].newIcon(context, flags) - appIcon1.setBounds(0, 0, memberIconSize.toInt(), memberIconSize.toInt()) - appIcon2.setBounds(0, 0, memberIconSize.toInt(), memberIconSize.toInt()) - - // Check disabled state - val shouldDrawAsDisabled = - parentIcon.info.isDisabled || !parentIcon.isLaunchableAtScreenSize - - appPairBackground.colorFilter = if (shouldDrawAsDisabled) getDisabledColorFilter() else null - appIcon1.setIsDisabled(shouldDrawAsDisabled) - appIcon2.setIsDisabled(shouldDrawAsDisabled) - } - - /** Gets this icon graphic's bounds, with respect to the parent icon's coordinate system. */ + /** + * Gets this icon graphic's visual bounds, with respect to the parent icon's coordinate system. + */ fun getIconBounds(outBounds: Rect) { - outBounds.set(0, 0, backgroundSize.toInt(), backgroundSize.toInt()) + outBounds.set(0, 0, drawParams.backgroundSize.toInt(), drawParams.backgroundSize.toInt()) + outBounds.offset( // x-coordinate in parent's coordinate system - ((parentIcon.width - backgroundSize) / 2).toInt(), + ((parentIcon.width - drawParams.backgroundSize) / 2).toInt(), // y-coordinate in parent's coordinate system - parentIcon.paddingTop + outerPadding.toInt() + (parentIcon.paddingTop + drawParams.standardIconPadding + drawParams.outerPadding) + .toInt() ) } override fun dispatchDraw(canvas: Canvas) { super.dispatchDraw(canvas) - - // Draw background - appPairBackground.draw(canvas) - - // Draw first icon - canvas.save() - // The app icons are placed differently depending on device orientation. - if (isLeftRightSplit) { - canvas.translate(innerPadding, height / 2f - memberIconSize / 2f) - } else { - canvas.translate(width / 2f - memberIconSize / 2f, innerPadding) - } - - appIcon1.draw(canvas) - canvas.restore() - - // Draw second icon - canvas.save() - // The app icons are placed differently depending on device orientation. - if (isLeftRightSplit) { - canvas.translate( - width - (innerPadding + memberIconSize), - height / 2f - memberIconSize / 2f - ) - } else { - canvas.translate( - width / 2f - memberIconSize / 2f, - height - (innerPadding + memberIconSize) - ) - } - - appIcon2.draw(canvas) - canvas.restore() + drawable.draw(canvas) } } diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java index e0a66276b6..1a57d91d7c 100644 --- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java +++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java @@ -20,6 +20,8 @@ import static android.view.View.MeasureSpec.EXACTLY; import static android.view.View.MeasureSpec.makeMeasureSpec; import static android.view.View.VISIBLE; +import static com.android.launcher3.BubbleTextView.DISPLAY_TASKBAR; +import static com.android.launcher3.BubbleTextView.DISPLAY_WORKSPACE; import static com.android.launcher3.DeviceProfile.DEFAULT_SCALE; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; import static com.android.launcher3.config.FeatureFlags.shouldShowFirstPageWidget; @@ -388,12 +390,14 @@ public class LauncherPreviewRenderer extends ContextWrapper } private void inflateAndAddCollectionIcon(FolderInfo info) { - CellLayout screen = info.container == Favorites.CONTAINER_DESKTOP + boolean isOnDesktop = info.container == Favorites.CONTAINER_DESKTOP; + CellLayout screen = isOnDesktop ? mWorkspaceScreens.get(info.screenId) : mHotseat; FrameLayout folderIcon = info.itemType == Favorites.ITEM_TYPE_FOLDER ? FolderIcon.inflateIcon(R.layout.folder_icon, this, screen, info) - : AppPairIcon.inflateIcon(R.layout.app_pair_icon, this, screen, info); + : AppPairIcon.inflateIcon(R.layout.app_pair_icon, this, screen, info, + isOnDesktop ? DISPLAY_WORKSPACE : DISPLAY_TASKBAR); addInScreenFromBind(folderIcon, info); } diff --git a/src/com/android/launcher3/util/ItemInflater.kt b/src/com/android/launcher3/util/ItemInflater.kt index cc66af1189..0f8311d50a 100644 --- a/src/com/android/launcher3/util/ItemInflater.kt +++ b/src/com/android/launcher3/util/ItemInflater.kt @@ -81,7 +81,8 @@ class ItemInflater( R.layout.app_pair_icon, context, parent, - item as FolderInfo + item as FolderInfo, + BubbleTextView.DISPLAY_WORKSPACE ) Favorites.ITEM_TYPE_APPWIDGET, Favorites.ITEM_TYPE_CUSTOM_APPWIDGET ->