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
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 367bf6c..c81bf7a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -354,7 +354,8 @@
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 3ee1c61..1285aca 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -58,7 +58,6 @@
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 @@
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 13fefc4..bbeb341 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.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 @@
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 @@
* 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 @@
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 @@
return mInfo;
}
- public View getIconDrawableArea() {
+ public BubbleTextView getTitleTextView() {
+ return mAppPairName;
+ }
+
+ public AppPairIconGraphic getIconDrawableArea() {
return mIconGraphic;
}
@@ -195,8 +203,8 @@
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 @@
// 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 187541f..0000000
--- 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 0000000..c0ac11a
--- /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 0000000..62e5771
--- /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 777831b..04050b0 100644
--- a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
+++ b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
@@ -21,14 +21,14 @@
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 @@
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 e0a6627..1a57d91 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.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 @@
}
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 cc66af1..0f8311d 100644
--- a/src/com/android/launcher3/util/ItemInflater.kt
+++ b/src/com/android/launcher3/util/ItemInflater.kt
@@ -81,7 +81,8 @@
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 ->