• 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 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