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