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 com.android.launcher3.util.HorizontalInsettableView; 37 import com.android.launcher3.util.MultiTranslateDelegate; 38 import com.android.launcher3.views.ActivityContext; 39 40 /** 41 * View class that represents the bottom row of the home screen. 42 */ 43 public class Hotseat extends CellLayout implements Insettable { 44 45 // Ratio of empty space, qsb should take up to appear visually centered. 46 public static final float QSB_CENTER_FACTOR = .325f; 47 private static final int BUBBLE_BAR_ADJUSTMENT_ANIMATION_DURATION_MS = 250; 48 49 @ViewDebug.ExportedProperty(category = "launcher") 50 private boolean mHasVerticalHotseat; 51 private Workspace<?> mWorkspace; 52 private boolean mSendTouchToWorkspace; 53 54 private final View mQsb; 55 Hotseat(Context context)56 public Hotseat(Context context) { 57 this(context, null); 58 } 59 Hotseat(Context context, AttributeSet attrs)60 public Hotseat(Context context, AttributeSet attrs) { 61 this(context, attrs, 0); 62 } 63 Hotseat(Context context, AttributeSet attrs, int defStyle)64 public Hotseat(Context context, AttributeSet attrs, int defStyle) { 65 super(context, attrs, defStyle); 66 67 mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false); 68 addView(mQsb); 69 } 70 71 /** 72 * Returns orientation specific cell X given invariant order in the hotseat 73 */ getCellXFromOrder(int rank)74 public int getCellXFromOrder(int rank) { 75 return mHasVerticalHotseat ? 0 : rank; 76 } 77 78 /** 79 * Returns orientation specific cell Y given invariant order in the hotseat 80 */ getCellYFromOrder(int rank)81 public int getCellYFromOrder(int rank) { 82 return mHasVerticalHotseat ? (getCountY() - (rank + 1)) : 0; 83 } 84 isHasVerticalHotseat()85 boolean isHasVerticalHotseat() { 86 return mHasVerticalHotseat; 87 } 88 resetLayout(boolean hasVerticalHotseat)89 public void resetLayout(boolean hasVerticalHotseat) { 90 ActivityContext activityContext = ActivityContext.lookupContext(getContext()); 91 boolean bubbleBarEnabled = activityContext.isBubbleBarEnabled(); 92 boolean hasBubbles = activityContext.hasBubbles(); 93 removeAllViewsInLayout(); 94 mHasVerticalHotseat = hasVerticalHotseat; 95 DeviceProfile dp = mActivity.getDeviceProfile(); 96 97 if (bubbleBarEnabled) { 98 float adjustedBorderSpace = dp.getHotseatAdjustedBorderSpaceForBubbleBar(getContext()); 99 if (hasBubbles && Float.compare(adjustedBorderSpace, 0f) != 0) { 100 getShortcutsAndWidgets().setTranslationProvider(child -> { 101 int index = getShortcutsAndWidgets().indexOfChild(child); 102 float borderSpaceDelta = adjustedBorderSpace - dp.hotseatBorderSpace; 103 return dp.iconSizePx + index * borderSpaceDelta; 104 }); 105 if (mQsb instanceof HorizontalInsettableView) { 106 HorizontalInsettableView insettableQsb = (HorizontalInsettableView) mQsb; 107 final float insetFraction = (float) dp.iconSizePx / dp.hotseatQsbWidth; 108 // post this to the looper so that QSB has a chance to redraw itself, e.g. 109 // after device rotation 110 mQsb.post(() -> insettableQsb.setHorizontalInsets(insetFraction)); 111 } 112 } else { 113 getShortcutsAndWidgets().setTranslationProvider(null); 114 if (mQsb instanceof HorizontalInsettableView) { 115 ((HorizontalInsettableView) mQsb).setHorizontalInsets(0); 116 } 117 } 118 } 119 120 resetCellSize(dp); 121 if (hasVerticalHotseat) { 122 setGridSize(1, dp.numShownHotseatIcons); 123 } else { 124 setGridSize(dp.numShownHotseatIcons, 1); 125 } 126 } 127 128 /** 129 * Adjust the hotseat icons for the bubble bar. 130 * 131 * <p>When the bubble bar becomes visible, if needed, this method animates the hotseat icons 132 * to reduce the spacing between them and make room for the bubble bar. The QSB width is 133 * animated as well to align with the hotseat icons. 134 * 135 * <p>When the bubble bar goes away, any adjustments that were previously made are reversed. 136 */ adjustForBubbleBar(boolean isBubbleBarVisible)137 public void adjustForBubbleBar(boolean isBubbleBarVisible) { 138 DeviceProfile dp = mActivity.getDeviceProfile(); 139 float adjustedBorderSpace = dp.getHotseatAdjustedBorderSpaceForBubbleBar(getContext()); 140 if (Float.compare(adjustedBorderSpace, 0f) == 0) { 141 return; 142 } 143 144 ShortcutAndWidgetContainer icons = getShortcutsAndWidgets(); 145 AnimatorSet animatorSet = new AnimatorSet(); 146 float borderSpaceDelta = adjustedBorderSpace - dp.hotseatBorderSpace; 147 148 // update the translation provider for future layout passes of hotseat icons. 149 if (isBubbleBarVisible) { 150 icons.setTranslationProvider(child -> { 151 int index = icons.indexOfChild(child); 152 return dp.iconSizePx + index * borderSpaceDelta; 153 }); 154 } else { 155 icons.setTranslationProvider(null); 156 } 157 158 for (int i = 0; i < icons.getChildCount(); i++) { 159 View child = icons.getChildAt(i); 160 float tx = isBubbleBarVisible ? dp.iconSizePx + i * borderSpaceDelta : 0; 161 if (child instanceof Reorderable) { 162 MultiTranslateDelegate mtd = ((Reorderable) child).getTranslateDelegate(); 163 animatorSet.play( 164 mtd.getTranslationX(INDEX_BUBBLE_ADJUSTMENT_ANIM).animateToValue(tx)); 165 } else { 166 animatorSet.play(ObjectAnimator.ofFloat(child, VIEW_TRANSLATE_X, tx)); 167 } 168 } 169 if (mQsb instanceof HorizontalInsettableView) { 170 HorizontalInsettableView horizontalInsettableQsb = (HorizontalInsettableView) mQsb; 171 ValueAnimator qsbAnimator = ValueAnimator.ofFloat(0f, 1f); 172 qsbAnimator.addUpdateListener(animation -> { 173 float fraction = qsbAnimator.getAnimatedFraction(); 174 float insetFraction = isBubbleBarVisible 175 ? (float) dp.iconSizePx * fraction / dp.hotseatQsbWidth 176 : (float) dp.iconSizePx * (1 - fraction) / dp.hotseatQsbWidth; 177 horizontalInsettableQsb.setHorizontalInsets(insetFraction); 178 }); 179 animatorSet.play(qsbAnimator); 180 } 181 animatorSet.setDuration(BUBBLE_BAR_ADJUSTMENT_ANIMATION_DURATION_MS).start(); 182 } 183 184 @Override setInsets(Rect insets)185 public void setInsets(Rect insets) { 186 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); 187 DeviceProfile grid = mActivity.getDeviceProfile(); 188 189 if (grid.isVerticalBarLayout()) { 190 mQsb.setVisibility(View.GONE); 191 lp.height = ViewGroup.LayoutParams.MATCH_PARENT; 192 if (grid.isSeascape()) { 193 lp.gravity = Gravity.LEFT; 194 lp.width = grid.hotseatBarSizePx + insets.left; 195 } else { 196 lp.gravity = Gravity.RIGHT; 197 lp.width = grid.hotseatBarSizePx + insets.right; 198 } 199 } else { 200 mQsb.setVisibility(View.VISIBLE); 201 lp.gravity = Gravity.BOTTOM; 202 lp.width = ViewGroup.LayoutParams.MATCH_PARENT; 203 lp.height = grid.hotseatBarSizePx; 204 } 205 206 Rect padding = grid.getHotseatLayoutPadding(getContext()); 207 setPadding(padding.left, padding.top, padding.right, padding.bottom); 208 setLayoutParams(lp); 209 InsettableFrameLayout.dispatchInsets(this, insets); 210 } 211 setWorkspace(Workspace<?> w)212 public void setWorkspace(Workspace<?> w) { 213 mWorkspace = w; 214 setCellLayoutContainer(w); 215 } 216 217 @Override onInterceptTouchEvent(MotionEvent ev)218 public boolean onInterceptTouchEvent(MotionEvent ev) { 219 // We allow horizontal workspace scrolling from within the Hotseat. We do this by delegating 220 // touch intercept the Workspace, and if it intercepts, delegating touch to the Workspace 221 // for the remainder of the this input stream. 222 int yThreshold = getMeasuredHeight() - getPaddingBottom(); 223 if (mWorkspace != null && ev.getY() <= yThreshold) { 224 mSendTouchToWorkspace = mWorkspace.onInterceptTouchEvent(ev); 225 return mSendTouchToWorkspace; 226 } 227 return false; 228 } 229 230 @Override onTouchEvent(MotionEvent event)231 public boolean onTouchEvent(MotionEvent event) { 232 // See comment in #onInterceptTouchEvent 233 if (mSendTouchToWorkspace) { 234 final int action = event.getAction(); 235 switch (action & MotionEvent.ACTION_MASK) { 236 case MotionEvent.ACTION_UP: 237 case MotionEvent.ACTION_CANCEL: 238 mSendTouchToWorkspace = false; 239 } 240 return mWorkspace.onTouchEvent(event); 241 } 242 // Always let touch follow through to Workspace. 243 return false; 244 } 245 246 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)247 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 248 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 249 250 DeviceProfile dp = mActivity.getDeviceProfile(); 251 mQsb.measure(MeasureSpec.makeMeasureSpec(dp.hotseatQsbWidth, MeasureSpec.EXACTLY), 252 MeasureSpec.makeMeasureSpec(dp.hotseatQsbHeight, MeasureSpec.EXACTLY)); 253 } 254 255 @Override onLayout(boolean changed, int l, int t, int r, int b)256 protected void onLayout(boolean changed, int l, int t, int r, int b) { 257 super.onLayout(changed, l, t, r, b); 258 259 int qsbMeasuredWidth = mQsb.getMeasuredWidth(); 260 int left; 261 DeviceProfile dp = mActivity.getDeviceProfile(); 262 if (dp.isQsbInline) { 263 int qsbSpace = dp.hotseatBorderSpace; 264 left = Utilities.isRtl(getResources()) ? r - getPaddingRight() + qsbSpace 265 : l + getPaddingLeft() - qsbMeasuredWidth - qsbSpace; 266 } else { 267 left = (r - l - qsbMeasuredWidth) / 2; 268 } 269 int right = left + qsbMeasuredWidth; 270 271 int bottom = b - t - dp.getQsbOffsetY(); 272 int top = bottom - dp.hotseatQsbHeight; 273 mQsb.layout(left, top, right, bottom); 274 } 275 276 /** 277 * Sets the alpha value of just our ShortcutAndWidgetContainer. 278 */ setIconsAlpha(float alpha)279 public void setIconsAlpha(float alpha) { 280 getShortcutsAndWidgets().setAlpha(alpha); 281 } 282 283 /** 284 * Sets the alpha value of just our QSB. 285 */ setQsbAlpha(float alpha)286 public void setQsbAlpha(float alpha) { 287 mQsb.setAlpha(alpha); 288 } 289 getIconsAlpha()290 public float getIconsAlpha() { 291 return getShortcutsAndWidgets().getAlpha(); 292 } 293 294 /** 295 * Returns the QSB inside hotseat 296 */ getQsb()297 public View getQsb() { 298 return mQsb; 299 } 300 301 } 302