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         ((TextView) findViewById(R.id.text)).setText(text);
141         ViewGroup parent = mActivity.getDragLayer();
142         parent.addView(this);
143 
144         DeviceProfile grid = mActivity.getDeviceProfile();
145 
146         DragLayer.LayoutParams params = (DragLayer.LayoutParams) getLayoutParams();
147         params.gravity = gravity;
148         params.leftMargin = mArrowMinOffset + grid.getInsets().left;
149         params.rightMargin = mArrowMinOffset + grid.getInsets().right;
150         LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mArrowView.getLayoutParams();
151 
152         lp.gravity = gravity;
153 
154         if (parent.getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
155             arrowMarginStart = parent.getMeasuredWidth() - arrowMarginStart;
156         }
157         if (gravity == Gravity.END) {
158             lp.setMarginEnd(Math.max(mArrowMinOffset,
159                     parent.getMeasuredWidth() - params.rightMargin - arrowMarginStart
160                             - mArrowWidth / 2));
161         } else if (gravity == Gravity.START) {
162             lp.setMarginStart(Math.max(mArrowMinOffset,
163                     arrowMarginStart - params.leftMargin - mArrowWidth / 2));
164         }
165         requestLayout();
166         post(() -> setY(top - (mIsPointingUp ? 0 : getHeight())));
167 
168         mIsOpen = true;
169         mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
170         setAlpha(0);
171         animate()
172                 .alpha(1f)
173                 .withLayer()
174                 .setStartDelay(SHOW_DELAY_MS)
175                 .setDuration(SHOW_DURATION_MS)
176                 .setInterpolator(Interpolators.DEACCEL)
177                 .start();
178         return this;
179     }
180 
181     /**
182      * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it
183      * cannot fit on screen in the requested orientation.
184      *
185      * @param text The text to be shown in the tooltip.
186      * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the
187      *                    center of tooltip unless the tooltip goes beyond screen margin.
188      * @param yCoord The Y coordinate of the pointed tip end of the tooltip.
189      * @return The tool tip view. {@code null} if the tip can not be shown.
190      */
showAtLocation(String text, @Px int arrowXCoord, @Px int yCoord)191     @Nullable public ArrowTipView showAtLocation(String text, @Px int arrowXCoord, @Px int yCoord) {
192         return showAtLocation(
193                 text,
194                 arrowXCoord,
195                 /* yCoordDownPointingTip= */ yCoord,
196                 /* yCoordUpPointingTip= */ yCoord);
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 rect The coordinates of the view which requests the tooltip to be shown.
207      * @param margin The margin between {@param rect} and the tooltip.
208      * @return The tool tip view. {@code null} if the tip can not be shown.
209      */
showAroundRect( String text, @Px int arrowXCoord, Rect rect, @Px int margin)210     @Nullable public ArrowTipView showAroundRect(
211             String text, @Px int arrowXCoord, Rect rect, @Px int margin) {
212         return showAtLocation(
213                 text,
214                 arrowXCoord,
215                 /* yCoordDownPointingTip= */ rect.top - margin,
216                 /* yCoordUpPointingTip= */ rect.bottom + margin);
217     }
218 
219     /**
220      * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it
221      * cannot fit on screen in the requested orientation.
222      *
223      * @param text The text to be shown in the tooltip.
224      * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the
225      *                    center of tooltip unless the tooltip goes beyond screen margin.
226      * @param yCoordDownPointingTip The Y coordinate of the pointed tip end of the tooltip when the
227      *                              tooltip is placed pointing downwards.
228      * @param yCoordUpPointingTip The Y coordinate of the pointed tip end of the tooltip when the
229      *                            tooltip is placed pointing upwards.
230      * @return The tool tip view. {@code null} if the tip can not be shown.
231      */
showAtLocation(String text, @Px int arrowXCoord, @Px int yCoordDownPointingTip, @Px int yCoordUpPointingTip)232     @Nullable private ArrowTipView showAtLocation(String text, @Px int arrowXCoord,
233             @Px int yCoordDownPointingTip, @Px int yCoordUpPointingTip) {
234         ViewGroup parent = mActivity.getDragLayer();
235         @Px int parentViewWidth = parent.getWidth();
236         @Px int parentViewHeight = parent.getHeight();
237         @Px int maxTextViewWidth = getContext().getResources()
238                 .getDimensionPixelSize(R.dimen.widget_picker_education_tip_max_width);
239         @Px int minViewMargin = getContext().getResources()
240                 .getDimensionPixelSize(R.dimen.widget_picker_education_tip_min_margin);
241         if (parentViewWidth < maxTextViewWidth + 2 * minViewMargin) {
242             Log.w(TAG, "Cannot display tip on a small screen of size: " + parentViewWidth);
243             return null;
244         }
245 
246         TextView textView = findViewById(R.id.text);
247         textView.setText(text);
248         textView.setMaxWidth(maxTextViewWidth);
249         parent.addView(this);
250         requestLayout();
251 
252         post(() -> {
253             // Adjust the tooltip horizontally.
254             float halfWidth = getWidth() / 2f;
255             float xCoord;
256             if (arrowXCoord - halfWidth < minViewMargin) {
257                 // If the tooltip is estimated to go beyond the left margin, place its start just at
258                 // the left margin.
259                 xCoord = minViewMargin;
260             } else if (arrowXCoord + halfWidth > parentViewWidth - minViewMargin) {
261                 // If the tooltip is estimated to go beyond the right margin, place it such that its
262                 // end is just at the right margin.
263                 xCoord = parentViewWidth - minViewMargin - getWidth();
264             } else {
265                 // Place the tooltip such that its center is at arrowXCoord.
266                 xCoord = arrowXCoord - halfWidth;
267             }
268             setX(xCoord);
269 
270             // Adjust the tooltip vertically.
271             @Px int viewHeight = getHeight();
272             if (mIsPointingUp
273                     ? (yCoordUpPointingTip + viewHeight > parentViewHeight)
274                     : (yCoordDownPointingTip - viewHeight < 0)) {
275                 // Flip the view if it exceeds the vertical bounds of screen.
276                 mIsPointingUp = !mIsPointingUp;
277                 updateArrowTipInView();
278             }
279             // Place the tooltip such that its top is at yCoordUpPointingTip if arrow is displayed
280             // pointing upwards, otherwise place it such that its bottom is at
281             // yCoordDownPointingTip.
282             setY(mIsPointingUp ? yCoordUpPointingTip : yCoordDownPointingTip - viewHeight);
283 
284             // Adjust the arrow's relative position on tooltip to make sure the actual position of
285             // arrow's pointed tip is always at arrowXCoord.
286             mArrowView.setX(arrowXCoord - xCoord - mArrowView.getWidth() / 2f);
287             requestLayout();
288         });
289 
290         mIsOpen = true;
291         mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
292         setAlpha(0);
293         animate()
294                 .alpha(1f)
295                 .withLayer()
296                 .setStartDelay(SHOW_DELAY_MS)
297                 .setDuration(SHOW_DURATION_MS)
298                 .setInterpolator(Interpolators.DEACCEL)
299                 .start();
300         return this;
301     }
302 
updateArrowTipInView()303     private void updateArrowTipInView() {
304         ViewGroup.LayoutParams arrowLp = mArrowView.getLayoutParams();
305         ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
306                 arrowLp.width, arrowLp.height, mIsPointingUp));
307         Paint arrowPaint = arrowDrawable.getPaint();
308         @Px int arrowTipRadius = getContext().getResources()
309                 .getDimensionPixelSize(R.dimen.arrow_toast_corner_radius);
310         arrowPaint.setColor(ContextCompat.getColor(getContext(), R.color.arrow_tip_view_bg));
311         arrowPaint.setPathEffect(new CornerPathEffect(arrowTipRadius));
312         mArrowView.setBackground(arrowDrawable);
313         // Add negative margin so that the rounded corners on base of arrow are not visible.
314         removeView(mArrowView);
315         if (mIsPointingUp) {
316             addView(mArrowView, 0);
317             ((ViewGroup.MarginLayoutParams) arrowLp).setMargins(0, 0, 0, -1 * arrowTipRadius);
318         } else {
319             addView(mArrowView, 1);
320             ((ViewGroup.MarginLayoutParams) arrowLp).setMargins(0, -1 * arrowTipRadius, 0, 0);
321         }
322     }
323 
324     /**
325      * Register a callback fired when toast is hidden
326      */
setOnClosedCallback(Runnable runnable)327     public ArrowTipView setOnClosedCallback(Runnable runnable) {
328         mOnClosed = runnable;
329         return this;
330     }
331 
332     @Override
onConfigurationChanged(Configuration newConfig)333     protected void onConfigurationChanged(Configuration newConfig) {
334         super.onConfigurationChanged(newConfig);
335         close(/* animate= */ false);
336     }
337 }
338