• 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.content.Context;
20 import android.content.res.Configuration;
21 import android.graphics.CornerPathEffect;
22 import android.graphics.Paint;
23 import android.graphics.Rect;
24 import android.graphics.drawable.ShapeDrawable;
25 import android.os.Handler;
26 import android.util.Log;
27 import android.view.Gravity;
28 import android.view.MotionEvent;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.widget.LinearLayout;
32 import android.widget.TextView;
33 
34 import androidx.annotation.Nullable;
35 import androidx.annotation.Px;
36 import androidx.core.content.ContextCompat;
37 
38 import com.android.launcher3.AbstractFloatingView;
39 import com.android.launcher3.BaseDraggingActivity;
40 import com.android.launcher3.R;
41 import com.android.launcher3.anim.Interpolators;
42 import com.android.launcher3.dragndrop.DragLayer;
43 import com.android.launcher3.graphics.TriangleShape;
44 
45 /**
46  * A base class for arrow tip view in launcher
47  */
48 public class ArrowTipView extends AbstractFloatingView {
49 
50     private static final String TAG = ArrowTipView.class.getSimpleName();
51     private static final long AUTO_CLOSE_TIMEOUT_MILLIS = 10 * 1000;
52     private static final long SHOW_DELAY_MS = 200;
53     private static final long SHOW_DURATION_MS = 300;
54     private static final long HIDE_DURATION_MS = 100;
55 
56     protected final BaseDraggingActivity mActivity;
57     private final Handler mHandler = new Handler();
58     private final int mArrowWidth;
59     private boolean mIsPointingUp;
60     private Runnable mOnClosed;
61     private View mArrowView;
62 
ArrowTipView(Context context)63     public ArrowTipView(Context context) {
64         this(context, false);
65     }
66 
ArrowTipView(Context context, boolean isPointingUp)67     public ArrowTipView(Context context, boolean isPointingUp) {
68         super(context, null, 0);
69         mActivity = BaseDraggingActivity.fromContext(context);
70         mIsPointingUp = isPointingUp;
71         mArrowWidth = context.getResources().getDimensionPixelSize(R.dimen.arrow_toast_arrow_width);
72         init(context);
73     }
74 
75     @Override
onControllerInterceptTouchEvent(MotionEvent ev)76     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
77         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
78             close(true);
79             if (mActivity.getDragLayer().isEventOverView(this, ev)) {
80                 return true;
81             }
82         }
83         return false;
84     }
85 
86     @Override
handleClose(boolean animate)87     protected void handleClose(boolean animate) {
88         if (mIsOpen) {
89             if (animate) {
90                 animate().alpha(0f)
91                         .withLayer()
92                         .setStartDelay(0)
93                         .setDuration(HIDE_DURATION_MS)
94                         .setInterpolator(Interpolators.ACCEL)
95                         .withEndAction(() -> mActivity.getDragLayer().removeView(this))
96                         .start();
97             } else {
98                 animate().cancel();
99                 mActivity.getDragLayer().removeView(this);
100             }
101             if (mOnClosed != null) mOnClosed.run();
102             mIsOpen = false;
103         }
104     }
105 
106     @Override
isOfType(int type)107     protected boolean isOfType(int type) {
108         return (type & TYPE_ON_BOARD_POPUP) != 0;
109     }
110 
init(Context context)111     private void init(Context context) {
112         inflate(context, R.layout.arrow_toast, this);
113         setOrientation(LinearLayout.VERTICAL);
114 
115         mArrowView = findViewById(R.id.arrow);
116         updateArrowTipInView();
117     }
118 
119     /**
120      * Show Tip with specified string and Y location
121      */
show(String text, int top)122     public ArrowTipView show(String text, int top) {
123         return show(text, Gravity.CENTER_HORIZONTAL, 0, top);
124     }
125 
126     /**
127      * Show the ArrowTipView (tooltip) center, start, or end aligned.
128      *
129      * @param text The text to be shown in the tooltip.
130      * @param gravity The gravity aligns the tooltip center, start, or end.
131      * @param arrowMarginStart The margin from start to place arrow (ignored if center)
132      * @param top The Y coordinate of the bottom of tooltip.
133      * @return The tooltip.
134      */
show(String text, int gravity, int arrowMarginStart, int top)135     public ArrowTipView show(String text, int gravity, int arrowMarginStart, int top) {
136         ((TextView) findViewById(R.id.text)).setText(text);
137         ViewGroup parent = mActivity.getDragLayer();
138         parent.addView(this);
139 
140         DragLayer.LayoutParams params = (DragLayer.LayoutParams) getLayoutParams();
141         params.gravity = gravity;
142         LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mArrowView.getLayoutParams();
143         lp.gravity = gravity;
144 
145         if (parent.getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
146             arrowMarginStart = parent.getMeasuredWidth() - arrowMarginStart;
147         }
148         if (gravity == Gravity.END) {
149             lp.setMarginEnd(parent.getMeasuredWidth() - arrowMarginStart - mArrowWidth);
150         } else if (gravity == Gravity.START) {
151             lp.setMarginStart(arrowMarginStart - mArrowWidth / 2);
152         }
153         requestLayout();
154 
155         params.leftMargin = mActivity.getDeviceProfile().workspacePadding.left;
156         params.rightMargin = mActivity.getDeviceProfile().workspacePadding.right;
157         post(() -> setY(top - (mIsPointingUp ? 0 : getHeight())));
158 
159         mIsOpen = true;
160         mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
161         setAlpha(0);
162         animate()
163                 .alpha(1f)
164                 .withLayer()
165                 .setStartDelay(SHOW_DELAY_MS)
166                 .setDuration(SHOW_DURATION_MS)
167                 .setInterpolator(Interpolators.DEACCEL)
168                 .start();
169         return this;
170     }
171 
172     /**
173      * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it
174      * cannot fit on screen in the requested orientation.
175      *
176      * @param text The text to be shown in the tooltip.
177      * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the
178      *                    center of tooltip unless the tooltip goes beyond screen margin.
179      * @param yCoord The Y coordinate of the pointed tip end of the tooltip.
180      * @return The tool tip view. {@code null} if the tip can not be shown.
181      */
showAtLocation(String text, @Px int arrowXCoord, @Px int yCoord)182     @Nullable public ArrowTipView showAtLocation(String text, @Px int arrowXCoord, @Px int yCoord) {
183         return showAtLocation(
184                 text,
185                 arrowXCoord,
186                 /* yCoordDownPointingTip= */ yCoord,
187                 /* yCoordUpPointingTip= */ yCoord);
188     }
189 
190     /**
191      * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it
192      * cannot fit on screen in the requested orientation.
193      *
194      * @param text The text to be shown in the tooltip.
195      * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the
196      *                    center of tooltip unless the tooltip goes beyond screen margin.
197      * @param rect The coordinates of the view which requests the tooltip to be shown.
198      * @param margin The margin between {@param rect} and the tooltip.
199      * @return The tool tip view. {@code null} if the tip can not be shown.
200      */
showAroundRect( String text, @Px int arrowXCoord, Rect rect, @Px int margin)201     @Nullable public ArrowTipView showAroundRect(
202             String text, @Px int arrowXCoord, Rect rect, @Px int margin) {
203         return showAtLocation(
204                 text,
205                 arrowXCoord,
206                 /* yCoordDownPointingTip= */ rect.top - margin,
207                 /* yCoordUpPointingTip= */ rect.bottom + margin);
208     }
209 
210     /**
211      * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it
212      * cannot fit on screen in the requested orientation.
213      *
214      * @param text The text to be shown in the tooltip.
215      * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the
216      *                    center of tooltip unless the tooltip goes beyond screen margin.
217      * @param yCoordDownPointingTip The Y coordinate of the pointed tip end of the tooltip when the
218      *                              tooltip is placed pointing downwards.
219      * @param yCoordUpPointingTip The Y coordinate of the pointed tip end of the tooltip when the
220      *                            tooltip is placed pointing upwards.
221      * @return The tool tip view. {@code null} if the tip can not be shown.
222      */
showAtLocation(String text, @Px int arrowXCoord, @Px int yCoordDownPointingTip, @Px int yCoordUpPointingTip)223     @Nullable private ArrowTipView showAtLocation(String text, @Px int arrowXCoord,
224             @Px int yCoordDownPointingTip, @Px int yCoordUpPointingTip) {
225         ViewGroup parent = mActivity.getDragLayer();
226         @Px int parentViewWidth = parent.getWidth();
227         @Px int parentViewHeight = parent.getHeight();
228         @Px int maxTextViewWidth = getContext().getResources()
229                 .getDimensionPixelSize(R.dimen.widget_picker_education_tip_max_width);
230         @Px int minViewMargin = getContext().getResources()
231                 .getDimensionPixelSize(R.dimen.widget_picker_education_tip_min_margin);
232         if (parentViewWidth < maxTextViewWidth + 2 * minViewMargin) {
233             Log.w(TAG, "Cannot display tip on a small screen of size: " + parentViewWidth);
234             return null;
235         }
236 
237         TextView textView = findViewById(R.id.text);
238         textView.setText(text);
239         textView.setMaxWidth(maxTextViewWidth);
240         parent.addView(this);
241         requestLayout();
242 
243         post(() -> {
244             // Adjust the tooltip horizontally.
245             float halfWidth = getWidth() / 2f;
246             float xCoord;
247             if (arrowXCoord - halfWidth < minViewMargin) {
248                 // If the tooltip is estimated to go beyond the left margin, place its start just at
249                 // the left margin.
250                 xCoord = minViewMargin;
251             } else if (arrowXCoord + halfWidth > parentViewWidth - minViewMargin) {
252                 // If the tooltip is estimated to go beyond the right margin, place it such that its
253                 // end is just at the right margin.
254                 xCoord = parentViewWidth - minViewMargin - getWidth();
255             } else {
256                 // Place the tooltip such that its center is at arrowXCoord.
257                 xCoord = arrowXCoord - halfWidth;
258             }
259             setX(xCoord);
260 
261             // Adjust the tooltip vertically.
262             @Px int viewHeight = getHeight();
263             if (mIsPointingUp
264                     ? (yCoordUpPointingTip + viewHeight > parentViewHeight)
265                     : (yCoordDownPointingTip - viewHeight < 0)) {
266                 // Flip the view if it exceeds the vertical bounds of screen.
267                 mIsPointingUp = !mIsPointingUp;
268                 updateArrowTipInView();
269             }
270             // Place the tooltip such that its top is at yCoordUpPointingTip if arrow is displayed
271             // pointing upwards, otherwise place it such that its bottom is at
272             // yCoordDownPointingTip.
273             setY(mIsPointingUp ? yCoordUpPointingTip : yCoordDownPointingTip - viewHeight);
274 
275             // Adjust the arrow's relative position on tooltip to make sure the actual position of
276             // arrow's pointed tip is always at arrowXCoord.
277             mArrowView.setX(arrowXCoord - xCoord - mArrowView.getWidth() / 2f);
278             requestLayout();
279         });
280 
281         mIsOpen = true;
282         mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
283         setAlpha(0);
284         animate()
285                 .alpha(1f)
286                 .withLayer()
287                 .setStartDelay(SHOW_DELAY_MS)
288                 .setDuration(SHOW_DURATION_MS)
289                 .setInterpolator(Interpolators.DEACCEL)
290                 .start();
291         return this;
292     }
293 
updateArrowTipInView()294     private void updateArrowTipInView() {
295         ViewGroup.LayoutParams arrowLp = mArrowView.getLayoutParams();
296         ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
297                 arrowLp.width, arrowLp.height, mIsPointingUp));
298         Paint arrowPaint = arrowDrawable.getPaint();
299         @Px int arrowTipRadius = getContext().getResources()
300                 .getDimensionPixelSize(R.dimen.arrow_toast_corner_radius);
301         arrowPaint.setColor(ContextCompat.getColor(getContext(), R.color.arrow_tip_view_bg));
302         arrowPaint.setPathEffect(new CornerPathEffect(arrowTipRadius));
303         mArrowView.setBackground(arrowDrawable);
304         // Add negative margin so that the rounded corners on base of arrow are not visible.
305         removeView(mArrowView);
306         if (mIsPointingUp) {
307             addView(mArrowView, 0);
308             ((ViewGroup.MarginLayoutParams) arrowLp).setMargins(0, 0, 0, -1 * arrowTipRadius);
309         } else {
310             addView(mArrowView, 1);
311             ((ViewGroup.MarginLayoutParams) arrowLp).setMargins(0, -1 * arrowTipRadius, 0, 0);
312         }
313     }
314 
315     /**
316      * Register a callback fired when toast is hidden
317      */
setOnClosedCallback(Runnable runnable)318     public ArrowTipView setOnClosedCallback(Runnable runnable) {
319         mOnClosed = runnable;
320         return this;
321     }
322 
323     @Override
onConfigurationChanged(Configuration newConfig)324     protected void onConfigurationChanged(Configuration newConfig) {
325         super.onConfigurationChanged(newConfig);
326         close(/* animate= */ false);
327     }
328 }
329