1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3.apppairs; 18 19 import static com.android.launcher3.BubbleTextView.DISPLAY_FOLDER; 20 21 import android.content.Context; 22 import android.graphics.Paint; 23 import android.graphics.Rect; 24 import android.util.AttributeSet; 25 import android.view.LayoutInflater; 26 import android.view.ViewGroup; 27 import android.widget.FrameLayout; 28 29 import androidx.annotation.Nullable; 30 31 import com.android.launcher3.BubbleTextView; 32 import com.android.launcher3.DeviceProfile; 33 import com.android.launcher3.Flags; 34 import com.android.launcher3.Launcher; 35 import com.android.launcher3.LauncherAppState; 36 import com.android.launcher3.R; 37 import com.android.launcher3.Reorderable; 38 import com.android.launcher3.dragndrop.DraggableView; 39 import com.android.launcher3.icons.IconCache; 40 import com.android.launcher3.model.data.AppPairInfo; 41 import com.android.launcher3.model.data.ItemInfo; 42 import com.android.launcher3.util.MultiTranslateDelegate; 43 import com.android.launcher3.views.ActivityContext; 44 45 import java.util.Comparator; 46 import java.util.function.Predicate; 47 48 /** 49 * A {@link android.widget.FrameLayout} used to represent an app pair icon on the workspace. 50 * <br> 51 * The app pair icon is two parallel background rectangles with rounded corners. Icons of the two 52 * member apps are set into these rectangles. 53 */ 54 public class AppPairIcon extends FrameLayout implements DraggableView, Reorderable { 55 private static final String TAG = "AppPairIcon"; 56 57 // A view that holds the app pair icon graphic. 58 private AppPairIconGraphic mIconGraphic; 59 // A view that holds the app pair's title. 60 private BubbleTextView mAppPairName; 61 // The underlying ItemInfo that stores info about the app pair members, etc. 62 private AppPairInfo mInfo; 63 // The containing element that holds this icon: workspace, taskbar, folder, etc. Affects certain 64 // aspects of how the icon is drawn. 65 private int mContainer; 66 67 // Required for Reorderable -- handles translation and bouncing movements 68 private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this); 69 private float mScaleForReorderBounce = 1f; 70 AppPairIcon(Context context, AttributeSet attrs)71 public AppPairIcon(Context context, AttributeSet attrs) { 72 super(context, attrs); 73 } 74 AppPairIcon(Context context)75 public AppPairIcon(Context context) { 76 super(context); 77 } 78 79 /** 80 * Builds an AppPairIcon to be added to the Launcher. 81 */ inflateIcon(int resId, ActivityContext activity, @Nullable ViewGroup group, AppPairInfo appPairInfo, int container)82 public static AppPairIcon inflateIcon(int resId, ActivityContext activity, 83 @Nullable ViewGroup group, AppPairInfo appPairInfo, int container) { 84 DeviceProfile grid = activity.getDeviceProfile(); 85 LayoutInflater inflater = (group != null) 86 ? LayoutInflater.from(group.getContext()) 87 : activity.getLayoutInflater(); 88 AppPairIcon icon = (AppPairIcon) inflater.inflate(resId, group, false); 89 90 if (Flags.enableFocusOutline() && activity instanceof Launcher) { 91 icon.setOnFocusChangeListener(((Launcher) activity).getFocusHandler()); 92 icon.setDefaultFocusHighlightEnabled(false); 93 } 94 95 // Sort contents, so that left-hand app comes first 96 appPairInfo.getContents().sort(Comparator.comparingInt(a -> a.rank)); 97 98 icon.setTag(appPairInfo); 99 icon.setOnClickListener(activity.getItemOnClickListener()); 100 icon.mInfo = appPairInfo; 101 icon.mContainer = container; 102 103 // Set up icon drawable area 104 icon.mIconGraphic = icon.findViewById(R.id.app_pair_icon_graphic); 105 icon.mIconGraphic.init(icon, container); 106 107 // Set up app pair title 108 icon.mAppPairName = icon.findViewById(R.id.app_pair_icon_name); 109 FrameLayout.LayoutParams lp = 110 (FrameLayout.LayoutParams) icon.mAppPairName.getLayoutParams(); 111 // Shift the title text down to leave room for the icon graphic. Since the icon graphic is 112 // a separate element (and not set as a CompoundDrawable on the BubbleTextView), we need to 113 // shift the text down manually. 114 lp.topMargin = container == DISPLAY_FOLDER 115 ? grid.folderChildIconSizePx + grid.folderChildDrawablePaddingPx 116 : grid.iconSizePx + grid.iconDrawablePaddingPx; 117 // For some reason, app icons have setIncludeFontPadding(false) inside folders, so we set it 118 // here to match that. 119 icon.mAppPairName.setIncludeFontPadding(container != DISPLAY_FOLDER); 120 // Set title text and accessibility title text. 121 icon.updateTitleAndA11yTitle(); 122 123 icon.setAccessibilityDelegate(activity.getAccessibilityDelegate()); 124 125 return icon; 126 } 127 128 /** 129 * Updates the title and a11y title of the app pair. Called on creation and when packages 130 * change, to reflect app name changes or user language changes. 131 */ updateTitleAndA11yTitle()132 public void updateTitleAndA11yTitle() { 133 updateTitleAndTextView(); 134 updateAccessibilityTitle(); 135 } 136 137 /** 138 * Updates AppPairInfo with a formatted app pair title, and sets it on the BubbleTextView. 139 */ updateTitleAndTextView()140 public void updateTitleAndTextView() { 141 CharSequence newTitle = getInfo().generateTitle(getContext()); 142 mAppPairName.setText(newTitle); 143 } 144 145 /** 146 * Updates the accessibility title with a formatted string template. 147 */ updateAccessibilityTitle()148 public void updateAccessibilityTitle() { 149 CharSequence app1 = getInfo().getFirstApp().title; 150 CharSequence app2 = getInfo().getSecondApp().title; 151 String a11yTitle = getContext().getString(R.string.app_pair_name_format, app1, app2); 152 setContentDescription( 153 getInfo().shouldReportDisabled(getContext()) 154 ? getContext().getString(R.string.disabled_app_label, a11yTitle) 155 : a11yTitle); 156 } 157 158 // Required for DraggableView 159 @Override getViewType()160 public int getViewType() { 161 return DRAGGABLE_ICON; 162 } 163 164 // Required for DraggableView 165 @Override getWorkspaceVisualDragBounds(Rect outBounds)166 public void getWorkspaceVisualDragBounds(Rect outBounds) { 167 mIconGraphic.getIconBounds(outBounds); 168 } 169 170 /** Sets the visibility of the icon's title text */ setTextVisible(boolean visible)171 public void setTextVisible(boolean visible) { 172 if (visible) { 173 mAppPairName.setVisibility(VISIBLE); 174 } else { 175 mAppPairName.setVisibility(INVISIBLE); 176 } 177 } 178 179 // Required for Reorderable 180 @Override getTranslateDelegate()181 public MultiTranslateDelegate getTranslateDelegate() { 182 return mTranslateDelegate; 183 } 184 185 // Required for Reorderable 186 @Override setReorderBounceScale(float scale)187 public void setReorderBounceScale(float scale) { 188 mScaleForReorderBounce = scale; 189 super.setScaleX(scale); 190 super.setScaleY(scale); 191 } 192 193 // Required for Reorderable 194 @Override getReorderBounceScale()195 public float getReorderBounceScale() { 196 return mScaleForReorderBounce; 197 } 198 getInfo()199 public AppPairInfo getInfo() { 200 return mInfo; 201 } 202 getTitleTextView()203 public BubbleTextView getTitleTextView() { 204 return mAppPairName; 205 } 206 getIconDrawableArea()207 public AppPairIconGraphic getIconDrawableArea() { 208 return mIconGraphic; 209 } 210 getContainer()211 public int getContainer() { 212 return mContainer; 213 } 214 215 /** 216 * Ensures that both app icons in the pair are loaded in high resolution. 217 */ verifyHighRes()218 public void verifyHighRes() { 219 IconCache iconCache = LauncherAppState.getInstance(getContext()).getIconCache(); 220 getInfo().fetchHiResIconsIfNeeded(iconCache); 221 } 222 223 /** 224 * Called when WorkspaceItemInfos get updated, and the app pair icon may need to be redrawn. 225 */ maybeRedrawForWorkspaceUpdate(Predicate<ItemInfo> itemCheck)226 public void maybeRedrawForWorkspaceUpdate(Predicate<ItemInfo> itemCheck) { 227 // If either of the app pair icons return true on the predicate (i.e. in the list of 228 // updated apps), redraw the icon graphic (icon background and both icons). 229 if (getInfo().anyMatch(itemCheck)) { 230 updateTitleAndA11yTitle(); 231 mIconGraphic.redraw(); 232 } 233 } 234 235 /** 236 * Inside folders, icons are vertically centered in their rows. See 237 * {@link BubbleTextView} for comparison. 238 */ 239 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)240 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 241 if (mContainer == DISPLAY_FOLDER) { 242 int height = MeasureSpec.getSize(heightMeasureSpec); 243 ActivityContext activity = ActivityContext.lookupContext(getContext()); 244 Paint.FontMetrics fm = mAppPairName.getPaint().getFontMetrics(); 245 int cellHeightPx = activity.getDeviceProfile().folderChildIconSizePx 246 + activity.getDeviceProfile().folderChildDrawablePaddingPx 247 + (int) Math.ceil(fm.bottom - fm.top); 248 setPadding(getPaddingLeft(), (height - cellHeightPx) / 2, getPaddingRight(), 249 getPaddingBottom()); 250 } 251 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 252 } 253 } 254