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