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