mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-27 23:36:47 +00:00
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:
@@ -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 & style</string>
|
||||
|
||||
@@ -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;
|
||||
|
||||
167
src/com/android/launcher3/apppairs/AppPairIconBackground.java
Normal file
167
src/com/android/launcher3/apppairs/AppPairIconBackground.java
Normal 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.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user