Loading quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +2 −1 Original line number Diff line number Diff line Loading @@ -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: Loading src/com/android/launcher3/BubbleTextView.java +3 −4 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading src/com/android/launcher3/apppairs/AppPairIcon.java +45 −19 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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); Loading @@ -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()) Loading @@ -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 Loading Loading @@ -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; } Loading @@ -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(); } /** Loading @@ -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); } } src/com/android/launcher3/apppairs/AppPairIconBackground.java→src/com/android/launcher3/apppairs/AppPairIconDrawable.java +208 −0 Original line number Diff line number Diff line Loading @@ -16,8 +16,6 @@ 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; Loading @@ -26,15 +24,19 @@ import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.os.Build; import com.android.launcher3.R; import androidx.annotation.NonNull; import com.android.launcher3.icons.FastBitmapDrawable; /** * A Drawable for the background behind the twin app icons (looks like two rectangles). * A composed Drawable consisting of the two app pair icons and the background behind them (looks * like two rectangles). */ class AppPairIconBackground extends Drawable { // The underlying view that we are drawing this background on. private final AppPairIconGraphic icon; 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 Loading @@ -44,23 +46,62 @@ class AppPairIconBackground extends Drawable { 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); AppPairIconDrawable( AppPairIconDrawingParams p, FastBitmapDrawable icon1, FastBitmapDrawable icon2) { mP = p; mBackgroundPaint.setStyle(Paint.Style.FILL); mBackgroundPaint.setColor( ta.getColor(R.styleable.FolderIconPreview_folderPreviewColor, 0)); ta.recycle(); mBackgroundPaint.setColor(p.getBgColor()); mIcon1 = icon1; mIcon2 = icon2; } @Override public void draw(Canvas canvas) { if (icon.isLeftRightSplit()) { 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); } /** Loading @@ -68,34 +109,34 @@ class AppPairIconBackground extends Drawable { */ private void drawLeftRightSplit(Canvas canvas) { // Get the bounds where we will draw the background image int width = getBounds().width(); int height = getBounds().height(); int width = mP.getIconSize(); int height = mP.getIconSize(); // The left half of the background image, excluding center channel RectF leftSide = new RectF( 0, 0, (width / 2f) - (icon.getCenterChannelSize() / 2f), height 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) + (icon.getCenterChannelSize() / 2f), 0, width, height (width / 2f) + (mP.getCenterChannelSize() / 2f), (mP.getStandardIconPadding() + mP.getOuterPadding()), width - (mP.getStandardIconPadding() + mP.getOuterPadding()), height - (mP.getStandardIconPadding() + mP.getOuterPadding()) ); drawCustomRoundedRect(canvas, leftSide, new float[]{ icon.getBigRadius(), icon.getBigRadius(), icon.getSmallRadius(), icon.getSmallRadius(), icon.getSmallRadius(), icon.getSmallRadius(), icon.getBigRadius(), icon.getBigRadius()}); mP.getBigRadius(), mP.getBigRadius(), mP.getSmallRadius(), mP.getSmallRadius(), mP.getSmallRadius(), mP.getSmallRadius(), mP.getBigRadius(), mP.getBigRadius()}); drawCustomRoundedRect(canvas, rightSide, new float[]{ icon.getSmallRadius(), icon.getSmallRadius(), icon.getBigRadius(), icon.getBigRadius(), icon.getBigRadius(), icon.getBigRadius(), icon.getSmallRadius(), icon.getSmallRadius()}); mP.getSmallRadius(), mP.getSmallRadius(), mP.getBigRadius(), mP.getBigRadius(), mP.getBigRadius(), mP.getBigRadius(), mP.getSmallRadius(), mP.getSmallRadius()}); } /** Loading @@ -103,34 +144,34 @@ class AppPairIconBackground extends Drawable { */ private void drawTopBottomSplit(Canvas canvas) { // Get the bounds where we will draw the background image int width = getBounds().width(); int height = getBounds().height(); int width = mP.getIconSize(); int height = mP.getIconSize(); // The top half of the background image, excluding center channel RectF topSide = new RectF( 0, 0, width, (height / 2f) - (icon.getCenterChannelSize() / 2f) (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( 0, (height / 2f) + (icon.getCenterChannelSize() / 2f), width, height (mP.getStandardIconPadding() + mP.getOuterPadding()), (height / 2f) + (mP.getCenterChannelSize() / 2f), width - (mP.getStandardIconPadding() + mP.getOuterPadding()), height - (mP.getStandardIconPadding() + mP.getOuterPadding()) ); drawCustomRoundedRect(canvas, topSide, new float[]{ icon.getBigRadius(), icon.getBigRadius(), icon.getBigRadius(), icon.getBigRadius(), icon.getSmallRadius(), icon.getSmallRadius(), icon.getSmallRadius(), icon.getSmallRadius()}); mP.getBigRadius(), mP.getBigRadius(), mP.getBigRadius(), mP.getBigRadius(), mP.getSmallRadius(), mP.getSmallRadius(), mP.getSmallRadius(), mP.getSmallRadius()}); drawCustomRoundedRect(canvas, bottomSide, new float[]{ icon.getSmallRadius(), icon.getSmallRadius(), icon.getSmallRadius(), icon.getSmallRadius(), icon.getBigRadius(), icon.getBigRadius(), icon.getBigRadius(), icon.getBigRadius()}); mP.getSmallRadius(), mP.getSmallRadius(), mP.getSmallRadius(), mP.getSmallRadius(), mP.getBigRadius(), mP.getBigRadius(), mP.getBigRadius(), mP.getBigRadius()}); } /** Loading @@ -146,7 +187,7 @@ class AppPairIconBackground extends Drawable { 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); c.drawRoundRect(rect, mP.getBigRadius(), mP.getBigRadius(), mBackgroundPaint); } } Loading src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt 0 → 100644 +98 −0 Original line number Diff line number Diff line /* * 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 } } Loading
quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +2 −1 Original line number Diff line number Diff line Loading @@ -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: Loading
src/com/android/launcher3/BubbleTextView.java +3 −4 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading
src/com/android/launcher3/apppairs/AppPairIcon.java +45 −19 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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); Loading @@ -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()) Loading @@ -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 Loading Loading @@ -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; } Loading @@ -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(); } /** Loading @@ -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); } }
src/com/android/launcher3/apppairs/AppPairIconBackground.java→src/com/android/launcher3/apppairs/AppPairIconDrawable.java +208 −0 Original line number Diff line number Diff line Loading @@ -16,8 +16,6 @@ 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; Loading @@ -26,15 +24,19 @@ import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.os.Build; import com.android.launcher3.R; import androidx.annotation.NonNull; import com.android.launcher3.icons.FastBitmapDrawable; /** * A Drawable for the background behind the twin app icons (looks like two rectangles). * A composed Drawable consisting of the two app pair icons and the background behind them (looks * like two rectangles). */ class AppPairIconBackground extends Drawable { // The underlying view that we are drawing this background on. private final AppPairIconGraphic icon; 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 Loading @@ -44,23 +46,62 @@ class AppPairIconBackground extends Drawable { 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); AppPairIconDrawable( AppPairIconDrawingParams p, FastBitmapDrawable icon1, FastBitmapDrawable icon2) { mP = p; mBackgroundPaint.setStyle(Paint.Style.FILL); mBackgroundPaint.setColor( ta.getColor(R.styleable.FolderIconPreview_folderPreviewColor, 0)); ta.recycle(); mBackgroundPaint.setColor(p.getBgColor()); mIcon1 = icon1; mIcon2 = icon2; } @Override public void draw(Canvas canvas) { if (icon.isLeftRightSplit()) { 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); } /** Loading @@ -68,34 +109,34 @@ class AppPairIconBackground extends Drawable { */ private void drawLeftRightSplit(Canvas canvas) { // Get the bounds where we will draw the background image int width = getBounds().width(); int height = getBounds().height(); int width = mP.getIconSize(); int height = mP.getIconSize(); // The left half of the background image, excluding center channel RectF leftSide = new RectF( 0, 0, (width / 2f) - (icon.getCenterChannelSize() / 2f), height 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) + (icon.getCenterChannelSize() / 2f), 0, width, height (width / 2f) + (mP.getCenterChannelSize() / 2f), (mP.getStandardIconPadding() + mP.getOuterPadding()), width - (mP.getStandardIconPadding() + mP.getOuterPadding()), height - (mP.getStandardIconPadding() + mP.getOuterPadding()) ); drawCustomRoundedRect(canvas, leftSide, new float[]{ icon.getBigRadius(), icon.getBigRadius(), icon.getSmallRadius(), icon.getSmallRadius(), icon.getSmallRadius(), icon.getSmallRadius(), icon.getBigRadius(), icon.getBigRadius()}); mP.getBigRadius(), mP.getBigRadius(), mP.getSmallRadius(), mP.getSmallRadius(), mP.getSmallRadius(), mP.getSmallRadius(), mP.getBigRadius(), mP.getBigRadius()}); drawCustomRoundedRect(canvas, rightSide, new float[]{ icon.getSmallRadius(), icon.getSmallRadius(), icon.getBigRadius(), icon.getBigRadius(), icon.getBigRadius(), icon.getBigRadius(), icon.getSmallRadius(), icon.getSmallRadius()}); mP.getSmallRadius(), mP.getSmallRadius(), mP.getBigRadius(), mP.getBigRadius(), mP.getBigRadius(), mP.getBigRadius(), mP.getSmallRadius(), mP.getSmallRadius()}); } /** Loading @@ -103,34 +144,34 @@ class AppPairIconBackground extends Drawable { */ private void drawTopBottomSplit(Canvas canvas) { // Get the bounds where we will draw the background image int width = getBounds().width(); int height = getBounds().height(); int width = mP.getIconSize(); int height = mP.getIconSize(); // The top half of the background image, excluding center channel RectF topSide = new RectF( 0, 0, width, (height / 2f) - (icon.getCenterChannelSize() / 2f) (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( 0, (height / 2f) + (icon.getCenterChannelSize() / 2f), width, height (mP.getStandardIconPadding() + mP.getOuterPadding()), (height / 2f) + (mP.getCenterChannelSize() / 2f), width - (mP.getStandardIconPadding() + mP.getOuterPadding()), height - (mP.getStandardIconPadding() + mP.getOuterPadding()) ); drawCustomRoundedRect(canvas, topSide, new float[]{ icon.getBigRadius(), icon.getBigRadius(), icon.getBigRadius(), icon.getBigRadius(), icon.getSmallRadius(), icon.getSmallRadius(), icon.getSmallRadius(), icon.getSmallRadius()}); mP.getBigRadius(), mP.getBigRadius(), mP.getBigRadius(), mP.getBigRadius(), mP.getSmallRadius(), mP.getSmallRadius(), mP.getSmallRadius(), mP.getSmallRadius()}); drawCustomRoundedRect(canvas, bottomSide, new float[]{ icon.getSmallRadius(), icon.getSmallRadius(), icon.getSmallRadius(), icon.getSmallRadius(), icon.getBigRadius(), icon.getBigRadius(), icon.getBigRadius(), icon.getBigRadius()}); mP.getSmallRadius(), mP.getSmallRadius(), mP.getSmallRadius(), mP.getSmallRadius(), mP.getBigRadius(), mP.getBigRadius(), mP.getBigRadius(), mP.getBigRadius()}); } /** Loading @@ -146,7 +187,7 @@ class AppPairIconBackground extends Drawable { 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); c.drawRoundRect(rect, mP.getBigRadius(), mP.getBigRadius(), mBackgroundPaint); } } Loading
src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt 0 → 100644 +98 −0 Original line number Diff line number Diff line /* * 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 } }