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
This commit is contained in:
Jeremy Sim
2024-03-22 16:05:09 -07:00
parent 3a7432d782
commit b37faec287
9 changed files with 427 additions and 317 deletions

View File

@@ -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:

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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);
}

View File

@@ -81,7 +81,8 @@ class ItemInflater<T>(
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 ->