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.ButtonDropTarget.TOOLTIP_DEFAULT; 20 import static com.android.launcher3.anim.AlphaUpdateListener.updateVisibility; 21 import static com.android.launcher3.config.FeatureFlags.HOME_GARDENING_WORKSPACE_BUTTONS; 22 23 import android.animation.TimeInterpolator; 24 import android.content.Context; 25 import android.graphics.Rect; 26 import android.util.AttributeSet; 27 import android.util.Log; 28 import android.util.TypedValue; 29 import android.view.Gravity; 30 import android.view.View; 31 import android.view.ViewDebug; 32 import android.view.ViewPropertyAnimator; 33 import android.widget.FrameLayout; 34 35 import androidx.annotation.NonNull; 36 37 import com.android.launcher3.anim.Interpolators; 38 import com.android.launcher3.dragndrop.DragController; 39 import com.android.launcher3.dragndrop.DragController.DragListener; 40 import com.android.launcher3.dragndrop.DragOptions; 41 import com.android.launcher3.testing.shared.TestProtocol; 42 43 /* 44 * The top bar containing various drop targets: Delete/App Info/Uninstall. 45 */ 46 public class DropTargetBar extends FrameLayout 47 implements DragListener, Insettable { 48 49 protected static final int DEFAULT_DRAG_FADE_DURATION = 175; 50 protected static final TimeInterpolator DEFAULT_INTERPOLATOR = Interpolators.ACCEL; 51 52 private final Runnable mFadeAnimationEndRunnable = 53 () -> updateVisibility(DropTargetBar.this); 54 55 private final Launcher mLauncher; 56 57 @ViewDebug.ExportedProperty(category = "launcher") 58 protected boolean mDeferOnDragEnd; 59 60 @ViewDebug.ExportedProperty(category = "launcher") 61 protected boolean mVisible = false; 62 63 private ButtonDropTarget[] mDropTargets; 64 private ButtonDropTarget[] mTempTargets; 65 private ViewPropertyAnimator mCurrentAnimation; 66 67 private boolean mIsVertical = true; 68 DropTargetBar(Context context, AttributeSet attrs)69 public DropTargetBar(Context context, AttributeSet attrs) { 70 super(context, attrs); 71 mLauncher = Launcher.getLauncher(context); 72 } 73 DropTargetBar(Context context, AttributeSet attrs, int defStyle)74 public DropTargetBar(Context context, AttributeSet attrs, int defStyle) { 75 super(context, attrs, defStyle); 76 mLauncher = Launcher.getLauncher(context); 77 } 78 79 @Override onFinishInflate()80 protected void onFinishInflate() { 81 super.onFinishInflate(); 82 mDropTargets = new ButtonDropTarget[getChildCount()]; 83 for (int i = 0; i < mDropTargets.length; i++) { 84 mDropTargets[i] = (ButtonDropTarget) getChildAt(i); 85 mDropTargets[i].setDropTargetBar(this); 86 } 87 mTempTargets = new ButtonDropTarget[getChildCount()]; 88 } 89 90 @Override setInsets(Rect insets)91 public void setInsets(Rect insets) { 92 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); 93 DeviceProfile grid = mLauncher.getDeviceProfile(); 94 mIsVertical = grid.isVerticalBarLayout(); 95 96 lp.leftMargin = insets.left; 97 lp.topMargin = insets.top; 98 lp.bottomMargin = insets.bottom; 99 lp.rightMargin = insets.right; 100 int tooltipLocation = TOOLTIP_DEFAULT; 101 102 int horizontalMargin; 103 if (grid.isTablet) { 104 // XXX: If the icon size changes across orientations, we will have to take 105 // that into account here too. 106 horizontalMargin = ((grid.widthPx - 2 * grid.edgeMarginPx 107 - (grid.inv.numColumns * grid.cellWidthPx)) 108 / (2 * (grid.inv.numColumns + 1))) 109 + grid.edgeMarginPx; 110 } else { 111 horizontalMargin = getContext().getResources() 112 .getDimensionPixelSize(R.dimen.drop_target_bar_margin_horizontal); 113 } 114 lp.topMargin += grid.dropTargetBarTopMarginPx; 115 lp.bottomMargin += grid.dropTargetBarBottomMarginPx; 116 lp.width = grid.availableWidthPx - 2 * horizontalMargin; 117 if (mIsVertical) { 118 lp.leftMargin = (grid.widthPx - lp.width) / 2; 119 lp.rightMargin = (grid.widthPx - lp.width) / 2; 120 } 121 lp.height = grid.dropTargetBarSizePx; 122 // TODO: Add tablet support for DropTargetBar when HOME_GARDENING_WORKSPACE_BUTTONS flag 123 // is on 124 if (HOME_GARDENING_WORKSPACE_BUTTONS.get()) { 125 lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; 126 } else { 127 lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP; 128 } 129 130 DeviceProfile dp = mLauncher.getDeviceProfile(); 131 int horizontalPadding = dp.dropTargetHorizontalPaddingPx; 132 int verticalPadding = dp.dropTargetVerticalPaddingPx; 133 setLayoutParams(lp); 134 for (ButtonDropTarget button : mDropTargets) { 135 button.setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.dropTargetTextSizePx); 136 button.setToolTipLocation(tooltipLocation); 137 button.setPadding(horizontalPadding, verticalPadding, horizontalPadding, 138 verticalPadding); 139 } 140 } 141 setup(DragController dragController)142 public void setup(DragController dragController) { 143 dragController.addDragListener(this); 144 for (int i = 0; i < mDropTargets.length; i++) { 145 dragController.addDragListener(mDropTargets[i]); 146 dragController.addDropTarget(mDropTargets[i]); 147 } 148 } 149 150 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)151 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 152 int width = MeasureSpec.getSize(widthMeasureSpec); 153 int height = MeasureSpec.getSize(heightMeasureSpec); 154 int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); 155 156 int visibleCount = getVisibleButtons(mTempTargets); 157 if (visibleCount == 1) { 158 int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST); 159 160 ButtonDropTarget firstButton = mTempTargets[0]; 161 firstButton.setTextSize(TypedValue.COMPLEX_UNIT_PX, 162 mLauncher.getDeviceProfile().dropTargetTextSizePx); 163 firstButton.setTextVisible(true); 164 firstButton.setIconVisible(true); 165 firstButton.measure(widthSpec, heightSpec); 166 } else if (visibleCount == 2) { 167 DeviceProfile dp = mLauncher.getDeviceProfile(); 168 int verticalPadding = dp.dropTargetVerticalPaddingPx; 169 int horizontalPadding = dp.dropTargetHorizontalPaddingPx; 170 171 ButtonDropTarget firstButton = mTempTargets[0]; 172 firstButton.setTextSize(TypedValue.COMPLEX_UNIT_PX, dp.dropTargetTextSizePx); 173 firstButton.setTextVisible(true); 174 firstButton.setIconVisible(true); 175 firstButton.setTextMultiLine(false); 176 // Reset first button padding in case it was previously changed to multi-line text. 177 firstButton.setPadding(horizontalPadding, verticalPadding, horizontalPadding, 178 verticalPadding); 179 180 ButtonDropTarget secondButton = mTempTargets[1]; 181 secondButton.setTextSize(TypedValue.COMPLEX_UNIT_PX, dp.dropTargetTextSizePx); 182 secondButton.setTextVisible(true); 183 secondButton.setIconVisible(true); 184 secondButton.setTextMultiLine(false); 185 // Reset second button padding in case it was previously changed to multi-line text. 186 secondButton.setPadding(horizontalPadding, verticalPadding, horizontalPadding, 187 verticalPadding); 188 189 int availableWidth; 190 if (dp.isTwoPanels) { 191 // Each button for two panel fits to half the width of the screen excluding the 192 // center gap between the buttons. 193 availableWidth = (dp.availableWidthPx - dp.dropTargetGapPx) / 2; 194 } else { 195 // Both buttons plus the button gap do not display past the edge of the screen. 196 availableWidth = dp.availableWidthPx - dp.dropTargetGapPx; 197 } 198 199 int widthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST); 200 firstButton.measure(widthSpec, heightSpec); 201 if (!mIsVertical) { 202 // Remove both icons and put the button's text on two lines if text is truncated. 203 if (firstButton.isTextTruncated(availableWidth)) { 204 firstButton.setIconVisible(false); 205 secondButton.setIconVisible(false); 206 firstButton.setTextMultiLine(true); 207 firstButton.setPadding(horizontalPadding, verticalPadding / 2, 208 horizontalPadding, verticalPadding / 2); 209 } 210 } 211 212 if (!dp.isTwoPanels) { 213 availableWidth -= firstButton.getMeasuredWidth() + dp.dropTargetGapPx; 214 widthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST); 215 } 216 secondButton.measure(widthSpec, heightSpec); 217 if (!mIsVertical) { 218 // Remove both icons and put the button's text on two lines if text is truncated. 219 if (secondButton.isTextTruncated(availableWidth)) { 220 secondButton.setIconVisible(false); 221 firstButton.setIconVisible(false); 222 secondButton.setTextMultiLine(true); 223 secondButton.setPadding(horizontalPadding, verticalPadding / 2, 224 horizontalPadding, verticalPadding / 2); 225 } 226 } 227 228 // If text is still truncated, shrink to fit in measured width and resize both targets. 229 float minTextSize = 230 Math.min(firstButton.resizeTextToFit(), secondButton.resizeTextToFit()); 231 if (firstButton.getTextSize() != minTextSize 232 || secondButton.getTextSize() != minTextSize) { 233 firstButton.setTextSize(minTextSize); 234 secondButton.setTextSize(minTextSize); 235 } 236 } 237 setMeasuredDimension(width, height); 238 } 239 240 @Override onLayout(boolean changed, int left, int top, int right, int bottom)241 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 242 int visibleCount = getVisibleButtons(mTempTargets); 243 if (visibleCount == 0) { 244 return; 245 } 246 247 DeviceProfile dp = mLauncher.getDeviceProfile(); 248 // Center vertical bar over scaled workspace, accounting for hotseat offset. 249 float scale = dp.getWorkspaceSpringLoadScale(mLauncher); 250 Workspace<?> ws = mLauncher.getWorkspace(); 251 int barCenter; 252 if (dp.isTwoPanels) { 253 barCenter = (right - left) / 2; 254 } else { 255 int workspaceCenter = (ws.getLeft() + ws.getRight()) / 2; 256 int cellLayoutCenter = ((dp.getInsets().left + dp.workspacePadding.left) + (dp.widthPx 257 - dp.getInsets().right - dp.workspacePadding.right)) / 2; 258 int cellLayoutCenterOffset = (int) ((cellLayoutCenter - workspaceCenter) * scale); 259 barCenter = workspaceCenter + cellLayoutCenterOffset - left; 260 } 261 262 if (visibleCount == 1) { 263 ButtonDropTarget button = mTempTargets[0]; 264 button.layout(barCenter - (button.getMeasuredWidth() / 2), 0, 265 barCenter + (button.getMeasuredWidth() / 2), button.getMeasuredHeight()); 266 } else if (visibleCount == 2) { 267 int buttonGap = dp.dropTargetGapPx; 268 269 ButtonDropTarget leftButton = mTempTargets[0]; 270 ButtonDropTarget rightButton = mTempTargets[1]; 271 if (dp.isTwoPanels) { 272 leftButton.layout(barCenter - leftButton.getMeasuredWidth() - (buttonGap / 2), 0, 273 barCenter - (buttonGap / 2), leftButton.getMeasuredHeight()); 274 rightButton.layout(barCenter + (buttonGap / 2), 0, 275 barCenter + (buttonGap / 2) + rightButton.getMeasuredWidth(), 276 rightButton.getMeasuredHeight()); 277 } else { 278 int scaledPanelWidth = (int) (dp.getCellLayoutWidth() * scale); 279 280 int leftButtonWidth = leftButton.getMeasuredWidth(); 281 int rightButtonWidth = rightButton.getMeasuredWidth(); 282 int extraSpace = scaledPanelWidth - leftButtonWidth - rightButtonWidth - buttonGap; 283 284 int leftButtonStart = barCenter - (scaledPanelWidth / 2) + extraSpace / 2; 285 int leftButtonEnd = leftButtonStart + leftButtonWidth; 286 int rightButtonStart = leftButtonEnd + buttonGap; 287 int rightButtonEnd = rightButtonStart + rightButtonWidth; 288 289 leftButton.layout(leftButtonStart, 0, leftButtonEnd, 290 leftButton.getMeasuredHeight()); 291 rightButton.layout(rightButtonStart, 0, rightButtonEnd, 292 rightButton.getMeasuredHeight()); 293 } 294 } 295 } 296 getVisibleButtons(ButtonDropTarget[] outVisibleButtons)297 private int getVisibleButtons(ButtonDropTarget[] outVisibleButtons) { 298 int visibleCount = 0; 299 for (ButtonDropTarget button : mDropTargets) { 300 if (button.getVisibility() != GONE) { 301 outVisibleButtons[visibleCount] = button; 302 visibleCount++; 303 } 304 } 305 return visibleCount; 306 } 307 animateToVisibility(boolean isVisible)308 public void animateToVisibility(boolean isVisible) { 309 if (TestProtocol.sDebugTracing) { 310 Log.d(TestProtocol.NO_DROP_TARGET, "8"); 311 } 312 if (mVisible != isVisible) { 313 mVisible = isVisible; 314 315 // Cancel any existing animation 316 if (mCurrentAnimation != null) { 317 mCurrentAnimation.cancel(); 318 mCurrentAnimation = null; 319 } 320 321 float finalAlpha = mVisible ? 1 : 0; 322 if (Float.compare(getAlpha(), finalAlpha) != 0) { 323 setVisibility(View.VISIBLE); 324 mCurrentAnimation = animate().alpha(finalAlpha) 325 .setInterpolator(DEFAULT_INTERPOLATOR) 326 .setDuration(DEFAULT_DRAG_FADE_DURATION) 327 .withEndAction(mFadeAnimationEndRunnable); 328 } 329 330 } 331 } 332 333 /* 334 * DragController.DragListener implementation 335 */ 336 @Override onDragStart(DropTarget.DragObject dragObject, DragOptions options)337 public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { 338 if (TestProtocol.sDebugTracing) { 339 Log.d(TestProtocol.NO_DROP_TARGET, "7"); 340 } 341 animateToVisibility(true); 342 } 343 344 /** 345 * This is called to defer hiding the delete drop target until the drop animation has completed, 346 * instead of hiding immediately when the drag has ended. 347 */ deferOnDragEnd()348 protected void deferOnDragEnd() { 349 mDeferOnDragEnd = true; 350 } 351 352 @Override onDragEnd()353 public void onDragEnd() { 354 if (!mDeferOnDragEnd) { 355 animateToVisibility(false); 356 } else { 357 mDeferOnDragEnd = false; 358 } 359 } 360 getDropTargets()361 public ButtonDropTarget[] getDropTargets() { 362 return getVisibility() == View.VISIBLE ? mDropTargets : new ButtonDropTarget[0]; 363 } 364 365 @Override onVisibilityChanged(@onNull View changedView, int visibility)366 protected void onVisibilityChanged(@NonNull View changedView, int visibility) { 367 super.onVisibilityChanged(changedView, visibility); 368 if (TestProtocol.sDebugTracing) { 369 if (visibility == VISIBLE) { 370 Log.d(TestProtocol.NO_DROP_TARGET, "9"); 371 } else { 372 Log.d(TestProtocol.NO_DROP_TARGET, "Hiding drop target", new Exception()); 373 } 374 } 375 } 376 } 377