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