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

Commit 46ca78ee authored by Jeremy Sim's avatar Jeremy Sim Committed by Android (Google) Code Review
Browse files

Merge "Refactor how app pair icons draw" into main

parents a8efc841 b37faec2
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;
+45 −19
Original line number Diff line number Diff line
@@ -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);
    }
}
+208 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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
@@ -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);
    }

    /**
@@ -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()});
    }

    /**
@@ -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()});
    }

    /**
@@ -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);
        }
    }

+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