1 /*
2  * Copyright (C) 2021 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 package com.android.launcher3.taskbar;
17 
18 import android.content.Context;
19 import android.content.res.Resources;
20 import android.graphics.Canvas;
21 import android.graphics.Rect;
22 import android.util.AttributeSet;
23 import android.view.MotionEvent;
24 import android.view.View;
25 import android.widget.FrameLayout;
26 
27 import androidx.annotation.LayoutRes;
28 import androidx.annotation.NonNull;
29 import androidx.annotation.Nullable;
30 
31 import com.android.launcher3.BubbleTextView;
32 import com.android.launcher3.Insettable;
33 import com.android.launcher3.R;
34 import com.android.launcher3.folder.FolderIcon;
35 import com.android.launcher3.model.data.FolderInfo;
36 import com.android.launcher3.model.data.ItemInfo;
37 import com.android.launcher3.model.data.WorkspaceItemInfo;
38 import com.android.launcher3.uioverrides.ApiWrapper;
39 import com.android.launcher3.views.ActivityContext;
40 
41 /**
42  * Hosts the Taskbar content such as Hotseat and Recent Apps. Drawn on top of other apps.
43  */
44 public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconParent, Insettable {
45 
46     private final int[] mTempOutLocation = new int[2];
47 
48     private final Rect mIconLayoutBounds = new Rect();
49     private final int mIconTouchSize;
50     private final int mItemMarginLeftRight;
51     private final int mItemPadding;
52 
53     private final TaskbarActivityContext mActivityContext;
54 
55     // Initialized in init.
56     private TaskbarViewController.TaskbarViewCallbacks mControllerCallbacks;
57     private View.OnClickListener mIconClickListener;
58     private View.OnLongClickListener mIconLongClickListener;
59 
60     // Prevents dispatching touches to children if true
61     private boolean mTouchEnabled = true;
62 
63     // Only non-null when the corresponding Folder is open.
64     private @Nullable FolderIcon mLeaveBehindFolderIcon;
65 
TaskbarView(@onNull Context context)66     public TaskbarView(@NonNull Context context) {
67         this(context, null);
68     }
69 
TaskbarView(@onNull Context context, @Nullable AttributeSet attrs)70     public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs) {
71         this(context, attrs, 0);
72     }
73 
TaskbarView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)74     public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs,
75             int defStyleAttr) {
76         this(context, attrs, defStyleAttr, 0);
77     }
78 
TaskbarView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)79     public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
80             int defStyleRes) {
81         super(context, attrs, defStyleAttr, defStyleRes);
82         mActivityContext = ActivityContext.lookupContext(context);
83 
84         Resources resources = getResources();
85         mIconTouchSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_touch_size);
86 
87         int actualMargin = resources.getDimensionPixelSize(R.dimen.taskbar_icon_spacing);
88         int actualIconSize = mActivityContext.getDeviceProfile().iconSizePx;
89 
90         // We layout the icons to be of mIconTouchSize in width and height
91         mItemMarginLeftRight = actualMargin - (mIconTouchSize - actualIconSize) / 2;
92         mItemPadding = (mIconTouchSize - actualIconSize) / 2;
93 
94         // Needed to draw folder leave-behind when opening one.
95         setWillNotDraw(false);
96     }
97 
init(TaskbarViewController.TaskbarViewCallbacks callbacks)98     protected void init(TaskbarViewController.TaskbarViewCallbacks callbacks) {
99         mControllerCallbacks = callbacks;
100         mIconClickListener = mControllerCallbacks.getIconOnClickListener();
101         mIconLongClickListener = mControllerCallbacks.getIconOnLongClickListener();
102 
103         setOnLongClickListener(mControllerCallbacks.getBackgroundOnLongClickListener());
104     }
105 
removeAndRecycle(View view)106     private void removeAndRecycle(View view) {
107         removeView(view);
108         view.setOnClickListener(null);
109         view.setOnLongClickListener(null);
110         if (!(view.getTag() instanceof FolderInfo)) {
111             mActivityContext.getViewCache().recycleView(view.getSourceLayoutResId(), view);
112         }
113         view.setTag(null);
114     }
115 
116     /**
117      * Inflates/binds the Hotseat views to show in the Taskbar given their ItemInfos.
118      */
updateHotseatItems(ItemInfo[] hotseatItemInfos)119     protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
120         int nextViewIndex = 0;
121         int numViewsAnimated = 0;
122 
123         for (int i = 0; i < hotseatItemInfos.length; i++) {
124             ItemInfo hotseatItemInfo = hotseatItemInfos[i];
125             if (hotseatItemInfo == null) {
126                 continue;
127             }
128 
129             // Replace any Hotseat views with the appropriate type if it's not already that type.
130             final int expectedLayoutResId;
131             boolean isFolder = false;
132             if (hotseatItemInfo.isPredictedItem()) {
133                 expectedLayoutResId = R.layout.taskbar_predicted_app_icon;
134             } else if (hotseatItemInfo instanceof FolderInfo) {
135                 expectedLayoutResId = R.layout.folder_icon;
136                 isFolder = true;
137             } else {
138                 expectedLayoutResId = R.layout.taskbar_app_icon;
139             }
140 
141             View hotseatView = null;
142             while (nextViewIndex < getChildCount()) {
143                 hotseatView = getChildAt(nextViewIndex);
144 
145                 // see if the view can be reused
146                 if ((hotseatView.getSourceLayoutResId() != expectedLayoutResId)
147                         || (isFolder && (hotseatView.getTag() != hotseatItemInfo))) {
148                     // Unlike for BubbleTextView, we can't reapply a new FolderInfo after inflation,
149                     // so if the info changes we need to reinflate. This should only happen if a new
150                     // folder is dragged to the position that another folder previously existed.
151                     removeAndRecycle(hotseatView);
152                     hotseatView = null;
153                 } else {
154                     // View found
155                     break;
156                 }
157             }
158 
159             if (hotseatView == null) {
160                 if (isFolder) {
161                     FolderInfo folderInfo = (FolderInfo) hotseatItemInfo;
162                     FolderIcon folderIcon = FolderIcon.inflateFolderAndIcon(expectedLayoutResId,
163                             mActivityContext, this, folderInfo);
164                     folderIcon.setTextVisible(false);
165                     hotseatView = folderIcon;
166                 } else {
167                     hotseatView = inflate(expectedLayoutResId);
168                 }
169                 LayoutParams lp = new LayoutParams(mIconTouchSize, mIconTouchSize);
170                 hotseatView.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
171                 addView(hotseatView, nextViewIndex, lp);
172             }
173 
174             // Apply the Hotseat ItemInfos, or hide the view if there is none for a given index.
175             if (hotseatView instanceof BubbleTextView
176                     && hotseatItemInfo instanceof WorkspaceItemInfo) {
177                 BubbleTextView btv = (BubbleTextView) hotseatView;
178                 WorkspaceItemInfo workspaceInfo = (WorkspaceItemInfo) hotseatItemInfo;
179 
180                 boolean animate = btv.shouldAnimateIconChange((WorkspaceItemInfo) hotseatItemInfo);
181                 btv.applyFromWorkspaceItem(workspaceInfo, animate, numViewsAnimated);
182                 if (animate) {
183                     numViewsAnimated++;
184                 }
185             }
186             setClickAndLongClickListenersForIcon(hotseatView);
187             nextViewIndex++;
188         }
189         // Remove remaining views
190         while (nextViewIndex < getChildCount()) {
191             removeAndRecycle(getChildAt(nextViewIndex));
192         }
193     }
194 
195     /**
196      * Sets OnClickListener and OnLongClickListener for the given view.
197      */
setClickAndLongClickListenersForIcon(View icon)198     public void setClickAndLongClickListenersForIcon(View icon) {
199         icon.setOnClickListener(mIconClickListener);
200         icon.setOnLongClickListener(mIconLongClickListener);
201     }
202 
203     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)204     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
205         int count = getChildCount();
206         int spaceNeeded = count * (mItemMarginLeftRight * 2 + mIconTouchSize);
207         int navSpaceNeeded = ApiWrapper.getHotseatEndOffset(getContext());
208         boolean layoutRtl = isLayoutRtl();
209         int iconEnd = right - (right - left - spaceNeeded) / 2;
210         boolean needMoreSpaceForNav = layoutRtl ?
211                 navSpaceNeeded > (iconEnd - spaceNeeded) :
212                 iconEnd > (right - navSpaceNeeded);
213         if (needMoreSpaceForNav) {
214             int offset = layoutRtl ?
215                     navSpaceNeeded - (iconEnd - spaceNeeded) :
216                     (right - navSpaceNeeded) - iconEnd;
217             iconEnd += offset;
218         }
219         // Layout the children
220         mIconLayoutBounds.right = iconEnd;
221         mIconLayoutBounds.top = (bottom - top - mIconTouchSize) / 2;
222         mIconLayoutBounds.bottom = mIconLayoutBounds.top + mIconTouchSize;
223         for (int i = count; i > 0; i--) {
224             View child = getChildAt(i - 1);
225             iconEnd -= mItemMarginLeftRight;
226             int iconStart = iconEnd - mIconTouchSize;
227             child.layout(iconStart, mIconLayoutBounds.top, iconEnd, mIconLayoutBounds.bottom);
228             iconEnd = iconStart - mItemMarginLeftRight;
229         }
230         mIconLayoutBounds.left = iconEnd;
231     }
232 
233     @Override
dispatchTouchEvent(MotionEvent ev)234     public boolean dispatchTouchEvent(MotionEvent ev) {
235         if (!mTouchEnabled) {
236             return true;
237         }
238         return super.dispatchTouchEvent(ev);
239     }
240 
241     @Override
onTouchEvent(MotionEvent event)242     public boolean onTouchEvent(MotionEvent event) {
243         if (!mTouchEnabled) {
244             return true;
245         }
246         if (mIconLayoutBounds.contains((int) event.getX(), (int) event.getY())) {
247             // Don't allow long pressing between icons.
248             return true;
249         }
250         if (mControllerCallbacks.onTouchEvent(event)) {
251             int oldAction = event.getAction();
252             try {
253                 event.setAction(MotionEvent.ACTION_CANCEL);
254                 return super.onTouchEvent(event);
255             } finally {
256                 event.setAction(oldAction);
257             }
258         }
259         return super.onTouchEvent(event);
260     }
261 
setTouchesEnabled(boolean touchEnabled)262     public void setTouchesEnabled(boolean touchEnabled) {
263         this.mTouchEnabled = touchEnabled;
264     }
265 
266     /**
267      * Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's
268      * touch bounds.
269      */
isEventOverAnyItem(MotionEvent ev)270     public boolean isEventOverAnyItem(MotionEvent ev) {
271         getLocationOnScreen(mTempOutLocation);
272         int xInOurCoordinates = (int) ev.getX() - mTempOutLocation[0];
273         int yInOurCoorindates = (int) ev.getY() - mTempOutLocation[1];
274         return isShown() && mIconLayoutBounds.contains(xInOurCoordinates, yInOurCoorindates);
275     }
276 
getIconLayoutBounds()277     public Rect getIconLayoutBounds() {
278         return mIconLayoutBounds;
279     }
280 
281     /**
282      * Returns the app icons currently shown in the taskbar.
283      */
getIconViews()284     public View[] getIconViews() {
285         final int count = getChildCount();
286         View[] icons = new View[count];
287         for (int i = 0; i < count; i++) {
288             icons[i] = getChildAt(i);
289         }
290         return icons;
291     }
292 
293     // FolderIconParent implemented methods.
294 
295     @Override
drawFolderLeaveBehindForIcon(FolderIcon child)296     public void drawFolderLeaveBehindForIcon(FolderIcon child) {
297         mLeaveBehindFolderIcon = child;
298         invalidate();
299     }
300 
301     @Override
clearFolderLeaveBehind(FolderIcon child)302     public void clearFolderLeaveBehind(FolderIcon child) {
303         mLeaveBehindFolderIcon = null;
304         invalidate();
305     }
306 
307     // End FolderIconParent implemented methods.
308 
309     @Override
onDraw(Canvas canvas)310     protected void onDraw(Canvas canvas) {
311         super.onDraw(canvas);
312         if (mLeaveBehindFolderIcon != null) {
313             canvas.save();
314             canvas.translate(mLeaveBehindFolderIcon.getLeft(), mLeaveBehindFolderIcon.getTop());
315             mLeaveBehindFolderIcon.getFolderBackground().drawLeaveBehind(canvas);
316             canvas.restore();
317         }
318     }
319 
inflate(@ayoutRes int layoutResId)320     private View inflate(@LayoutRes int layoutResId) {
321         return mActivityContext.getViewCache().getView(layoutResId, mActivityContext, this);
322     }
323 
324     @Override
setInsets(Rect insets)325     public void setInsets(Rect insets) {
326         // Ignore, we just implement Insettable to draw behind system insets.
327     }
328 
areIconsVisible()329     public boolean areIconsVisible() {
330         // Consider the overall visibility
331         return getVisibility() == VISIBLE;
332     }
333 }
334