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.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 com.android.launcher3.anim.Interpolators;
37 import com.android.launcher3.dragndrop.DragController;
38 import com.android.launcher3.dragndrop.DragLayer;
39 import com.android.launcher3.dragndrop.DragOptions;
40 import com.android.launcher3.dragndrop.DragView;
41 import com.android.launcher3.model.data.ItemInfo;
42 
43 /**
44  * Implements a DropTarget.
45  */
46 public abstract class ButtonDropTarget extends TextView
47         implements DropTarget, DragController.DragListener, OnClickListener {
48 
49     private static final int[] sTempCords = new int[2];
50     private static final int DRAG_VIEW_DROP_DURATION = 285;
51     private static final float DRAG_VIEW_HOVER_OVER_OPACITY = 0.65f;
52 
53     public static final int TOOLTIP_DEFAULT = 0;
54     public static final int TOOLTIP_LEFT = 1;
55     public static final int TOOLTIP_RIGHT = 2;
56 
57     protected final Launcher mLauncher;
58 
59     protected DropTargetBar mDropTargetBar;
60 
61     /** Whether this drop target is active for the current drag */
62     protected boolean mActive;
63     /** Whether an accessible drag is in progress */
64     private boolean mAccessibleDrag;
65     /** An item must be dragged at least this many pixels before this drop target is enabled. */
66     private final int mDragDistanceThreshold;
67     /** The size of the drawable shown in the drop target. */
68     private final int mDrawableSize;
69     /** The padding, in pixels, between the text and drawable. */
70     private final int mDrawablePadding;
71 
72     protected CharSequence mText;
73     protected Drawable mDrawable;
74     private boolean mTextVisible = true;
75 
76     private PopupWindow mToolTip;
77     private int mToolTipLocation;
78 
ButtonDropTarget(Context context, AttributeSet attrs)79     public ButtonDropTarget(Context context, AttributeSet attrs) {
80         this(context, attrs, 0);
81     }
82 
ButtonDropTarget(Context context, AttributeSet attrs, int defStyle)83     public ButtonDropTarget(Context context, AttributeSet attrs, int defStyle) {
84         super(context, attrs, defStyle);
85         mLauncher = Launcher.getLauncher(context);
86 
87         Resources resources = getResources();
88         mDragDistanceThreshold = resources.getDimensionPixelSize(R.dimen.drag_distanceThreshold);
89         mDrawableSize = resources.getDimensionPixelSize(R.dimen.drop_target_text_size);
90         mDrawablePadding = resources.getDimensionPixelSize(
91                 R.dimen.drop_target_button_drawable_padding);
92     }
93 
94     @Override
onFinishInflate()95     protected void onFinishInflate() {
96         super.onFinishInflate();
97         mText = getText();
98         setContentDescription(mText);
99     }
100 
updateText(int resId)101     protected void updateText(int resId) {
102         setText(resId);
103         mText = getText();
104         setContentDescription(mText);
105     }
106 
setDrawable(int resId)107     protected void setDrawable(int resId) {
108         // We do not set the drawable in the xml as that inflates two drawables corresponding to
109         // drawableLeft and drawableStart.
110         mDrawable = getContext().getDrawable(resId).mutate();
111         mDrawable.setTintList(getTextColors());
112         centerIcon();
113         setCompoundDrawablesRelative(mDrawable, null, null, null);
114     }
115 
setDropTargetBar(DropTargetBar dropTargetBar)116     public void setDropTargetBar(DropTargetBar dropTargetBar) {
117         mDropTargetBar = dropTargetBar;
118     }
119 
hideTooltip()120     private void hideTooltip() {
121         if (mToolTip != null) {
122             mToolTip.dismiss();
123             mToolTip = null;
124         }
125     }
126 
127     @Override
onDragEnter(DragObject d)128     public final void onDragEnter(DragObject d) {
129         if (!mAccessibleDrag && !mTextVisible) {
130             // Show tooltip
131             hideTooltip();
132 
133             TextView message = (TextView) LayoutInflater.from(getContext()).inflate(
134                     R.layout.drop_target_tool_tip, null);
135             message.setText(mText);
136 
137             mToolTip = new PopupWindow(message, WRAP_CONTENT, WRAP_CONTENT);
138             int x = 0, y = 0;
139             if (mToolTipLocation != TOOLTIP_DEFAULT) {
140                 y = -getMeasuredHeight();
141                 message.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
142                 if (mToolTipLocation == TOOLTIP_LEFT) {
143                     x = - getMeasuredWidth() - message.getMeasuredWidth() / 2;
144                 } else {
145                     x = getMeasuredWidth() / 2 + message.getMeasuredWidth() / 2;
146                 }
147             }
148             mToolTip.showAsDropDown(this, x, y);
149         }
150 
151         d.dragView.setAlpha(DRAG_VIEW_HOVER_OVER_OPACITY);
152         setSelected(true);
153         if (d.stateAnnouncer != null) {
154             d.stateAnnouncer.cancel();
155         }
156         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
157     }
158 
159     @Override
onDragOver(DragObject d)160     public void onDragOver(DragObject d) {
161         // Do nothing
162     }
163 
164     @Override
onDragExit(DragObject d)165     public final void onDragExit(DragObject d) {
166         hideTooltip();
167 
168         if (!d.dragComplete) {
169             d.dragView.setAlpha(1f);
170             setSelected(false);
171         } else {
172             d.dragView.setAlpha(DRAG_VIEW_HOVER_OVER_OPACITY);
173         }
174     }
175 
176     @Override
onDragStart(DropTarget.DragObject dragObject, DragOptions options)177     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
178         mActive = !options.isKeyboardDrag && supportsDrop(dragObject.dragInfo);
179         setVisibility(mActive ? View.VISIBLE : View.GONE);
180 
181         mAccessibleDrag = options.isAccessibleDrag;
182         setOnClickListener(mAccessibleDrag ? this : null);
183     }
184 
185     @Override
acceptDrop(DragObject dragObject)186     public final boolean acceptDrop(DragObject dragObject) {
187         return supportsDrop(dragObject.dragInfo);
188     }
189 
supportsDrop(ItemInfo info)190     protected abstract boolean supportsDrop(ItemInfo info);
191 
supportsAccessibilityDrop(ItemInfo info, View view)192     public abstract boolean supportsAccessibilityDrop(ItemInfo info, View view);
193 
194     @Override
isDropEnabled()195     public boolean isDropEnabled() {
196         return mActive && (mAccessibleDrag ||
197                 mLauncher.getDragController().getDistanceDragged() >= mDragDistanceThreshold);
198     }
199 
200     @Override
onDragEnd()201     public void onDragEnd() {
202         mActive = false;
203         setOnClickListener(null);
204         setSelected(false);
205     }
206 
207     /**
208      * On drop animate the dropView to the icon.
209      */
210     @Override
onDrop(final DragObject d, final DragOptions options)211     public void onDrop(final DragObject d, final DragOptions options) {
212         if (options.isFlingToDelete) {
213             // FlingAnimation handles the animation and then calls completeDrop().
214             return;
215         }
216         final DragLayer dragLayer = mLauncher.getDragLayer();
217         final DragView dragView = d.dragView;
218         final Rect to = getIconRect(d);
219         final float scale = (float) to.width() / dragView.getMeasuredWidth();
220         dragView.detachContentView(/* reattachToPreviousParent= */ true);
221         mDropTargetBar.deferOnDragEnd();
222 
223         Runnable onAnimationEndRunnable = () -> {
224             completeDrop(d);
225             mDropTargetBar.onDragEnd();
226             mLauncher.getStateManager().goToState(NORMAL);
227         };
228 
229         dragLayer.animateView(d.dragView, to, scale, 0.1f, 0.1f,
230                 DRAG_VIEW_DROP_DURATION,
231                 Interpolators.DEACCEL_2, onAnimationEndRunnable,
232                 DragLayer.ANIMATION_END_DISAPPEAR, null);
233     }
234 
getAccessibilityAction()235     public abstract int getAccessibilityAction();
236 
237     @Override
prepareAccessibilityDrop()238     public void prepareAccessibilityDrop() { }
239 
onAccessibilityDrop(View view, ItemInfo item)240     public abstract void onAccessibilityDrop(View view, ItemInfo item);
241 
completeDrop(DragObject d)242     public abstract void completeDrop(DragObject d);
243 
244     @Override
getHitRectRelativeToDragLayer(android.graphics.Rect outRect)245     public void getHitRectRelativeToDragLayer(android.graphics.Rect outRect) {
246         super.getHitRect(outRect);
247         outRect.bottom += mLauncher.getDeviceProfile().dropTargetDragPaddingPx;
248 
249         sTempCords[0] = sTempCords[1] = 0;
250         mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, sTempCords);
251         outRect.offsetTo(sTempCords[0], sTempCords[1]);
252     }
253 
getIconRect(DragObject dragObject)254     public Rect getIconRect(DragObject dragObject) {
255         int viewWidth = dragObject.dragView.getMeasuredWidth();
256         int viewHeight = dragObject.dragView.getMeasuredHeight();
257         int drawableWidth = mDrawable.getIntrinsicWidth();
258         int drawableHeight = mDrawable.getIntrinsicHeight();
259         DragLayer dragLayer = mLauncher.getDragLayer();
260 
261         // Find the rect to animate to (the view is center aligned)
262         Rect to = new Rect();
263         dragLayer.getViewRectRelativeToSelf(this, to);
264 
265         final int width = drawableWidth;
266         final int height = drawableHeight;
267 
268         final int left;
269         final int right;
270 
271         if (Utilities.isRtl(getResources())) {
272             right = to.right - getPaddingRight();
273             left = right - width;
274         } else {
275             left = to.left + getPaddingLeft();
276             right = left + width;
277         }
278 
279         final int top = to.top + (getMeasuredHeight() - height) / 2;
280         final int bottom = top + height;
281 
282         to.set(left, top, right, bottom);
283 
284         // Center the destination rect about the trash icon
285         final int xOffset = -(viewWidth - width) / 2;
286         final int yOffset = -(viewHeight - height) / 2;
287         to.offset(xOffset, yOffset);
288 
289         return to;
290     }
291 
centerIcon()292     private void centerIcon() {
293         int x = mTextVisible ? 0
294                 : (getWidth() - getPaddingLeft() - getPaddingRight()) / 2 - mDrawableSize / 2;
295         mDrawable.setBounds(x, 0, x + mDrawableSize, mDrawableSize);
296     }
297 
298     @Override
onClick(View v)299     public void onClick(View v) {
300         mLauncher.getAccessibilityDelegate().handleAccessibleDrop(this, null, null);
301     }
302 
setTextVisible(boolean isVisible)303     public void setTextVisible(boolean isVisible) {
304         CharSequence newText = isVisible ? mText : "";
305         if (mTextVisible != isVisible || !TextUtils.equals(newText, getText())) {
306             mTextVisible = isVisible;
307             setText(newText);
308             centerIcon();
309             setCompoundDrawablesRelative(mDrawable, null, null, null);
310             int drawablePadding = mTextVisible ? mDrawablePadding : 0;
311             setCompoundDrawablePadding(drawablePadding);
312         }
313     }
314 
315     @Override
onSizeChanged(int w, int h, int oldw, int oldh)316     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
317         super.onSizeChanged(w, h, oldw, oldh);
318         centerIcon();
319     }
320 
setToolTipLocation(int location)321     public void setToolTipLocation(int location) {
322         mToolTipLocation = location;
323         hideTooltip();
324     }
325 
isTextTruncated(int availableWidth)326     public boolean isTextTruncated(int availableWidth) {
327         availableWidth -= (getPaddingLeft() + getPaddingRight() + mDrawable.getIntrinsicWidth()
328                 + getCompoundDrawablePadding());
329         CharSequence displayedText = TextUtils.ellipsize(mText, getPaint(), availableWidth,
330                 TextUtils.TruncateAt.END);
331         return !mText.equals(displayedText);
332     }
333 }
334