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