/* * 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 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.view.LayoutInflater; import android.view.ViewGroup; import android.widget.FrameLayout; import androidx.annotation.Nullable; import com.android.launcher3.BubbleTextView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Flags; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.Reorderable; import com.android.launcher3.dragndrop.DraggableView; import com.android.launcher3.icons.IconCache; import com.android.launcher3.model.data.AppPairInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.util.MultiTranslateDelegate; import com.android.launcher3.views.ActivityContext; import java.util.Comparator; import java.util.function.Predicate; /** * A {@link android.widget.FrameLayout} used to represent an app pair icon on the workspace. *
* 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, Reorderable { private static final String TAG = "AppPairIcon"; // A view that holds the app pair icon graphic. private AppPairIconGraphic mIconGraphic; // 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 AppPairInfo 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); private float mScaleForReorderBounce = 1f; public AppPairIcon(Context context, AttributeSet attrs) { super(context, attrs); } public AppPairIcon(Context context) { super(context); } /** * Builds an AppPairIcon to be added to the Launcher. */ public static AppPairIcon inflateIcon(int resId, ActivityContext activity, @Nullable ViewGroup group, AppPairInfo appPairInfo, int container) { DeviceProfile grid = activity.getDeviceProfile(); LayoutInflater inflater = (group != null) ? LayoutInflater.from(group.getContext()) : activity.getLayoutInflater(); AppPairIcon icon = (AppPairIcon) inflater.inflate(resId, group, false); if (Flags.enableFocusOutline() && activity instanceof Launcher) { icon.setOnFocusChangeListener(((Launcher) activity).getFocusHandler()); icon.setDefaultFocusHighlightEnabled(false); } // Sort contents, so that left-hand app comes first appPairInfo.getContents().sort(Comparator.comparingInt(a -> a.rank)); icon.setTag(appPairInfo); icon.setOnClickListener(activity.getItemOnClickListener()); icon.mInfo = appPairInfo; icon.mContainer = container; // Set up icon drawable area icon.mIconGraphic = icon.findViewById(R.id.app_pair_icon_graphic); icon.mIconGraphic.init(icon, container); // Set up app pair title icon.mAppPairName = icon.findViewById(R.id.app_pair_icon_name); FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mAppPairName.getLayoutParams(); // 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); // Set title text and accessibility title text. icon.updateTitleAndA11yTitle(); icon.setAccessibilityDelegate(activity.getAccessibilityDelegate()); return icon; } /** * Updates the title and a11y title of the app pair. Called on creation and when packages * change, to reflect app name changes or user language changes. */ public void updateTitleAndA11yTitle() { updateTitleAndTextView(); updateAccessibilityTitle(); } /** * Updates AppPairInfo with a formatted app pair title, and sets it on the BubbleTextView. */ public void updateTitleAndTextView() { CharSequence newTitle = getInfo().generateTitle(getContext()); mAppPairName.setText(newTitle); } /** * Updates the accessibility title with a formatted string template. */ public void updateAccessibilityTitle() { CharSequence app1 = getInfo().getFirstApp().title; CharSequence app2 = getInfo().getSecondApp().title; String a11yTitle = getContext().getString(R.string.app_pair_name_format, app1, app2); setContentDescription( getInfo().shouldReportDisabled(getContext()) ? getContext().getString(R.string.disabled_app_label, a11yTitle) : a11yTitle); } // Required for DraggableView @Override public int getViewType() { return DRAGGABLE_ICON; } // Required for DraggableView @Override public void getWorkspaceVisualDragBounds(Rect outBounds) { mIconGraphic.getIconBounds(outBounds); } /** Sets the visibility of the icon's title text */ public void setTextVisible(boolean visible) { if (visible) { mAppPairName.setVisibility(VISIBLE); } else { mAppPairName.setVisibility(INVISIBLE); } } // Required for Reorderable @Override public MultiTranslateDelegate getTranslateDelegate() { return mTranslateDelegate; } // Required for Reorderable @Override public void setReorderBounceScale(float scale) { mScaleForReorderBounce = scale; super.setScaleX(scale); super.setScaleY(scale); } // Required for Reorderable @Override public float getReorderBounceScale() { return mScaleForReorderBounce; } public AppPairInfo getInfo() { return mInfo; } public BubbleTextView getTitleTextView() { return mAppPairName; } public AppPairIconGraphic getIconDrawableArea() { return mIconGraphic; } public int getContainer() { return mContainer; } /** * Ensures that both app icons in the pair are loaded in high resolution. */ public void verifyHighRes() { IconCache iconCache = LauncherAppState.getInstance(getContext()).getIconCache(); getInfo().fetchHiResIconsIfNeeded(iconCache); } /** * Called when WorkspaceItemInfos get updated, and the app pair icon may need to be redrawn. */ public void maybeRedrawForWorkspaceUpdate(Predicate itemCheck) { // 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().anyMatch(itemCheck)) { updateTitleAndA11yTitle(); mIconGraphic.redraw(); } } /** * Inside folders, icons are vertically centered in their rows. See * {@link BubbleTextView} 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); } }