• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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