Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fcbef122 authored by Jeremy Sim's avatar Jeremy Sim
Browse files

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
(cherry picked from commit b37faec2)
Merged-In: I7242e0c525ef578a54a06fb9137fcfc42c6f0e86
parent d6b0dc89
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -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:
+3 −4
Original line number Diff line number Diff line
@@ -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;
+48 −19
Original line number Diff line number Diff line
@@ -16,13 +16,14 @@

package com.android.launcher3.apppairs;

import static com.android.launcher3.BubbleTextView.DISPLAY_FOLDER;

import android.content.Context;
import android.graphics.Canvas;
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;

@@ -38,7 +39,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;

@@ -62,6 +62,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);
@@ -79,7 +82,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())
@@ -87,30 +90,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;
        icon.mContainer = container;

        if (icon.mInfo.contents.size() != 2) {
            Log.wtf(TAG, "AppPair contents not 2, size: " + icon.mInfo.contents.size());
            return icon;
        }

        icon.checkScreenSize();
        icon.checkDisabledState();

        // 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);

        // 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 +179,11 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab
        return mInfo;
    }

    public View getIconDrawableArea() {
    public BubbleTextView getTitleTextView() {
        return mAppPairName;
    }

    public AppPairIconGraphic getIconDrawableArea() {
        return mIconGraphic;
    }

@@ -194,12 +203,14 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab
     * {@link AppPairIconGraphic#dispatchDraw(Canvas)} or clicked on
     * {@link com.android.launcher3.touch.ItemClickHandler#onClickAppPairIcon(View)}
     */
    public void checkScreenSize() {
    public void checkDisabledState() {
        DeviceProfile dp = ActivityContext.lookupContext(getContext()).getDeviceProfile();
        // If user is on a small screen, we can't launch if either of the apps is non-resizeable
        mIsLaunchableAtScreenSize =
                dp.isTablet || getInfo().contents.stream().noneMatch(
                        wii -> wii.hasStatusFlag(WorkspaceItemInfo.FLAG_NON_RESIZEABLE));
        // Invalidate to update icons
        mIconGraphic.redraw();
    }

    /**
@@ -209,8 +220,26 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab
        // If either of the app pair icons return true on the predicate (i.e. in the list of
        // updated apps), redraw the icon graphic (icon background and both icons).
        if (getInfo().contents.stream().anyMatch(itemCheck)) {
            checkScreenSize();
            mIconGraphic.invalidate();
            checkDisabledState();
        }
    }

    /**
     * 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);
    }
}
+0 −166
Original line number Diff line number Diff line
/*
 * 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) {
        // Required by Drawable but not used.
    }
}
+208 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
Loading