1 /* 2 * Copyright (C) 2012 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.appprediction; 18 19 import android.content.Context; 20 import android.graphics.Canvas; 21 import android.util.AttributeSet; 22 import android.view.LayoutInflater; 23 import android.view.View; 24 import android.view.ViewGroup; 25 import android.widget.LinearLayout; 26 27 import androidx.annotation.NonNull; 28 import androidx.annotation.Nullable; 29 30 import com.android.launcher3.BubbleTextView; 31 import com.android.launcher3.DeviceProfile; 32 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; 33 import com.android.launcher3.Flags; 34 import com.android.launcher3.LauncherPrefs; 35 import com.android.launcher3.R; 36 import com.android.launcher3.Utilities; 37 import com.android.launcher3.allapps.FloatingHeaderRow; 38 import com.android.launcher3.allapps.FloatingHeaderView; 39 import com.android.launcher3.anim.AlphaUpdateListener; 40 import com.android.launcher3.keyboard.FocusIndicatorHelper; 41 import com.android.launcher3.keyboard.FocusIndicatorHelper.SimpleFocusIndicatorHelper; 42 import com.android.launcher3.model.data.ItemInfo; 43 import com.android.launcher3.model.data.ItemInfoWithIcon; 44 import com.android.launcher3.model.data.WorkspaceItemInfo; 45 import com.android.launcher3.views.ActivityContext; 46 47 import java.io.PrintWriter; 48 import java.util.ArrayList; 49 import java.util.List; 50 import java.util.stream.Collectors; 51 52 public class PredictionRowView<T extends Context & ActivityContext> 53 extends LinearLayout implements OnDeviceProfileChangeListener, FloatingHeaderRow { 54 55 private final T mActivityContext; 56 private int mNumPredictedAppsPerRow; 57 // Vertical padding of the icon that contributes to the expected cell height. 58 private final int mVerticalPadding; 59 // Extra padding that is used in the top app rows (prediction and search) that is not used in 60 // the regular A-Z list. This only applies to single line label. 61 private final int mTopRowExtraHeight; 62 63 // Helper to drawing the focus indicator. 64 private final FocusIndicatorHelper mFocusHelper; 65 66 // The set of predicted apps resolved from the component names and the current set of apps 67 private final List<WorkspaceItemInfo> mPredictedApps = new ArrayList<>(); 68 69 private FloatingHeaderView mParent; 70 71 private boolean mPredictionsEnabled = false; 72 73 private boolean mPredictionUiUpdatePaused = false; 74 PredictionRowView(@onNull Context context)75 public PredictionRowView(@NonNull Context context) { 76 this(context, null); 77 } 78 PredictionRowView(@onNull Context context, @Nullable AttributeSet attrs)79 public PredictionRowView(@NonNull Context context, @Nullable AttributeSet attrs) { 80 super(context, attrs); 81 setOrientation(LinearLayout.HORIZONTAL); 82 83 mFocusHelper = new SimpleFocusIndicatorHelper(this); 84 mActivityContext = ActivityContext.lookupContext(context); 85 mNumPredictedAppsPerRow = mActivityContext.getDeviceProfile().numShownAllAppsColumns; 86 mTopRowExtraHeight = getResources().getDimensionPixelSize( 87 R.dimen.all_apps_search_top_row_extra_height); 88 mVerticalPadding = getResources().getDimensionPixelSize( 89 R.dimen.all_apps_predicted_icon_vertical_padding); 90 updateVisibility(); 91 } 92 93 @Override onAttachedToWindow()94 protected void onAttachedToWindow() { 95 super.onAttachedToWindow(); 96 mActivityContext.addOnDeviceProfileChangeListener(this); 97 } 98 99 @Override onDetachedFromWindow()100 protected void onDetachedFromWindow() { 101 super.onDetachedFromWindow(); 102 mActivityContext.removeOnDeviceProfileChangeListener(this); 103 } 104 setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden)105 public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) { 106 mParent = parent; 107 } 108 updateVisibility()109 private void updateVisibility() { 110 setVisibility(mPredictionsEnabled ? VISIBLE : GONE); 111 if (mActivityContext.getAppsView() != null) { 112 if (mPredictionsEnabled) { 113 mActivityContext.getAppsView().getAppsStore().registerIconContainer(this); 114 } else { 115 mActivityContext.getAppsView().getAppsStore().unregisterIconContainer(this); 116 } 117 } 118 } 119 120 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)121 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 122 super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(getExpectedHeight(), 123 MeasureSpec.EXACTLY)); 124 } 125 126 @Override dispatchDraw(Canvas canvas)127 protected void dispatchDraw(Canvas canvas) { 128 mFocusHelper.draw(canvas); 129 super.dispatchDraw(canvas); 130 } 131 132 @Override getExpectedHeight()133 public int getExpectedHeight() { 134 DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); 135 int iconHeight = deviceProfile.allAppsIconSizePx; 136 int iconPadding = deviceProfile.allAppsIconDrawablePaddingPx; 137 int textHeight = Utilities.calculateTextHeight(deviceProfile.allAppsIconTextSizePx); 138 int totalHeight = iconHeight + iconPadding + textHeight + mVerticalPadding * 2; 139 // Prediction row height will be 4dp bigger than the regular apps in A-Z list when two line 140 // is not enabled. Otherwise, the extra height will increase by just the textHeight. 141 int extraHeight = (Flags.enableTwolineToggle() && 142 LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(getContext())) 143 ? textHeight : mTopRowExtraHeight; 144 totalHeight += extraHeight; 145 return getVisibility() == GONE ? 0 : totalHeight + getPaddingTop() + getPaddingBottom(); 146 } 147 148 @Override shouldDraw()149 public boolean shouldDraw() { 150 return getVisibility() != GONE; 151 } 152 153 @Override hasVisibleContent()154 public boolean hasVisibleContent() { 155 return mPredictionsEnabled; 156 } 157 158 @Override isVisible()159 public boolean isVisible() { 160 return getVisibility() == VISIBLE; 161 } 162 163 /** 164 * Returns the predicted apps. 165 */ getPredictedApps()166 public List<ItemInfoWithIcon> getPredictedApps() { 167 return new ArrayList<>(mPredictedApps); 168 } 169 170 /** 171 * Sets the current set of predicted apps. 172 * 173 * This can be called before we get the full set of applications, we should merge the results 174 * only in onPredictionsUpdated() which is idempotent. 175 * 176 * If the number of predicted apps is the same as the previous list of predicted apps, 177 * we can optimize by swapping them in place. 178 */ setPredictedApps(List<ItemInfo> items)179 public void setPredictedApps(List<ItemInfo> items) { 180 applyPredictedApps(items); 181 } 182 applyPredictedApps(List<ItemInfo> items)183 private void applyPredictedApps(List<ItemInfo> items) { 184 mPredictedApps.clear(); 185 mPredictedApps.addAll(items.stream() 186 .filter(itemInfo -> itemInfo instanceof WorkspaceItemInfo) 187 .map(itemInfo -> (WorkspaceItemInfo) itemInfo).collect(Collectors.toList())); 188 applyPredictionApps(); 189 } 190 191 @Override onDeviceProfileChanged(DeviceProfile dp)192 public void onDeviceProfileChanged(DeviceProfile dp) { 193 mNumPredictedAppsPerRow = dp.numShownAllAppsColumns; 194 removeAllViews(); 195 applyPredictionApps(); 196 } 197 198 /** Pause the prediction row UI update */ setPredictionUiUpdatePaused(boolean predictionUiUpdatePaused)199 public void setPredictionUiUpdatePaused(boolean predictionUiUpdatePaused) { 200 mPredictionUiUpdatePaused = predictionUiUpdatePaused; 201 if (!mPredictionUiUpdatePaused) { 202 applyPredictionApps(); 203 } 204 } 205 applyPredictionApps()206 private void applyPredictionApps() { 207 if (mPredictionUiUpdatePaused) { 208 return; 209 } 210 if (getChildCount() != mNumPredictedAppsPerRow) { 211 while (getChildCount() > mNumPredictedAppsPerRow) { 212 removeViewAt(0); 213 } 214 LayoutInflater inflater = mActivityContext.getAppsView().getLayoutInflater(); 215 while (getChildCount() < mNumPredictedAppsPerRow) { 216 BubbleTextView icon = (BubbleTextView) inflater.inflate( 217 R.layout.all_apps_prediction_row_icon, this, false); 218 icon.setOnClickListener(mActivityContext.getItemOnClickListener()); 219 icon.setOnLongClickListener(mActivityContext.getAllAppsItemLongClickListener()); 220 icon.setLongPressTimeoutFactor(1f); 221 icon.setOnFocusChangeListener(mFocusHelper); 222 223 LayoutParams lp = (LayoutParams) icon.getLayoutParams(); 224 if (Flags.enableFocusOutline()) { 225 lp.height = ViewGroup.LayoutParams.MATCH_PARENT; 226 } else { 227 // Ensure the all apps icon height matches the workspace icons in portrait mode. 228 lp.height = mActivityContext.getDeviceProfile().allAppsCellHeightPx; 229 } 230 lp.width = 0; 231 lp.weight = 1; 232 addView(icon); 233 } 234 } 235 236 int predictionCount = mPredictedApps.size(); 237 238 for (int i = 0; i < getChildCount(); i++) { 239 BubbleTextView icon = (BubbleTextView) getChildAt(i); 240 icon.reset(); 241 if (predictionCount > i) { 242 icon.setVisibility(View.VISIBLE); 243 WorkspaceItemInfo predictedItem = mPredictedApps.get(i); 244 predictedItem.rank = i; 245 predictedItem.cellX = i; 246 predictedItem.cellY = 0; 247 icon.applyFromWorkspaceItem(predictedItem); 248 } else { 249 icon.setVisibility(predictionCount == 0 ? GONE : INVISIBLE); 250 } 251 } 252 253 boolean predictionsEnabled = predictionCount > 0; 254 if (predictionsEnabled != mPredictionsEnabled) { 255 mPredictionsEnabled = predictionsEnabled; 256 updateVisibility(); 257 } 258 mParent.onHeightUpdated(); 259 } 260 261 @Override hasOverlappingRendering()262 public boolean hasOverlappingRendering() { 263 return false; 264 } 265 266 267 @Override setVerticalScroll(int scroll, boolean isScrolledOut)268 public void setVerticalScroll(int scroll, boolean isScrolledOut) { 269 if (!isScrolledOut) { 270 setTranslationY(scroll); 271 } 272 setAlpha(isScrolledOut ? 0 : 1); 273 if (getVisibility() != GONE) { 274 AlphaUpdateListener.updateVisibility(this); 275 } 276 } 277 278 @Override getTypeClass()279 public Class<PredictionRowView> getTypeClass() { 280 return PredictionRowView.class; 281 } 282 283 @Override getFocusedChild()284 public View getFocusedChild() { 285 return getChildAt(0); 286 } 287 dump(String prefix, PrintWriter writer)288 public void dump(String prefix, PrintWriter writer) { 289 writer.println(prefix + "PredictionRowView"); 290 writer.println(prefix + "\tmPredictionsEnabled: " + mPredictionsEnabled); 291 writer.println(prefix + "\tmPredictionUiUpdatePaused: " + mPredictionUiUpdatePaused); 292 writer.println(prefix + "\tmNumPredictedAppsPerRow: " + mNumPredictedAppsPerRow); 293 writer.println(prefix + "\tmPredictedApps: " + mPredictedApps); 294 } 295 } 296