App Pairs: Implement app pairs icon

[App Pairs 5/?]

This patch implements the app pairs icon, which displays the two member apps and rotates with the device.

Flag: ENABLE_APP_PAIRS (set to false)
Bug: 274835596
Test: Manual
Change-Id: I07085339d1e2d28f004c1661f0948c59e605c76a
This commit is contained in:
Jeremy Sim
2023-09-01 21:18:31 -07:00
parent 9acb884c90
commit 9d6dbd0a8d
3 changed files with 303 additions and 11 deletions

View File

@@ -250,6 +250,10 @@
<!-- Folder name format when folder has 4 or more items shown in preview-->
<string name="folder_name_format_overflow">Folder: <xliff:g id="name" example="Games">%1$s</xliff:g>, <xliff:g id="size" example="2">%2$d</xliff:g> or more items</string>
<!-- App pair accessibility -->
<!-- App pair name -->
<string name="app_pair_name_format">App pair: <xliff:g id="app1" example="Chrome">%1$s</xliff:g> and <xliff:g id="app2" example="YouTube">%2$s</xliff:g></string>
<!-- Strings for the customization mode -->
<!-- Text for wallpaper change button [CHAR LIMIT=30]-->
<string name="styles_wallpaper_button_text">Wallpaper &amp; style</string>

View File

@@ -17,7 +17,9 @@
package com.android.launcher3.apppairs;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.ViewGroup;
@@ -26,6 +28,7 @@ import android.widget.FrameLayout;
import androidx.annotation.Nullable;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.dragndrop.DraggableView;
import com.android.launcher3.model.data.FolderInfo;
@@ -37,11 +40,41 @@ import java.util.Comparator;
/**
* A {@link android.widget.FrameLayout} used to represent an app pair icon on the workspace.
* <br>
* The app pair icon is two parallel background rectangles with rounded corners. Icons of the two
* member apps are set into these rectangles.
*/
public class AppPairIcon extends FrameLayout implements DraggableView {
/**
* Design specs -- the below ratios are in relation to the size of a standard app icon.
*/
private static final float OUTER_PADDING_SCALE = 1 / 30f;
private static final float INNER_PADDING_SCALE = 1 / 24f;
private static final float MEMBER_ICON_SCALE = 11 / 30f;
private static final float CENTER_CHANNEL_SCALE = 1 / 30f;
private static final float BIG_RADIUS_SCALE = 1 / 5f;
private static final float SMALL_RADIUS_SCALE = 1 / 15f;
// App pair icons are slightly smaller than regular icons, so we pad the icon by this much on
// each side.
float mOuterPadding;
// Inside of the icon, the two member apps are padded by this much.
float mInnerPadding;
// The two member apps have icons that are this big (in diameter).
float mMemberIconSize;
// The size of the center channel.
float mCenterChannelSize;
// The large outer radius of the background rectangles.
float mBigRadius;
// The small inner radius of the background rectangles.
float mSmallRadius;
// The app pairs icon appears differently in portrait and landscape.
boolean mIsLandscape;
private ActivityContext mActivity;
// A view that holds the app pair's title.
private BubbleTextView mAppPairName;
// The underlying ItemInfo that stores info about the app pair members, etc.
private FolderInfo mInfo;
public AppPairIcon(Context context, AttributeSet attrs) {
@@ -53,11 +86,11 @@ public class AppPairIcon extends FrameLayout implements DraggableView {
}
/**
* Builds an AppPairIcon to be added to the Launcher
* Builds an AppPairIcon to be added to the Launcher.
*/
public static AppPairIcon inflateIcon(int resId, ActivityContext activity,
@Nullable ViewGroup group, FolderInfo appPairInfo) {
DeviceProfile grid = activity.getDeviceProfile();
LayoutInflater inflater = (group != null)
? LayoutInflater.from(group.getContext())
: activity.getLayoutInflater();
@@ -67,25 +100,113 @@ public class AppPairIcon extends FrameLayout implements DraggableView {
Collections.sort(appPairInfo.contents, Comparator.comparingInt(a -> a.rank));
icon.setClipToPadding(false);
icon.mAppPairName = icon.findViewById(R.id.app_pair_icon_name);
// TODO (jeremysim b/274189428): Replace this placeholder icon
WorkspaceItemInfo placeholder = new WorkspaceItemInfo();
placeholder.newIcon(icon.getContext());
icon.mAppPairName.applyFromWorkspaceItem(placeholder);
icon.mAppPairName.setText(appPairInfo.title);
icon.setTag(appPairInfo);
icon.setOnClickListener(activity.getItemOnClickListener());
icon.mInfo = appPairInfo;
icon.mActivity = activity;
// 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;
icon.mAppPairName.setText(appPairInfo.title);
// Set up accessibility
icon.setContentDescription(icon.getAccessibilityTitle(
appPairInfo.contents.get(0).title, appPairInfo.contents.get(1).title));
icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
return icon;
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
// Calculate device-specific measurements
DeviceProfile grid = mActivity.getDeviceProfile();
int defaultIconSize = grid.iconSizePx;
mOuterPadding = OUTER_PADDING_SCALE * defaultIconSize;
mInnerPadding = INNER_PADDING_SCALE * defaultIconSize;
mMemberIconSize = MEMBER_ICON_SCALE * defaultIconSize;
mCenterChannelSize = CENTER_CHANNEL_SCALE * defaultIconSize;
mBigRadius = BIG_RADIUS_SCALE * defaultIconSize;
mSmallRadius = SMALL_RADIUS_SCALE * defaultIconSize;
mIsLandscape = grid.isLandscape;
// Calculate drawable area position
float leftBound = (canvas.getWidth() / 2f) - (defaultIconSize / 2f);
float topBound = getPaddingTop();
// Prepare to draw app pair icon background
Drawable background = new AppPairIconBackground(getContext(), this);
background.setBounds(0, 0, defaultIconSize, defaultIconSize);
// Draw background
canvas.save();
canvas.translate(leftBound, topBound);
background.draw(canvas);
canvas.restore();
// Prepare to draw icons
WorkspaceItemInfo app1 = mInfo.contents.get(0);
WorkspaceItemInfo app2 = mInfo.contents.get(1);
Drawable app1Icon = app1.newIcon(getContext());
Drawable app2Icon = app2.newIcon(getContext());
app1Icon.setBounds(0, 0, defaultIconSize, defaultIconSize);
app2Icon.setBounds(0, 0, defaultIconSize, defaultIconSize);
// Draw first icon
canvas.save();
canvas.translate(leftBound, topBound);
// The app icons are placed differently depending on device orientation.
if (mIsLandscape) {
canvas.translate(
(defaultIconSize / 2f) - (mCenterChannelSize / 2f) - mInnerPadding
- mMemberIconSize,
(defaultIconSize / 2f) - (mMemberIconSize / 2f)
);
} else {
canvas.translate(
(defaultIconSize / 2f) - (mMemberIconSize / 2f),
(defaultIconSize / 2f) - (mCenterChannelSize / 2f) - mInnerPadding
- mMemberIconSize
);
}
canvas.scale(MEMBER_ICON_SCALE, MEMBER_ICON_SCALE);
app1Icon.draw(canvas);
canvas.restore();
// Draw second icon
canvas.save();
canvas.translate(leftBound, topBound);
// The app icons are placed differently depending on device orientation.
if (mIsLandscape) {
canvas.translate(
(defaultIconSize / 2f) + (mCenterChannelSize / 2f) + mInnerPadding,
(defaultIconSize / 2f) - (mMemberIconSize / 2f)
);
} else {
canvas.translate(
(defaultIconSize / 2f) - (mMemberIconSize / 2f),
(defaultIconSize / 2f) + (mCenterChannelSize / 2f) + mInnerPadding
);
}
canvas.scale(MEMBER_ICON_SCALE, MEMBER_ICON_SCALE);
app2Icon.draw(canvas);
canvas.restore();
}
/**
* Returns a formatted accessibility title for app pairs.
*/
public String getAccessibilityTitle(CharSequence app1, CharSequence app2) {
return getContext().getString(R.string.app_pair_name_format, app1, app2);
}
@Override
public int getViewType() {
return DRAGGABLE_ICON;

View File

@@ -0,0 +1,167 @@
/*
* 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 icon that we will draw this background on.
private final AppPairIcon 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, AppPairIcon appPairIcon) {
icon = appPairIcon;
// 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.mIsLandscape) {
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(
icon.mOuterPadding,
icon.mOuterPadding,
(width / 2f) - (icon.mCenterChannelSize / 2f),
height - icon.mOuterPadding
);
// The right half of the background image, excluding center channel
RectF rightSide = new RectF(
(width / 2f) + (icon.mCenterChannelSize / 2f),
icon.mOuterPadding,
width - icon.mOuterPadding,
height - icon.mOuterPadding
);
drawCustomRoundedRect(canvas, leftSide, new float[]{
icon.mBigRadius, icon.mBigRadius,
icon.mSmallRadius, icon.mSmallRadius,
icon.mSmallRadius, icon.mSmallRadius,
icon.mBigRadius, icon.mBigRadius});
drawCustomRoundedRect(canvas, rightSide, new float[]{
icon.mSmallRadius, icon.mSmallRadius,
icon.mBigRadius, icon.mBigRadius,
icon.mBigRadius, icon.mBigRadius,
icon.mSmallRadius, icon.mSmallRadius});
}
/**
* 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(
icon.mOuterPadding,
icon.mOuterPadding,
width - icon.mOuterPadding,
(height / 2f) - (icon.mCenterChannelSize / 2f)
);
// The bottom half of the background image, excluding center channel
RectF bottomSide = new RectF(
icon.mOuterPadding,
(height / 2f) + (icon.mCenterChannelSize / 2f),
width - icon.mOuterPadding,
height - icon.mOuterPadding
);
drawCustomRoundedRect(canvas, topSide, new float[]{
icon.mBigRadius, icon.mBigRadius,
icon.mBigRadius, icon.mBigRadius,
icon.mSmallRadius, icon.mSmallRadius,
icon.mSmallRadius, icon.mSmallRadius});
drawCustomRoundedRect(canvas, bottomSide, new float[]{
icon.mSmallRadius, icon.mSmallRadius,
icon.mSmallRadius, icon.mSmallRadius,
icon.mBigRadius, icon.mBigRadius,
icon.mBigRadius, icon.mBigRadius});
}
/**
* 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.mBigRadius, icon.mBigRadius, mBackgroundPaint);
}
}
@Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
@Override
public void setAlpha(int i) {
// Required by Drawable but not used.
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
// Required by Drawable but not used.
}
}