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.animation.AnimatorSet;
24 import android.animation.FloatArrayEvaluator;
25 import android.animation.ObjectAnimator;
26 import android.animation.ValueAnimator;
27 import android.content.Context;
28 import android.content.res.ColorStateList;
29 import android.content.res.Resources;
30 import android.graphics.ColorMatrix;
31 import android.graphics.ColorMatrixColorFilter;
32 import android.graphics.Rect;
33 import android.graphics.drawable.Drawable;
34 import android.text.TextUtils;
35 import android.util.AttributeSet;
36 import android.util.Property;
37 import android.view.LayoutInflater;
38 import android.view.View;
39 import android.view.View.OnClickListener;
40 import android.view.accessibility.AccessibilityEvent;
41 import android.widget.PopupWindow;
42 import android.widget.TextView;
43 
44 import com.android.launcher3.anim.Interpolators;
45 import com.android.launcher3.dragndrop.DragController;
46 import com.android.launcher3.dragndrop.DragLayer;
47 import com.android.launcher3.dragndrop.DragOptions;
48 import com.android.launcher3.dragndrop.DragView;
49 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
50 import com.android.launcher3.util.Themes;
51 import com.android.launcher3.util.Thunk;
52 
53 /**
54  * Implements a DropTarget.
55  */
56 public abstract class ButtonDropTarget extends TextView
57         implements DropTarget, DragController.DragListener, OnClickListener {
58 
59     private static final Property<ButtonDropTarget, Integer> TEXT_COLOR =
60             new Property<ButtonDropTarget, Integer>(Integer.TYPE, "textColor") {
61 
62                 @Override
63                 public Integer get(ButtonDropTarget target) {
64                     return target.getTextColor();
65                 }
66 
67                 @Override
68                 public void set(ButtonDropTarget target, Integer value) {
69                     target.setTextColor(value);
70                 }
71             };
72 
73     private static final int[] sTempCords = new int[2];
74     private static final int DRAG_VIEW_DROP_DURATION = 285;
75 
76     public static final int TOOLTIP_DEFAULT = 0;
77     public static final int TOOLTIP_LEFT = 1;
78     public static final int TOOLTIP_RIGHT = 2;
79 
80     protected final Launcher mLauncher;
81 
82     private int mBottomDragPadding;
83     protected DropTargetBar mDropTargetBar;
84 
85     /** Whether this drop target is active for the current drag */
86     protected boolean mActive;
87     /** Whether an accessible drag is in progress */
88     private boolean mAccessibleDrag;
89     /** An item must be dragged at least this many pixels before this drop target is enabled. */
90     private final int mDragDistanceThreshold;
91 
92     /** The paint applied to the drag view on hover */
93     protected int mHoverColor = 0;
94 
95     protected CharSequence mText;
96     protected ColorStateList mOriginalTextColor;
97     protected Drawable mDrawable;
98     private boolean mTextVisible = true;
99 
100     private PopupWindow mToolTip;
101     private int mToolTipLocation;
102 
103     private AnimatorSet mCurrentColorAnim;
104     @Thunk ColorMatrix mSrcFilter, mDstFilter, mCurrentFilter;
105 
ButtonDropTarget(Context context, AttributeSet attrs)106     public ButtonDropTarget(Context context, AttributeSet attrs) {
107         this(context, attrs, 0);
108     }
109 
ButtonDropTarget(Context context, AttributeSet attrs, int defStyle)110     public ButtonDropTarget(Context context, AttributeSet attrs, int defStyle) {
111         super(context, attrs, defStyle);
112         mLauncher = Launcher.getLauncher(context);
113 
114         Resources resources = getResources();
115         mBottomDragPadding = resources.getDimensionPixelSize(R.dimen.drop_target_drag_padding);
116         mDragDistanceThreshold = resources.getDimensionPixelSize(R.dimen.drag_distanceThreshold);
117     }
118 
119     @Override
onFinishInflate()120     protected void onFinishInflate() {
121         super.onFinishInflate();
122         mText = getText();
123         mOriginalTextColor = getTextColors();
124         setContentDescription(mText);
125     }
126 
updateText(int resId)127     protected void updateText(int resId) {
128         setText(resId);
129         mText = getText();
130         setContentDescription(mText);
131     }
132 
setDrawable(int resId)133     protected void setDrawable(int resId) {
134         // We do not set the drawable in the xml as that inflates two drawables corresponding to
135         // drawableLeft and drawableStart.
136         if (mTextVisible) {
137             setCompoundDrawablesRelativeWithIntrinsicBounds(resId, 0, 0, 0);
138             mDrawable = getCompoundDrawablesRelative()[0];
139         } else {
140             setCompoundDrawablesRelativeWithIntrinsicBounds(0, resId, 0, 0);
141             mDrawable = getCompoundDrawablesRelative()[1];
142         }
143     }
144 
setDropTargetBar(DropTargetBar dropTargetBar)145     public void setDropTargetBar(DropTargetBar dropTargetBar) {
146         mDropTargetBar = dropTargetBar;
147     }
148 
hideTooltip()149     private void hideTooltip() {
150         if (mToolTip != null) {
151             mToolTip.dismiss();
152             mToolTip = null;
153         }
154     }
155 
156     @Override
onDragEnter(DragObject d)157     public final void onDragEnter(DragObject d) {
158         if (!d.accessibleDrag && !mTextVisible) {
159             // Show tooltip
160             hideTooltip();
161 
162             TextView message = (TextView) LayoutInflater.from(getContext()).inflate(
163                     R.layout.drop_target_tool_tip, null);
164             message.setText(mText);
165 
166             mToolTip = new PopupWindow(message, WRAP_CONTENT, WRAP_CONTENT);
167             int x = 0, y = 0;
168             if (mToolTipLocation != TOOLTIP_DEFAULT) {
169                 y = -getMeasuredHeight();
170                 message.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
171                 if (mToolTipLocation == TOOLTIP_LEFT) {
172                     x = - getMeasuredWidth() - message.getMeasuredWidth() / 2;
173                 } else {
174                     x = getMeasuredWidth() / 2 + message.getMeasuredWidth() / 2;
175                 }
176             }
177             mToolTip.showAsDropDown(this, x, y);
178         }
179 
180         d.dragView.setColor(mHoverColor);
181         animateTextColor(mHoverColor);
182         if (d.stateAnnouncer != null) {
183             d.stateAnnouncer.cancel();
184         }
185         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
186     }
187 
188     @Override
onDragOver(DragObject d)189     public void onDragOver(DragObject d) {
190         // Do nothing
191     }
192 
resetHoverColor()193     protected void resetHoverColor() {
194         animateTextColor(mOriginalTextColor.getDefaultColor());
195     }
196 
animateTextColor(int targetColor)197     private void animateTextColor(int targetColor) {
198         if (mCurrentColorAnim != null) {
199             mCurrentColorAnim.cancel();
200         }
201 
202         mCurrentColorAnim = new AnimatorSet();
203         mCurrentColorAnim.setDuration(DragView.COLOR_CHANGE_DURATION);
204 
205         if (mSrcFilter == null) {
206             mSrcFilter = new ColorMatrix();
207             mDstFilter = new ColorMatrix();
208             mCurrentFilter = new ColorMatrix();
209         }
210 
211         int defaultTextColor = mOriginalTextColor.getDefaultColor();
212         Themes.setColorChangeOnMatrix(defaultTextColor, getTextColor(), mSrcFilter);
213         Themes.setColorChangeOnMatrix(defaultTextColor, targetColor, mDstFilter);
214 
215         ValueAnimator anim1 = ValueAnimator.ofObject(
216                 new FloatArrayEvaluator(mCurrentFilter.getArray()),
217                 mSrcFilter.getArray(), mDstFilter.getArray());
218         anim1.addUpdateListener((anim) -> {
219             mDrawable.setColorFilter(new ColorMatrixColorFilter(mCurrentFilter));
220             invalidate();
221         });
222 
223         mCurrentColorAnim.play(anim1);
224         mCurrentColorAnim.play(ObjectAnimator.ofArgb(this, TEXT_COLOR, targetColor));
225         mCurrentColorAnim.start();
226     }
227 
228     @Override
onDragExit(DragObject d)229     public final void onDragExit(DragObject d) {
230         hideTooltip();
231 
232         if (!d.dragComplete) {
233             d.dragView.setColor(0);
234             resetHoverColor();
235         } else {
236             // Restore the hover color
237             d.dragView.setColor(mHoverColor);
238         }
239     }
240 
241     @Override
onDragStart(DropTarget.DragObject dragObject, DragOptions options)242     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
243         mActive = supportsDrop(dragObject.dragInfo);
244         mDrawable.setColorFilter(null);
245         if (mCurrentColorAnim != null) {
246             mCurrentColorAnim.cancel();
247             mCurrentColorAnim = null;
248         }
249         setTextColor(mOriginalTextColor);
250         setVisibility(mActive ? View.VISIBLE : View.GONE);
251 
252         mAccessibleDrag = options.isAccessibleDrag;
253         setOnClickListener(mAccessibleDrag ? this : null);
254     }
255 
256     @Override
acceptDrop(DragObject dragObject)257     public final boolean acceptDrop(DragObject dragObject) {
258         return supportsDrop(dragObject.dragInfo);
259     }
260 
supportsDrop(ItemInfo info)261     protected abstract boolean supportsDrop(ItemInfo info);
262 
supportsAccessibilityDrop(ItemInfo info, View view)263     public abstract boolean supportsAccessibilityDrop(ItemInfo info, View view);
264 
265     @Override
isDropEnabled()266     public boolean isDropEnabled() {
267         return mActive && (mAccessibleDrag ||
268                 mLauncher.getDragController().getDistanceDragged() >= mDragDistanceThreshold);
269     }
270 
271     @Override
onDragEnd()272     public void onDragEnd() {
273         mActive = false;
274         setOnClickListener(null);
275     }
276 
277     /**
278      * On drop animate the dropView to the icon.
279      */
280     @Override
onDrop(final DragObject d, final DragOptions options)281     public void onDrop(final DragObject d, final DragOptions options) {
282         if (options.isFlingToDelete) {
283             // FlingAnimation handles the animation and then calls completeDrop().
284             return;
285         }
286         final DragLayer dragLayer = mLauncher.getDragLayer();
287         final Rect from = new Rect();
288         dragLayer.getViewRectRelativeToSelf(d.dragView, from);
289 
290         final Rect to = getIconRect(d);
291         final float scale = (float) to.width() / from.width();
292         mDropTargetBar.deferOnDragEnd();
293 
294         Runnable onAnimationEndRunnable = () -> {
295             completeDrop(d);
296             mDropTargetBar.onDragEnd();
297             mLauncher.getStateManager().goToState(NORMAL);
298         };
299 
300         dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f,
301                 DRAG_VIEW_DROP_DURATION,
302                 Interpolators.DEACCEL_2, Interpolators.LINEAR, onAnimationEndRunnable,
303                 DragLayer.ANIMATION_END_DISAPPEAR, null);
304     }
305 
getAccessibilityAction()306     public abstract int getAccessibilityAction();
307 
308     @Override
prepareAccessibilityDrop()309     public void prepareAccessibilityDrop() { }
310 
onAccessibilityDrop(View view, ItemInfo item)311     public abstract void onAccessibilityDrop(View view, ItemInfo item);
312 
completeDrop(DragObject d)313     public abstract void completeDrop(DragObject d);
314 
315     @Override
getHitRectRelativeToDragLayer(android.graphics.Rect outRect)316     public void getHitRectRelativeToDragLayer(android.graphics.Rect outRect) {
317         super.getHitRect(outRect);
318         outRect.bottom += mBottomDragPadding;
319 
320         sTempCords[0] = sTempCords[1] = 0;
321         mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, sTempCords);
322         outRect.offsetTo(sTempCords[0], sTempCords[1]);
323     }
324 
getIconRect(DragObject dragObject)325     public Rect getIconRect(DragObject dragObject) {
326         int viewWidth = dragObject.dragView.getMeasuredWidth();
327         int viewHeight = dragObject.dragView.getMeasuredHeight();
328         int drawableWidth = mDrawable.getIntrinsicWidth();
329         int drawableHeight = mDrawable.getIntrinsicHeight();
330         DragLayer dragLayer = mLauncher.getDragLayer();
331 
332         // Find the rect to animate to (the view is center aligned)
333         Rect to = new Rect();
334         dragLayer.getViewRectRelativeToSelf(this, to);
335 
336         final int width = drawableWidth;
337         final int height = drawableHeight;
338 
339         final int left;
340         final int right;
341 
342         if (Utilities.isRtl(getResources())) {
343             right = to.right - getPaddingRight();
344             left = right - width;
345         } else {
346             left = to.left + getPaddingLeft();
347             right = left + width;
348         }
349 
350         final int top = to.top + (getMeasuredHeight() - height) / 2;
351         final int bottom = top +  height;
352 
353         to.set(left, top, right, bottom);
354 
355         // Center the destination rect about the trash icon
356         final int xOffset = -(viewWidth - width) / 2;
357         final int yOffset = -(viewHeight - height) / 2;
358         to.offset(xOffset, yOffset);
359 
360         return to;
361     }
362 
363     @Override
onClick(View v)364     public void onClick(View v) {
365         mLauncher.getAccessibilityDelegate().handleAccessibleDrop(this, null, null);
366     }
367 
getTextColor()368     public int getTextColor() {
369         return getTextColors().getDefaultColor();
370     }
371 
setTextVisible(boolean isVisible)372     public void setTextVisible(boolean isVisible) {
373         CharSequence newText = isVisible ? mText : "";
374         if (mTextVisible != isVisible || !TextUtils.equals(newText, getText())) {
375             mTextVisible = isVisible;
376             setText(newText);
377             if (mTextVisible) {
378                 setCompoundDrawablesRelativeWithIntrinsicBounds(mDrawable, null, null, null);
379             } else {
380                 setCompoundDrawablesRelativeWithIntrinsicBounds(null, mDrawable, null, null);
381             }
382         }
383     }
384 
setToolTipLocation(int location)385     public void setToolTipLocation(int location) {
386         mToolTipLocation = location;
387         hideTooltip();
388     }
389 
isTextTruncated(int availableWidth)390     public boolean isTextTruncated(int availableWidth) {
391         availableWidth -= (getPaddingLeft() + getPaddingRight() + mDrawable.getIntrinsicWidth()
392                 + getCompoundDrawablePadding());
393         CharSequence displayedText = TextUtils.ellipsize(mText, getPaint(), availableWidth,
394                 TextUtils.TruncateAt.END);
395         return !mText.equals(displayedText);
396     }
397 
getDropTargetForLogging()398     public abstract Target getDropTargetForLogging();
399 }
400