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