1 /* 2 * Copyright (C) 2010 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 android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 20 21 import static com.android.launcher3.LauncherState.NORMAL; 22 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.graphics.Rect; 26 import android.graphics.drawable.Drawable; 27 import android.text.InputType; 28 import android.text.TextUtils; 29 import android.util.AttributeSet; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.View.OnClickListener; 33 import android.view.accessibility.AccessibilityEvent; 34 import android.widget.PopupWindow; 35 import android.widget.TextView; 36 37 import com.android.launcher3.anim.Interpolators; 38 import com.android.launcher3.dragndrop.DragController; 39 import com.android.launcher3.dragndrop.DragLayer; 40 import com.android.launcher3.dragndrop.DragOptions; 41 import com.android.launcher3.dragndrop.DragView; 42 import com.android.launcher3.model.data.ItemInfo; 43 44 /** 45 * Implements a DropTarget. 46 */ 47 public abstract class ButtonDropTarget extends TextView 48 implements DropTarget, DragController.DragListener, OnClickListener { 49 50 private static final int[] sTempCords = new int[2]; 51 private static final int DRAG_VIEW_DROP_DURATION = 285; 52 private static final float DRAG_VIEW_HOVER_OVER_OPACITY = 0.65f; 53 private static final int MAX_LINES_TEXT_MULTI_LINE = 2; 54 private static final int MAX_LINES_TEXT_SINGLE_LINE = 1; 55 56 public static final int TOOLTIP_DEFAULT = 0; 57 public static final int TOOLTIP_LEFT = 1; 58 public static final int TOOLTIP_RIGHT = 2; 59 60 private final Rect mTempRect = new Rect(); 61 62 protected final Launcher mLauncher; 63 64 protected DropTargetBar mDropTargetBar; 65 66 /** Whether this drop target is active for the current drag */ 67 protected boolean mActive; 68 /** Whether an accessible drag is in progress */ 69 private boolean mAccessibleDrag; 70 /** An item must be dragged at least this many pixels before this drop target is enabled. */ 71 private final int mDragDistanceThreshold; 72 /** The size of the drawable shown in the drop target. */ 73 private final int mDrawableSize; 74 /** The padding, in pixels, between the text and drawable. */ 75 private final int mDrawablePadding; 76 77 protected CharSequence mText; 78 protected Drawable mDrawable; 79 private boolean mTextVisible = true; 80 private boolean mIconVisible = true; 81 private boolean mTextMultiLine = true; 82 83 private PopupWindow mToolTip; 84 private int mToolTipLocation; 85 ButtonDropTarget(Context context, AttributeSet attrs)86 public ButtonDropTarget(Context context, AttributeSet attrs) { 87 this(context, attrs, 0); 88 } 89 ButtonDropTarget(Context context, AttributeSet attrs, int defStyle)90 public ButtonDropTarget(Context context, AttributeSet attrs, int defStyle) { 91 super(context, attrs, defStyle); 92 mLauncher = Launcher.getLauncher(context); 93 94 Resources resources = getResources(); 95 mDragDistanceThreshold = resources.getDimensionPixelSize(R.dimen.drag_distanceThreshold); 96 mDrawableSize = resources.getDimensionPixelSize(R.dimen.drop_target_button_drawable_size); 97 mDrawablePadding = resources.getDimensionPixelSize( 98 R.dimen.drop_target_button_drawable_padding); 99 } 100 101 @Override onFinishInflate()102 protected void onFinishInflate() { 103 super.onFinishInflate(); 104 mText = getText(); 105 setContentDescription(mText); 106 } 107 updateText(int resId)108 protected void updateText(int resId) { 109 setText(resId); 110 mText = getText(); 111 setContentDescription(mText); 112 } 113 setDrawable(int resId)114 protected void setDrawable(int resId) { 115 // We do not set the drawable in the xml as that inflates two drawables corresponding to 116 // drawableLeft and drawableStart. 117 mDrawable = getContext().getDrawable(resId).mutate(); 118 mDrawable.setTintList(getTextColors()); 119 updateIconVisibility(); 120 } 121 setDropTargetBar(DropTargetBar dropTargetBar)122 public void setDropTargetBar(DropTargetBar dropTargetBar) { 123 mDropTargetBar = dropTargetBar; 124 } 125 hideTooltip()126 private void hideTooltip() { 127 if (mToolTip != null) { 128 mToolTip.dismiss(); 129 mToolTip = null; 130 } 131 } 132 133 @Override onDragEnter(DragObject d)134 public final void onDragEnter(DragObject d) { 135 if (!mAccessibleDrag && !mTextVisible) { 136 // Show tooltip 137 hideTooltip(); 138 139 TextView message = (TextView) LayoutInflater.from(getContext()).inflate( 140 R.layout.drop_target_tool_tip, null); 141 message.setText(mText); 142 143 mToolTip = new PopupWindow(message, WRAP_CONTENT, WRAP_CONTENT); 144 int x = 0, y = 0; 145 if (mToolTipLocation != TOOLTIP_DEFAULT) { 146 y = -getMeasuredHeight(); 147 message.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 148 if (mToolTipLocation == TOOLTIP_LEFT) { 149 x = -getMeasuredWidth() - message.getMeasuredWidth() / 2; 150 } else { 151 x = getMeasuredWidth() / 2 + message.getMeasuredWidth() / 2; 152 } 153 } 154 mToolTip.showAsDropDown(this, x, y); 155 } 156 157 d.dragView.setAlpha(DRAG_VIEW_HOVER_OVER_OPACITY); 158 setSelected(true); 159 if (d.stateAnnouncer != null) { 160 d.stateAnnouncer.cancel(); 161 } 162 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 163 } 164 165 @Override onDragOver(DragObject d)166 public void onDragOver(DragObject d) { 167 // Do nothing 168 } 169 170 @Override onDragExit(DragObject d)171 public final void onDragExit(DragObject d) { 172 hideTooltip(); 173 174 if (!d.dragComplete) { 175 d.dragView.setAlpha(1f); 176 setSelected(false); 177 } else { 178 d.dragView.setAlpha(DRAG_VIEW_HOVER_OVER_OPACITY); 179 } 180 } 181 182 @Override onDragStart(DropTarget.DragObject dragObject, DragOptions options)183 public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { 184 if (options.isKeyboardDrag) { 185 mActive = false; 186 } else { 187 setupItemInfo(dragObject.dragInfo); 188 mActive = supportsDrop(dragObject.dragInfo); 189 } 190 setVisibility(mActive ? View.VISIBLE : View.GONE); 191 192 mAccessibleDrag = options.isAccessibleDrag; 193 setOnClickListener(mAccessibleDrag ? this : null); 194 } 195 196 @Override acceptDrop(DragObject dragObject)197 public final boolean acceptDrop(DragObject dragObject) { 198 return supportsDrop(dragObject.dragInfo); 199 } 200 201 /** 202 * Setups button for the specified ItemInfo. 203 */ setupItemInfo(ItemInfo info)204 protected abstract void setupItemInfo(ItemInfo info); 205 supportsDrop(ItemInfo info)206 protected abstract boolean supportsDrop(ItemInfo info); 207 supportsAccessibilityDrop(ItemInfo info, View view)208 public abstract boolean supportsAccessibilityDrop(ItemInfo info, View view); 209 210 @Override isDropEnabled()211 public boolean isDropEnabled() { 212 return mActive && (mAccessibleDrag || 213 mLauncher.getDragController().getDistanceDragged() >= mDragDistanceThreshold); 214 } 215 216 @Override onDragEnd()217 public void onDragEnd() { 218 mActive = false; 219 setOnClickListener(null); 220 setSelected(false); 221 } 222 223 /** 224 * On drop animate the dropView to the icon. 225 */ 226 @Override onDrop(final DragObject d, final DragOptions options)227 public void onDrop(final DragObject d, final DragOptions options) { 228 if (options.isFlingToDelete) { 229 // FlingAnimation handles the animation and then calls completeDrop(). 230 return; 231 } 232 final DragLayer dragLayer = mLauncher.getDragLayer(); 233 final DragView dragView = d.dragView; 234 final Rect to = getIconRect(d); 235 final float scale = (float) to.width() / dragView.getMeasuredWidth(); 236 dragView.detachContentView(/* reattachToPreviousParent= */ true); 237 238 mDropTargetBar.deferOnDragEnd(); 239 240 Runnable onAnimationEndRunnable = () -> { 241 completeDrop(d); 242 mDropTargetBar.onDragEnd(); 243 mLauncher.getStateManager().goToState(NORMAL); 244 }; 245 246 dragLayer.animateView(d.dragView, to, scale, 0.1f, 0.1f, 247 DRAG_VIEW_DROP_DURATION, 248 Interpolators.DEACCEL_2, onAnimationEndRunnable, 249 DragLayer.ANIMATION_END_DISAPPEAR, null); 250 } 251 getAccessibilityAction()252 public abstract int getAccessibilityAction(); 253 254 @Override prepareAccessibilityDrop()255 public void prepareAccessibilityDrop() { } 256 onAccessibilityDrop(View view, ItemInfo item)257 public abstract void onAccessibilityDrop(View view, ItemInfo item); 258 completeDrop(DragObject d)259 public abstract void completeDrop(DragObject d); 260 261 @Override getHitRectRelativeToDragLayer(android.graphics.Rect outRect)262 public void getHitRectRelativeToDragLayer(android.graphics.Rect outRect) { 263 super.getHitRect(outRect); 264 outRect.bottom += mLauncher.getDeviceProfile().dropTargetDragPaddingPx; 265 266 sTempCords[0] = sTempCords[1] = 0; 267 mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, sTempCords); 268 outRect.offsetTo(sTempCords[0], sTempCords[1]); 269 } 270 getIconRect(DragObject dragObject)271 public Rect getIconRect(DragObject dragObject) { 272 int viewWidth = dragObject.dragView.getMeasuredWidth(); 273 int viewHeight = dragObject.dragView.getMeasuredHeight(); 274 int drawableWidth = mDrawable.getIntrinsicWidth(); 275 int drawableHeight = mDrawable.getIntrinsicHeight(); 276 DragLayer dragLayer = mLauncher.getDragLayer(); 277 278 // Find the rect to animate to (the view is center aligned) 279 Rect to = new Rect(); 280 dragLayer.getViewRectRelativeToSelf(this, to); 281 282 final int width = drawableWidth; 283 final int height = drawableHeight; 284 285 final int left; 286 final int right; 287 288 if (Utilities.isRtl(getResources())) { 289 right = to.right - getPaddingRight(); 290 left = right - width; 291 } else { 292 left = to.left + getPaddingLeft(); 293 right = left + width; 294 } 295 296 final int top = to.top + (getMeasuredHeight() - height) / 2; 297 final int bottom = top + height; 298 299 to.set(left, top, right, bottom); 300 301 // Center the destination rect about the trash icon 302 final int xOffset = -(viewWidth - width) / 2; 303 final int yOffset = -(viewHeight - height) / 2; 304 to.offset(xOffset, yOffset); 305 306 return to; 307 } 308 centerIcon()309 private void centerIcon() { 310 int x = mTextVisible ? 0 311 : (getWidth() - getPaddingLeft() - getPaddingRight()) / 2 - mDrawableSize / 2; 312 mDrawable.setBounds(x, 0, x + mDrawableSize, mDrawableSize); 313 } 314 315 @Override onClick(View v)316 public void onClick(View v) { 317 mLauncher.getAccessibilityDelegate().handleAccessibleDrop(this, null, null); 318 } 319 setTextVisible(boolean isVisible)320 public void setTextVisible(boolean isVisible) { 321 CharSequence newText = isVisible ? mText : ""; 322 if (mTextVisible != isVisible || !TextUtils.equals(newText, getText())) { 323 mTextVisible = isVisible; 324 setText(newText); 325 updateIconVisibility(); 326 } 327 } 328 329 /** 330 * Display button text over multiple lines when isMultiLine is true, single line otherwise. 331 */ setTextMultiLine(boolean isMultiLine)332 public void setTextMultiLine(boolean isMultiLine) { 333 if (mTextMultiLine != isMultiLine) { 334 mTextMultiLine = isMultiLine; 335 setSingleLine(!isMultiLine); 336 setMaxLines(isMultiLine ? MAX_LINES_TEXT_MULTI_LINE : MAX_LINES_TEXT_SINGLE_LINE); 337 int inputType = InputType.TYPE_CLASS_TEXT; 338 if (isMultiLine) { 339 inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE; 340 341 } 342 setInputType(inputType); 343 } 344 } 345 isTextMultiLine()346 protected boolean isTextMultiLine() { 347 return mTextMultiLine; 348 } 349 350 /** 351 * Sets the button icon visible when isVisible is true, hides it otherwise. 352 */ setIconVisible(boolean isVisible)353 public void setIconVisible(boolean isVisible) { 354 if (mIconVisible != isVisible) { 355 mIconVisible = isVisible; 356 updateIconVisibility(); 357 } 358 } 359 updateIconVisibility()360 private void updateIconVisibility() { 361 if (mIconVisible) { 362 centerIcon(); 363 } 364 setCompoundDrawablesRelative(mIconVisible ? mDrawable : null, null, null, null); 365 setCompoundDrawablePadding(mIconVisible && mTextVisible ? mDrawablePadding : 0); 366 } 367 368 @Override onSizeChanged(int w, int h, int oldw, int oldh)369 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 370 super.onSizeChanged(w, h, oldw, oldh); 371 centerIcon(); 372 } 373 setToolTipLocation(int location)374 public void setToolTipLocation(int location) { 375 mToolTipLocation = location; 376 hideTooltip(); 377 } 378 379 /** 380 * Returns if the text will be truncated within the provided availableWidth. 381 */ isTextTruncated(int availableWidth)382 public boolean isTextTruncated(int availableWidth) { 383 availableWidth -= getPaddingLeft() + getPaddingRight(); 384 if (mIconVisible) { 385 availableWidth -= mDrawable.getIntrinsicWidth() + getCompoundDrawablePadding(); 386 } 387 if (availableWidth <= 0) { 388 return true; 389 } 390 CharSequence firstLine = TextUtils.ellipsize(mText, getPaint(), availableWidth, 391 TextUtils.TruncateAt.END); 392 if (!mTextMultiLine) { 393 return !TextUtils.equals(mText, firstLine); 394 } 395 if (TextUtils.equals(mText, firstLine)) { 396 // When multi-line is active, if it can display as one line, then text is not truncated. 397 return false; 398 } 399 CharSequence secondLine = 400 TextUtils.ellipsize(mText.subSequence(firstLine.length(), mText.length()), 401 getPaint(), availableWidth, TextUtils.TruncateAt.END); 402 return !(TextUtils.equals(mText.subSequence(0, firstLine.length()), firstLine) 403 && TextUtils.equals(mText.subSequence(firstLine.length(), secondLine.length()), 404 secondLine)); 405 } 406 407 /** 408 * Returns if the text will be clipped vertically within the provided availableHeight. 409 */ isTextClippedVertically(int availableHeight)410 private boolean isTextClippedVertically(int availableHeight) { 411 availableHeight -= getPaddingTop() + getPaddingBottom(); 412 if (availableHeight <= 0) { 413 return true; 414 } 415 416 getPaint().getTextBounds(mText.toString(), 0, mText.length(), mTempRect); 417 // Add bounds bottom to height, as text bounds height measures from the text baseline and 418 // above, which characters can descend below 419 return mTempRect.bottom + mTempRect.height() >= availableHeight; 420 } 421 422 /** 423 * Reduce the size of the text until it fits the measured width or reaches a minimum. 424 * 425 * The minimum size is defined by {@code R.dimen.button_drop_target_min_text_size} and 426 * it diminishes by intervals defined by 427 * {@code R.dimen.button_drop_target_resize_text_increment} 428 * This functionality is very similar to the option 429 * {@link TextView#setAutoSizeTextTypeWithDefaults(int)} but can't be used in this view because 430 * the layout width is {@code WRAP_CONTENT}. 431 * 432 * @return The biggest text size in SP that makes the text fit or if the text can't fit returns 433 * the min available value 434 */ resizeTextToFit()435 public float resizeTextToFit() { 436 float minSize = Utilities.pxToSp(getResources() 437 .getDimensionPixelSize(R.dimen.button_drop_target_min_text_size)); 438 float step = Utilities.pxToSp(getResources() 439 .getDimensionPixelSize(R.dimen.button_drop_target_resize_text_increment)); 440 float textSize = Utilities.pxToSp(getTextSize()); 441 442 int availableWidth = getMeasuredWidth(); 443 int availableHeight = getMeasuredHeight(); 444 445 while (isTextTruncated(availableWidth) || isTextClippedVertically(availableHeight)) { 446 textSize -= step; 447 if (textSize < minSize) { 448 textSize = minSize; 449 setTextSize(textSize); 450 break; 451 } 452 setTextSize(textSize); 453 } 454 return textSize; 455 } 456 } 457