mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-03-03 17:36:49 +00:00
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:
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
208
src/com/android/launcher3/apppairs/AppPairIconDrawable.java
Normal file
208
src/com/android/launcher3/apppairs/AppPairIconDrawable.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
Reference in New Issue
Block a user