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