1 /* 2 * Copyright (C) 2011 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; 18 19 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; 20 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_BUBBLE_ADJUSTMENT_ANIM; 21 22 import android.animation.AnimatorSet; 23 import android.animation.ObjectAnimator; 24 import android.animation.ValueAnimator; 25 import android.content.Context; 26 import android.graphics.Rect; 27 import android.util.AttributeSet; 28 import android.view.Gravity; 29 import android.view.LayoutInflater; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.view.ViewDebug; 33 import android.view.ViewGroup; 34 import android.widget.FrameLayout; 35 36 import androidx.annotation.IntDef; 37 import androidx.annotation.Nullable; 38 39 import com.android.launcher3.ShortcutAndWidgetContainer.TranslationProvider; 40 import com.android.launcher3.celllayout.CellLayoutLayoutParams; 41 import com.android.launcher3.util.HorizontalInsettableView; 42 import com.android.launcher3.util.MultiPropertyFactory; 43 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; 44 import com.android.launcher3.util.MultiTranslateDelegate; 45 import com.android.launcher3.util.MultiValueAlpha; 46 import com.android.launcher3.views.ActivityContext; 47 48 import java.io.PrintWriter; 49 import java.lang.annotation.Retention; 50 import java.lang.annotation.RetentionPolicy; 51 52 /** 53 * View class that represents the bottom row of the home screen. 54 */ 55 public class Hotseat extends CellLayout implements Insettable { 56 57 public static final int ALPHA_CHANNEL_TASKBAR_ALIGNMENT = 0; 58 public static final int ALPHA_CHANNEL_PREVIEW_RENDERER = 1; 59 public static final int ALPHA_CHANNEL_TASKBAR_STASH = 2; 60 public static final int ALPHA_CHANNEL_CHANNELS_COUNT = 3; 61 62 @Retention(RetentionPolicy.RUNTIME) 63 @IntDef({ALPHA_CHANNEL_TASKBAR_ALIGNMENT, ALPHA_CHANNEL_PREVIEW_RENDERER, 64 ALPHA_CHANNEL_TASKBAR_STASH}) 65 public @interface HotseatQsbAlphaId { 66 } 67 68 public static final int ICONS_TRANSLATION_X_NAV_BAR_ALIGNMENT = 0; 69 public static final int ICONS_TRANSLATION_X_CHANNELS_COUNT = 1; 70 71 @Retention(RetentionPolicy.RUNTIME) 72 @IntDef({ICONS_TRANSLATION_X_NAV_BAR_ALIGNMENT}) 73 public @interface IconsTranslationX { 74 } 75 76 // Ratio of empty space, qsb should take up to appear visually centered. 77 public static final float QSB_CENTER_FACTOR = .325f; 78 private static final int BUBBLE_BAR_ADJUSTMENT_ANIMATION_DURATION_MS = 250; 79 80 @ViewDebug.ExportedProperty(category = "launcher") 81 private boolean mHasVerticalHotseat; 82 private Workspace<?> mWorkspace; 83 private boolean mSendTouchToWorkspace; 84 private final MultiValueAlpha mIconsAlphaChannels; 85 private final MultiValueAlpha mQsbAlphaChannels; 86 87 private @Nullable MultiProperty mQsbTranslationX; 88 89 private final MultiPropertyFactory mIconsTranslationXFactory; 90 91 private final View mQsb; 92 Hotseat(Context context)93 public Hotseat(Context context) { 94 this(context, null); 95 } 96 Hotseat(Context context, AttributeSet attrs)97 public Hotseat(Context context, AttributeSet attrs) { 98 this(context, attrs, 0); 99 } 100 Hotseat(Context context, AttributeSet attrs, int defStyle)101 public Hotseat(Context context, AttributeSet attrs, int defStyle) { 102 super(context, attrs, defStyle); 103 mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false); 104 addView(mQsb); 105 mIconsAlphaChannels = new MultiValueAlpha(getShortcutsAndWidgets(), 106 ALPHA_CHANNEL_CHANNELS_COUNT); 107 if (mQsb instanceof Reorderable qsbReorderable) { 108 mQsbTranslationX = qsbReorderable.getTranslateDelegate() 109 .getTranslationX(MultiTranslateDelegate.INDEX_NAV_BAR_ANIM); 110 } 111 mIconsTranslationXFactory = new MultiPropertyFactory<>(getShortcutsAndWidgets(), 112 VIEW_TRANSLATE_X, ICONS_TRANSLATION_X_CHANNELS_COUNT, Float::sum); 113 mQsbAlphaChannels = new MultiValueAlpha(mQsb, ALPHA_CHANNEL_CHANNELS_COUNT); 114 } 115 116 /** Provides translation X for hotseat icons for the channel. */ getIconsTranslationX(@consTranslationX int channelId)117 public MultiProperty getIconsTranslationX(@IconsTranslationX int channelId) { 118 return mIconsTranslationXFactory.get(channelId); 119 } 120 121 /** Provides translation X for hotseat Qsb. */ 122 @Nullable getQsbTranslationX()123 public MultiProperty getQsbTranslationX() { 124 return mQsbTranslationX; 125 } 126 127 /** 128 * Returns orientation specific cell X given invariant order in the hotseat 129 */ getCellXFromOrder(int rank)130 public int getCellXFromOrder(int rank) { 131 return mHasVerticalHotseat ? 0 : rank; 132 } 133 134 /** 135 * Returns orientation specific cell Y given invariant order in the hotseat 136 */ getCellYFromOrder(int rank)137 public int getCellYFromOrder(int rank) { 138 return mHasVerticalHotseat ? (getCountY() - (rank + 1)) : 0; 139 } 140 isHasVerticalHotseat()141 boolean isHasVerticalHotseat() { 142 return mHasVerticalHotseat; 143 } 144 resetLayout(boolean hasVerticalHotseat)145 public void resetLayout(boolean hasVerticalHotseat) { 146 ActivityContext activityContext = ActivityContext.lookupContext(getContext()); 147 boolean bubbleBarEnabled = activityContext.isBubbleBarEnabled(); 148 boolean hasBubbles = activityContext.hasBubbles(); 149 removeAllViewsInLayout(); 150 mHasVerticalHotseat = hasVerticalHotseat; 151 DeviceProfile dp = mActivity.getDeviceProfile(); 152 153 if (bubbleBarEnabled) { 154 if (dp.shouldAdjustHotseatForBubbleBar(getContext(), hasBubbles)) { 155 getShortcutsAndWidgets().setTranslationProvider( 156 cellX -> dp.getHotseatAdjustedTranslation(getContext(), cellX)); 157 if (mQsb instanceof HorizontalInsettableView) { 158 HorizontalInsettableView insettableQsb = (HorizontalInsettableView) mQsb; 159 final float insetFraction = (float) dp.iconSizePx / dp.hotseatQsbWidth; 160 // post this to the looper so that QSB has a chance to redraw itself, e.g. 161 // after device rotation 162 mQsb.post(() -> insettableQsb.setHorizontalInsets(insetFraction)); 163 } 164 } else { 165 getShortcutsAndWidgets().setTranslationProvider(null); 166 if (mQsb instanceof HorizontalInsettableView) { 167 ((HorizontalInsettableView) mQsb).setHorizontalInsets(0); 168 } 169 } 170 } 171 172 resetCellSize(dp); 173 if (hasVerticalHotseat) { 174 setGridSize(1, dp.numShownHotseatIcons); 175 } else { 176 setGridSize(dp.numShownHotseatIcons, 1); 177 } 178 } 179 180 /** 181 * Adjust the hotseat icons for the bubble bar. 182 * 183 * <p>When the bubble bar becomes visible, if needed, this method animates the hotseat icons 184 * to reduce the spacing between them and make room for the bubble bar. The QSB width is 185 * animated as well to align with the hotseat icons. 186 * 187 * <p>When the bubble bar goes away, any adjustments that were previously made are reversed. 188 */ adjustForBubbleBar(boolean isBubbleBarVisible)189 public void adjustForBubbleBar(boolean isBubbleBarVisible) { 190 DeviceProfile dp = mActivity.getDeviceProfile(); 191 boolean shouldAdjust = isBubbleBarVisible 192 && dp.shouldAdjustHotseatOrQsbForBubbleBar(getContext()); 193 boolean shouldAdjustHotseat = shouldAdjust 194 && dp.shouldAlignBubbleBarWithHotseat(); 195 ShortcutAndWidgetContainer icons = getShortcutsAndWidgets(); 196 // update the translation provider for future layout passes of hotseat icons. 197 if (shouldAdjustHotseat) { 198 icons.setTranslationProvider( 199 cellX -> dp.getHotseatAdjustedTranslation(getContext(), cellX)); 200 } else { 201 icons.setTranslationProvider(null); 202 } 203 AnimatorSet animatorSet = new AnimatorSet(); 204 for (int i = 0; i < icons.getChildCount(); i++) { 205 View child = icons.getChildAt(i); 206 if (child.getLayoutParams() instanceof CellLayoutLayoutParams lp) { 207 float tx = shouldAdjustHotseat 208 ? dp.getHotseatAdjustedTranslation(getContext(), lp.getCellX()) : 0; 209 if (child instanceof Reorderable) { 210 MultiTranslateDelegate mtd = ((Reorderable) child).getTranslateDelegate(); 211 animatorSet.play( 212 mtd.getTranslationX(INDEX_BUBBLE_ADJUSTMENT_ANIM).animateToValue(tx)); 213 } else { 214 animatorSet.play(ObjectAnimator.ofFloat(child, VIEW_TRANSLATE_X, tx)); 215 } 216 } 217 } 218 //TODO(b/381109832) refactor & simplify adjustment logic 219 boolean shouldAdjustQsb = 220 shouldAdjustHotseat || (shouldAdjust && dp.shouldAlignBubbleBarWithQSB()); 221 if (mQsb instanceof HorizontalInsettableView horizontalInsettableQsb) { 222 final float currentInsetFraction = horizontalInsettableQsb.getHorizontalInsets(); 223 final float targetInsetFraction = shouldAdjustQsb 224 ? (float) dp.iconSizePx / dp.hotseatQsbWidth : 0; 225 ValueAnimator qsbAnimator = 226 ValueAnimator.ofFloat(currentInsetFraction, targetInsetFraction); 227 qsbAnimator.addUpdateListener(animation -> { 228 float insetFraction = (float) animation.getAnimatedValue(); 229 horizontalInsettableQsb.setHorizontalInsets(insetFraction); 230 }); 231 animatorSet.play(qsbAnimator); 232 } 233 animatorSet.setDuration(BUBBLE_BAR_ADJUSTMENT_ANIMATION_DURATION_MS).start(); 234 } 235 236 @Override getTranslationXForCell(int cellX, int cellY)237 protected int getTranslationXForCell(int cellX, int cellY) { 238 TranslationProvider translationProvider = getShortcutsAndWidgets().getTranslationProvider(); 239 if (translationProvider == null) return 0; 240 return (int) translationProvider.getTranslationX(cellX); 241 } 242 243 @Override setInsets(Rect insets)244 public void setInsets(Rect insets) { 245 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); 246 DeviceProfile grid = mActivity.getDeviceProfile(); 247 248 if (grid.isVerticalBarLayout()) { 249 mQsb.setVisibility(View.GONE); 250 lp.height = ViewGroup.LayoutParams.MATCH_PARENT; 251 if (grid.isSeascape()) { 252 lp.gravity = Gravity.LEFT; 253 lp.width = grid.hotseatBarSizePx + insets.left; 254 } else { 255 lp.gravity = Gravity.RIGHT; 256 lp.width = grid.hotseatBarSizePx + insets.right; 257 } 258 } else { 259 mQsb.setVisibility(View.VISIBLE); 260 lp.gravity = Gravity.BOTTOM; 261 lp.width = ViewGroup.LayoutParams.MATCH_PARENT; 262 lp.height = grid.hotseatBarSizePx; 263 } 264 265 Rect padding = grid.getHotseatLayoutPadding(getContext()); 266 setPadding(padding.left, padding.top, padding.right, padding.bottom); 267 setLayoutParams(lp); 268 InsettableFrameLayout.dispatchInsets(this, insets); 269 } 270 setWorkspace(Workspace<?> w)271 public void setWorkspace(Workspace<?> w) { 272 mWorkspace = w; 273 setCellLayoutContainer(w); 274 } 275 276 @Override onInterceptTouchEvent(MotionEvent ev)277 public boolean onInterceptTouchEvent(MotionEvent ev) { 278 // We allow horizontal workspace scrolling from within the Hotseat. We do this by delegating 279 // touch intercept the Workspace, and if it intercepts, delegating touch to the Workspace 280 // for the remainder of the this input stream. 281 int yThreshold = getMeasuredHeight() - getPaddingBottom(); 282 if (mWorkspace != null && ev.getY() <= yThreshold) { 283 mSendTouchToWorkspace = mWorkspace.onInterceptTouchEvent(ev); 284 return mSendTouchToWorkspace; 285 } 286 return false; 287 } 288 289 @Override onTouchEvent(MotionEvent event)290 public boolean onTouchEvent(MotionEvent event) { 291 // See comment in #onInterceptTouchEvent 292 if (mSendTouchToWorkspace) { 293 final int action = event.getAction(); 294 switch (action & MotionEvent.ACTION_MASK) { 295 case MotionEvent.ACTION_UP: 296 case MotionEvent.ACTION_CANCEL: 297 mSendTouchToWorkspace = false; 298 } 299 return mWorkspace.onTouchEvent(event); 300 } 301 // Always let touch follow through to Workspace. 302 return false; 303 } 304 305 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)306 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 307 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 308 309 DeviceProfile dp = mActivity.getDeviceProfile(); 310 mQsb.measure(MeasureSpec.makeMeasureSpec(dp.hotseatQsbWidth, MeasureSpec.EXACTLY), 311 MeasureSpec.makeMeasureSpec(dp.hotseatQsbHeight, MeasureSpec.EXACTLY)); 312 } 313 314 @Override onLayout(boolean changed, int l, int t, int r, int b)315 protected void onLayout(boolean changed, int l, int t, int r, int b) { 316 super.onLayout(changed, l, t, r, b); 317 318 int qsbMeasuredWidth = mQsb.getMeasuredWidth(); 319 int left; 320 DeviceProfile dp = mActivity.getDeviceProfile(); 321 if (dp.isQsbInline) { 322 int qsbSpace = dp.hotseatBorderSpace; 323 left = Utilities.isRtl(getResources()) ? r - getPaddingRight() + qsbSpace 324 : l + getPaddingLeft() - qsbMeasuredWidth - qsbSpace; 325 } else { 326 left = (r - l - qsbMeasuredWidth) / 2; 327 } 328 int right = left + qsbMeasuredWidth; 329 330 int bottom = b - t - dp.getQsbOffsetY(); 331 int top = bottom - dp.hotseatQsbHeight; 332 mQsb.layout(left, top, right, bottom); 333 } 334 335 /** 336 * Sets the alpha value of the specified alpha channel of just our ShortcutAndWidgetContainer. 337 */ setIconsAlpha(float alpha, @HotseatQsbAlphaId int channelId)338 public void setIconsAlpha(float alpha, @HotseatQsbAlphaId int channelId) { 339 getIconsAlpha(channelId).setValue(alpha); 340 } 341 342 /** 343 * Sets the alpha value of just our QSB. 344 */ setQsbAlpha(float alpha, @HotseatQsbAlphaId int channelId)345 public void setQsbAlpha(float alpha, @HotseatQsbAlphaId int channelId) { 346 getQsbAlpha(channelId).setValue(alpha); 347 } 348 349 /** Returns the alpha channel for ShortcutAndWidgetContainer */ getIconsAlpha(@otseatQsbAlphaId int channelId)350 public MultiProperty getIconsAlpha(@HotseatQsbAlphaId int channelId) { 351 return mIconsAlphaChannels.get(channelId); 352 } 353 354 /** Returns the alpha channel for Qsb */ getQsbAlpha(@otseatQsbAlphaId int channelId)355 public MultiProperty getQsbAlpha(@HotseatQsbAlphaId int channelId) { 356 return mQsbAlphaChannels.get(channelId); 357 } 358 359 /** 360 * Returns the QSB inside hotseat 361 */ getQsb()362 public View getQsb() { 363 return mQsb; 364 } 365 366 /** Dumps the Hotseat internal state */ dump(String prefix, PrintWriter writer)367 public void dump(String prefix, PrintWriter writer) { 368 writer.println(prefix + "Hotseat:"); 369 mIconsAlphaChannels.dump( 370 prefix + "\t", 371 writer, 372 "mIconsAlphaChannels", 373 "ALPHA_CHANNEL_TASKBAR_ALIGNMENT", 374 "ALPHA_CHANNEL_PREVIEW_RENDERER", 375 "ALPHA_CHANNEL_TASKBAR_STASH"); 376 mQsbAlphaChannels.dump( 377 prefix + "\t", 378 writer, 379 "mQsbAlphaChannels", 380 "ALPHA_CHANNEL_TASKBAR_ALIGNMENT", 381 "ALPHA_CHANNEL_PREVIEW_RENDERER", 382 "ALPHA_CHANNEL_TASKBAR_STASH" 383 ); 384 } 385 386 } 387