• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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.views;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ObjectAnimator;
23 import android.content.Context;
24 import android.content.res.Configuration;
25 import android.content.res.TypedArray;
26 import android.graphics.CornerPathEffect;
27 import android.graphics.Paint;
28 import android.graphics.Rect;
29 import android.graphics.drawable.ShapeDrawable;
30 import android.os.Handler;
31 import android.util.IntProperty;
32 import android.util.Log;
33 import android.view.ContextThemeWrapper;
34 import android.view.Gravity;
35 import android.view.MotionEvent;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.widget.LinearLayout;
39 import android.widget.TextView;
40 
41 import androidx.annotation.Nullable;
42 import androidx.annotation.Px;
43 
44 import com.android.app.animation.Interpolators;
45 import com.android.launcher3.AbstractFloatingView;
46 import com.android.launcher3.DeviceProfile;
47 import com.android.launcher3.R;
48 import com.android.launcher3.anim.AnimatorListeners;
49 import com.android.launcher3.dragndrop.DragLayer;
50 import com.android.launcher3.graphics.TriangleShape;
51 
52 /**
53  * A base class for arrow tip view in launcher.
54  */
55 public class ArrowTipView extends AbstractFloatingView {
56 
57     private static final String TAG = "ArrowTipView";
58     private static final long AUTO_CLOSE_TIMEOUT_MILLIS = 10 * 1000;
59     private static final long SHOW_DELAY_MS = 200;
60     private static final long SHOW_DURATION_MS = 300;
61     private static final long HIDE_DURATION_MS = 100;
62 
63     public static final IntProperty<ArrowTipView> TEXT_ALPHA =
64             new IntProperty<>("textAlpha") {
65                 @Override
66                 public void setValue(ArrowTipView view, int v) {
67                     view.setTextAlpha(v);
68                 }
69 
70                 @Override
71                 public Integer get(ArrowTipView view) {
72                     return view.getTextAlpha();
73                 }
74             };
75 
76     protected final ActivityContext mActivityContext;
77     private final Handler mHandler = new Handler();
78     private boolean mIsPointingUp;
79     private Runnable mOnClosed;
80     private View mArrowView;
81     private final int mArrowWidth;
82     private final int mArrowMinOffset;
83     private final int mArrowViewPaintColor;
84 
85     private AnimatorSet mOpenAnimator = new AnimatorSet();
86     private AnimatorSet mCloseAnimator = new AnimatorSet();
87 
88     private int mTextAlpha;
89 
ArrowTipView(Context context)90     public ArrowTipView(Context context) {
91         this(context, false);
92     }
93 
ArrowTipView(Context context, boolean isPointingUp)94     public ArrowTipView(Context context, boolean isPointingUp) {
95         this(context, isPointingUp, R.layout.arrow_toast);
96     }
97 
ArrowTipView(Context context, boolean isPointingUp, int layoutId)98     public ArrowTipView(Context context, boolean isPointingUp, int layoutId) {
99         super(context, null, 0);
100         mActivityContext = ActivityContext.lookupContext(context);
101         mIsPointingUp = isPointingUp;
102         mArrowWidth = context.getResources().getDimensionPixelSize(
103                 R.dimen.arrow_toast_arrow_width);
104         mArrowMinOffset = context.getResources().getDimensionPixelSize(
105                 R.dimen.dynamic_grid_cell_border_spacing);
106         Context localContext = context;
107         TypedArray ta = localContext.obtainStyledAttributes(R.styleable.ArrowTipView);
108         // Set style to default to avoid inflation issues with missing attributes.
109         if (!ta.hasValue(R.styleable.ArrowTipView_arrowTipBackground)
110                 || !ta.hasValue(R.styleable.ArrowTipView_arrowTipTextColor)) {
111             localContext = new ContextThemeWrapper(localContext, R.style.ArrowTipStyle);
112         }
113         mArrowViewPaintColor = applyArrowPaintColor(ta, localContext);
114         init(localContext, layoutId);
115     }
116 
applyArrowPaintColor(TypedArray typedArray, Context context)117     protected int applyArrowPaintColor(TypedArray typedArray, Context context) {
118         int arrowPaintColor = typedArray.getColor(R.styleable.ArrowTipView_arrowTipBackground,
119                 context.getColor(R.color.arrow_tip_view_bg));
120         typedArray.recycle();
121         return arrowPaintColor;
122     }
123 
getArrowId()124     protected int getArrowId() {
125         return R.id.arrow;
126     }
127 
128     @Override
onControllerInterceptTouchEvent(MotionEvent ev)129     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
130         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
131             close(true);
132             if (mActivityContext.getDragLayer().isEventOverView(this, ev)) {
133                 return true;
134             }
135         }
136         return false;
137     }
138 
139     @Override
handleClose(boolean animate)140     protected void handleClose(boolean animate) {
141         if (mOpenAnimator.isStarted()) {
142             mOpenAnimator.cancel();
143         }
144         if (mIsOpen) {
145             if (animate) {
146                 mCloseAnimator.addListener(AnimatorListeners.forSuccessCallback(
147                         () -> mActivityContext.getDragLayer().removeView(this)));
148                 mCloseAnimator.start();
149             } else {
150                 mCloseAnimator.cancel();
151                 mActivityContext.getDragLayer().removeView(this);
152             }
153             if (mOnClosed != null) mOnClosed.run();
154             mIsOpen = false;
155         }
156     }
157 
158     @Override
isOfType(int type)159     protected boolean isOfType(int type) {
160         return (type & TYPE_ON_BOARD_POPUP) != 0;
161     }
162 
init(Context context, int layoutId)163     private void init(Context context, int layoutId) {
164         inflate(context, layoutId, this);
165         setOrientation(LinearLayout.VERTICAL);
166 
167         mArrowView = findViewById(getArrowId());
168         updateArrowTipInView(mIsPointingUp);
169         setAlpha(0);
170 
171         // Create default open animator.
172         mOpenAnimator.play(ObjectAnimator.ofFloat(this, ALPHA, 1f));
173         mOpenAnimator.setStartDelay(SHOW_DELAY_MS);
174         mOpenAnimator.setDuration(SHOW_DURATION_MS);
175         mOpenAnimator.setInterpolator(Interpolators.DECELERATE);
176 
177         // Create default close animator.
178         mCloseAnimator.play(ObjectAnimator.ofFloat(this, ALPHA, 0));
179         mCloseAnimator.setStartDelay(0);
180         mCloseAnimator.setDuration(HIDE_DURATION_MS);
181         mCloseAnimator.setInterpolator(Interpolators.ACCELERATE);
182         mCloseAnimator.addListener(new AnimatorListenerAdapter() {
183             @Override
184             public void onAnimationEnd(Animator animation) {
185                 mActivityContext.getDragLayer().removeView(ArrowTipView.this);
186             }
187         });
188     }
189 
190     /**
191      * Show Tip with specified string and Y location
192      */
show(String text, int top)193     public ArrowTipView show(String text, int top) {
194         return show(text, Gravity.CENTER_HORIZONTAL, 0, top);
195     }
196 
197     /**
198      * Show the ArrowTipView (tooltip) center, start, or end aligned.
199      *
200      * @param text             The text to be shown in the tooltip.
201      * @param gravity          The gravity aligns the tooltip center, start, or end.
202      * @param arrowMarginStart The margin from start to place arrow (ignored if center)
203      * @param top              The Y coordinate of the bottom of tooltip.
204      * @return The tooltip.
205      */
show(String text, int gravity, int arrowMarginStart, int top)206     public ArrowTipView show(String text, int gravity, int arrowMarginStart, int top) {
207         return show(text, gravity, arrowMarginStart, top, true);
208     }
209 
210     /**
211      * Show the ArrowTipView (tooltip) center, start, or end aligned.
212      *
213      * @param text The text to be shown in the tooltip.
214      * @param gravity The gravity aligns the tooltip center, start, or end.
215      * @param arrowMarginStart The margin from start to place arrow (ignored if center)
216      * @param top  The Y coordinate of the bottom of tooltip.
217      * @param shouldAutoClose If Tooltip should be auto close.
218      * @return The tooltip.
219      */
show( String text, int gravity, int arrowMarginStart, int top, boolean shouldAutoClose)220     public ArrowTipView show(
221             String text, int gravity, int arrowMarginStart, int top, boolean shouldAutoClose) {
222         ((TextView) findViewById(R.id.text)).setText(text);
223         ViewGroup parent = mActivityContext.getDragLayer();
224         parent.addView(this);
225 
226         DeviceProfile grid = mActivityContext.getDeviceProfile();
227 
228         DragLayer.LayoutParams params = (DragLayer.LayoutParams) getLayoutParams();
229         params.gravity = gravity;
230         params.leftMargin = mArrowMinOffset + grid.getInsets().left;
231         params.rightMargin = mArrowMinOffset + grid.getInsets().right;
232         params.width = LayoutParams.MATCH_PARENT;
233         LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mArrowView.getLayoutParams();
234 
235         lp.gravity = gravity;
236 
237         if (parent.getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
238             arrowMarginStart = parent.getMeasuredWidth() - arrowMarginStart;
239         }
240         if (gravity == Gravity.END) {
241             lp.setMarginEnd(Math.max(mArrowMinOffset,
242                     parent.getMeasuredWidth() - params.rightMargin - arrowMarginStart
243                             - mArrowWidth / 2));
244         } else if (gravity == Gravity.START) {
245             lp.setMarginStart(Math.max(mArrowMinOffset,
246                     arrowMarginStart - params.leftMargin - mArrowWidth / 2));
247         }
248         requestLayout();
249         post(() -> setY(top - (mIsPointingUp ? 0 : getHeight())));
250 
251         mIsOpen = true;
252         if (shouldAutoClose) {
253             mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
254         }
255 
256         mOpenAnimator.start();
257         return this;
258     }
259 
260     /**
261      * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it
262      * cannot fit on screen in the requested orientation.
263      *
264      * @param text The text to be shown in the tooltip.
265      * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the
266      *                    center of tooltip unless the tooltip goes beyond screen margin.
267      * @param yCoord The Y coordinate of the pointed tip end of the tooltip.
268      * @return The tool tip view. {@code null} if the tip can not be shown.
269      */
showAtLocation(String text, @Px int arrowXCoord, @Px int yCoord)270     @Nullable public ArrowTipView showAtLocation(String text, @Px int arrowXCoord, @Px int yCoord) {
271         return showAtLocation(
272             text,
273             arrowXCoord,
274             /* yCoordDownPointingTip= */ yCoord,
275             /* yCoordUpPointingTip= */ yCoord,
276             /* shouldAutoClose= */ true);
277     }
278 
279     /**
280      * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it
281      * cannot fit on screen in the requested orientation.
282      *
283      * @param text The text to be shown in the tooltip.
284      * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the
285      *                    center of tooltip unless the tooltip goes beyond screen margin.
286      * @param yCoord The Y coordinate of the pointed tip end of the tooltip.
287      * @param shouldAutoClose If Tooltip should be auto close.
288      * @return The tool tip view. {@code null} if the tip can not be shown.
289      */
showAtLocation( String text, @Px int arrowXCoord, @Px int yCoord, boolean shouldAutoClose)290     @Nullable public ArrowTipView showAtLocation(
291             String text, @Px int arrowXCoord, @Px int yCoord, boolean shouldAutoClose) {
292         return showAtLocation(
293                 text,
294                 arrowXCoord,
295                 /* yCoordDownPointingTip= */ yCoord,
296                 /* yCoordUpPointingTip= */ yCoord,
297                 /* shouldAutoClose= */ shouldAutoClose);
298     }
299 
300     /**
301      * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it
302      * cannot fit on screen in the requested orientation.
303      *
304      * @param text The text to be shown in the tooltip.
305      * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the
306      *                    center of tooltip unless the tooltip goes beyond screen margin.
307      * @param rect The coordinates of the view which requests the tooltip to be shown.
308      * @param margin The margin between {@param rect} and the tooltip.
309      * @return The tool tip view. {@code null} if the tip can not be shown.
310      */
showAroundRect( String text, @Px int arrowXCoord, Rect rect, @Px int margin)311     @Nullable public ArrowTipView showAroundRect(
312             String text, @Px int arrowXCoord, Rect rect, @Px int margin) {
313         return showAtLocation(
314                 text,
315                 arrowXCoord,
316                 /* yCoordDownPointingTip= */ rect.top - margin,
317                 /* yCoordUpPointingTip= */ rect.bottom + margin,
318                 /* shouldAutoClose= */ true);
319     }
320 
321     /**
322      * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it
323      * cannot fit on screen in the requested orientation.
324      *
325      * @param text The text to be shown in the tooltip.
326      * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the
327      *                    center of tooltip unless the tooltip goes beyond screen margin.
328      * @param yCoordDownPointingTip The Y coordinate of the pointed tip end of the tooltip when the
329      *                              tooltip is placed pointing downwards.
330      * @param yCoordUpPointingTip The Y coordinate of the pointed tip end of the tooltip when the
331      *                            tooltip is placed pointing upwards.
332      * @param shouldAutoClose If Tooltip should be auto close.
333      * @return The tool tip view. {@code null} if the tip can not be shown.
334      */
showAtLocation(String text, @Px int arrowXCoord, @Px int yCoordDownPointingTip, @Px int yCoordUpPointingTip, boolean shouldAutoClose)335     @Nullable private ArrowTipView showAtLocation(String text, @Px int arrowXCoord,
336             @Px int yCoordDownPointingTip, @Px int yCoordUpPointingTip, boolean shouldAutoClose) {
337         ViewGroup parent = mActivityContext.getDragLayer();
338         @Px int parentViewWidth = parent.getWidth();
339         @Px int parentViewHeight = parent.getHeight();
340         @Px int maxTextViewWidth = getContext().getResources()
341                 .getDimensionPixelSize(R.dimen.widget_picker_education_tip_max_width);
342         @Px int minViewMargin = getContext().getResources()
343                 .getDimensionPixelSize(R.dimen.widget_picker_education_tip_min_margin);
344         if (parentViewWidth < maxTextViewWidth + 2 * minViewMargin) {
345             Log.w(TAG, "Cannot display tip on a small screen of size: " + parentViewWidth);
346             return null;
347         }
348 
349         TextView textView = findViewById(R.id.text);
350         textView.setText(text);
351         textView.setMaxWidth(maxTextViewWidth);
352         if (parent.indexOfChild(this) < 0) {
353             parent.addView(this);
354             requestLayout();
355         }
356         return showAtLocation(arrowXCoord, yCoordDownPointingTip, yCoordUpPointingTip,
357                 minViewMargin, parentViewWidth, parentViewHeight, shouldAutoClose);
358     }
359 
360     /**
361      * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it
362      * cannot fit on screen in the requested orientation.
363      *
364      * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the
365      *                    center of tooltip unless the tooltip goes beyond screen margin.
366      * @param yCoordDownPointingTip The Y coordinate of the pointed tip end of the tooltip when the
367      *                              tooltip is placed pointing downwards.
368      * @param yCoordUpPointingTip The Y coordinate of the pointed tip end of the tooltip when the
369      *                            tooltip is placed pointing upwards.
370      * @param minViewMargin The view margin in pixels from the tip end to the y coordinate.
371      * @param parentViewWidth The width in pixels of the parent view.
372      * @param parentViewHeight The height in pixels of the parent view.
373      * @param shouldAutoClose If Tooltip should be auto close.
374      * @return The tool tip view. {@code null} if the tip can not be shown.
375      */
showAtLocation( @x int arrowXCoord, @Px int yCoordDownPointingTip, @Px int yCoordUpPointingTip, @Px int minViewMargin, @Px int parentViewWidth, @Px int parentViewHeight, boolean shouldAutoClose)376     protected ArrowTipView showAtLocation(
377             @Px int arrowXCoord,
378             @Px int yCoordDownPointingTip,
379             @Px int yCoordUpPointingTip,
380             @Px int minViewMargin,
381             @Px int parentViewWidth,
382             @Px int parentViewHeight,
383             boolean shouldAutoClose) {
384 
385         post(() -> {
386             // Adjust the tooltip horizontally.
387             float halfWidth = getWidth() / 2f;
388             float xCoord;
389             if (arrowXCoord - halfWidth < minViewMargin) {
390                 // If the tooltip is estimated to go beyond the left margin, place its start just at
391                 // the left margin.
392                 xCoord = minViewMargin;
393             } else if (arrowXCoord + halfWidth > parentViewWidth - minViewMargin) {
394                 // If the tooltip is estimated to go beyond the right margin, place it such that its
395                 // end is just at the right margin.
396                 xCoord = parentViewWidth - minViewMargin - getWidth();
397             } else {
398                 // Place the tooltip such that its center is at arrowXCoord.
399                 xCoord = arrowXCoord - halfWidth;
400             }
401             setX(xCoord);
402 
403             // Adjust the tooltip vertically.
404             @Px int viewHeight = getHeight();
405             boolean isPointingUp = mIsPointingUp;
406             if (mIsPointingUp
407                     ? (yCoordUpPointingTip + viewHeight > parentViewHeight)
408                     : (yCoordDownPointingTip - viewHeight < 0)) {
409                 // Flip the view if it exceeds the vertical bounds of screen.
410                 isPointingUp = !mIsPointingUp;
411             }
412             updateArrowTipInView(isPointingUp);
413             // Place the tooltip such that its top is at yCoordUpPointingTip if arrow is displayed
414             // pointing upwards, otherwise place it such that its bottom is at
415             // yCoordDownPointingTip.
416             setY(isPointingUp ? yCoordUpPointingTip : yCoordDownPointingTip - viewHeight);
417 
418             // Adjust the arrow's relative position on tooltip to make sure the actual position of
419             // arrow's pointed tip is always at arrowXCoord.
420             mArrowView.setX(arrowXCoord - xCoord - mArrowView.getWidth() / 2f);
421             requestLayout();
422         });
423 
424         mIsOpen = true;
425         if (shouldAutoClose) {
426             mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
427         }
428 
429         mOpenAnimator.start();
430         return this;
431     }
432 
updateArrowTipInView(boolean isPointingUp)433     private void updateArrowTipInView(boolean isPointingUp) {
434         ViewGroup.LayoutParams arrowLp = mArrowView.getLayoutParams();
435         ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
436                 arrowLp.width, arrowLp.height, isPointingUp));
437         Paint arrowPaint = arrowDrawable.getPaint();
438         @Px int arrowTipRadius = getContext().getResources()
439                 .getDimensionPixelSize(R.dimen.arrow_toast_corner_radius);
440         arrowPaint.setColor(mArrowViewPaintColor);
441         arrowPaint.setPathEffect(new CornerPathEffect(arrowTipRadius));
442         mArrowView.setBackground(arrowDrawable);
443         // Add negative margin so that the rounded corners on base of arrow are not visible.
444         removeView(mArrowView);
445         if (isPointingUp) {
446             addView(mArrowView, 0);
447             ((ViewGroup.MarginLayoutParams) arrowLp).setMargins(0, 0, 0, -1 * arrowTipRadius);
448         } else {
449             addView(mArrowView, 1);
450             ((ViewGroup.MarginLayoutParams) arrowLp).setMargins(0, -1 * arrowTipRadius, 0, 0);
451         }
452     }
453 
454     /**
455      * Register a callback fired when toast is hidden
456      */
setOnClosedCallback(Runnable runnable)457     public ArrowTipView setOnClosedCallback(Runnable runnable) {
458         mOnClosed = runnable;
459         return this;
460     }
461 
462     @Override
onConfigurationChanged(Configuration newConfig)463     protected void onConfigurationChanged(Configuration newConfig) {
464         super.onConfigurationChanged(newConfig);
465         close(/* animate= */ false);
466     }
467 
468     /**
469      * Sets a custom animation to run on open of the ArrowTipView.
470      */
setCustomOpenAnimation(AnimatorSet animator)471     public void setCustomOpenAnimation(AnimatorSet animator) {
472         mOpenAnimator = animator;
473     }
474 
475     /**
476      * Sets a custom animation to run on close of the ArrowTipView.
477      */
setCustomCloseAnimation(AnimatorSet animator)478     public void setCustomCloseAnimation(AnimatorSet animator) {
479         mCloseAnimator = animator;
480     }
481 
setTextAlpha(int textAlpha)482     private void setTextAlpha(int textAlpha) {
483         if (mTextAlpha != textAlpha) {
484             mTextAlpha = textAlpha;
485             TextView textView = findViewById(R.id.text);
486             textView.setTextColor(textView.getTextColors().withAlpha(mTextAlpha));
487         }
488     }
489 
getTextAlpha()490     private int getTextAlpha() {
491         return mTextAlpha;
492     }
493 }
494