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