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