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