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 private 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 TypedArray ta = context.obtainStyledAttributes(R.styleable.ArrowTipView); 107 // Set style to default to avoid inflation issues with missing attributes. 108 if (!ta.hasValue(R.styleable.ArrowTipView_arrowTipBackground) 109 || !ta.hasValue(R.styleable.ArrowTipView_arrowTipTextColor)) { 110 context = new ContextThemeWrapper(context, R.style.ArrowTipStyle); 111 } 112 mArrowViewPaintColor = ta.getColor(R.styleable.ArrowTipView_arrowTipBackground, 113 context.getColor(R.color.arrow_tip_view_bg)); 114 ta.recycle(); 115 init(context, layoutId); 116 } 117 118 @Override onControllerInterceptTouchEvent(MotionEvent ev)119 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 120 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 121 close(true); 122 if (mActivityContext.getDragLayer().isEventOverView(this, ev)) { 123 return true; 124 } 125 } 126 return false; 127 } 128 129 @Override handleClose(boolean animate)130 protected void handleClose(boolean animate) { 131 if (mOpenAnimator.isStarted()) { 132 mOpenAnimator.cancel(); 133 } 134 if (mIsOpen) { 135 if (animate) { 136 mCloseAnimator.addListener(AnimatorListeners.forSuccessCallback( 137 () -> mActivityContext.getDragLayer().removeView(this))); 138 mCloseAnimator.start(); 139 } else { 140 mCloseAnimator.cancel(); 141 mActivityContext.getDragLayer().removeView(this); 142 } 143 if (mOnClosed != null) mOnClosed.run(); 144 mIsOpen = false; 145 } 146 } 147 148 @Override isOfType(int type)149 protected boolean isOfType(int type) { 150 return (type & TYPE_ON_BOARD_POPUP) != 0; 151 } 152 init(Context context, int layoutId)153 private void init(Context context, int layoutId) { 154 inflate(context, layoutId, this); 155 setOrientation(LinearLayout.VERTICAL); 156 157 mArrowView = findViewById(R.id.arrow); 158 updateArrowTipInView(mIsPointingUp); 159 setAlpha(0); 160 161 // Create default open animator. 162 mOpenAnimator.play(ObjectAnimator.ofFloat(this, ALPHA, 1f)); 163 mOpenAnimator.setStartDelay(SHOW_DELAY_MS); 164 mOpenAnimator.setDuration(SHOW_DURATION_MS); 165 mOpenAnimator.setInterpolator(Interpolators.DECELERATE); 166 167 // Create default close animator. 168 mCloseAnimator.play(ObjectAnimator.ofFloat(this, ALPHA, 0)); 169 mCloseAnimator.setStartDelay(0); 170 mCloseAnimator.setDuration(HIDE_DURATION_MS); 171 mCloseAnimator.setInterpolator(Interpolators.ACCELERATE); 172 mCloseAnimator.addListener(new AnimatorListenerAdapter() { 173 @Override 174 public void onAnimationEnd(Animator animation) { 175 mActivityContext.getDragLayer().removeView(ArrowTipView.this); 176 } 177 }); 178 } 179 180 /** 181 * Show Tip with specified string and Y location 182 */ show(String text, int top)183 public ArrowTipView show(String text, int top) { 184 return show(text, Gravity.CENTER_HORIZONTAL, 0, top); 185 } 186 187 /** 188 * Show the ArrowTipView (tooltip) center, start, or end aligned. 189 * 190 * @param text The text to be shown in the tooltip. 191 * @param gravity The gravity aligns the tooltip center, start, or end. 192 * @param arrowMarginStart The margin from start to place arrow (ignored if center) 193 * @param top The Y coordinate of the bottom of tooltip. 194 * @return The tooltip. 195 */ show(String text, int gravity, int arrowMarginStart, int top)196 public ArrowTipView show(String text, int gravity, int arrowMarginStart, int top) { 197 return show(text, gravity, arrowMarginStart, top, true); 198 } 199 200 /** 201 * Show the ArrowTipView (tooltip) center, start, or end aligned. 202 * 203 * @param text The text to be shown in the tooltip. 204 * @param gravity The gravity aligns the tooltip center, start, or end. 205 * @param arrowMarginStart The margin from start to place arrow (ignored if center) 206 * @param top The Y coordinate of the bottom of tooltip. 207 * @param shouldAutoClose If Tooltip should be auto close. 208 * @return The tooltip. 209 */ show( String text, int gravity, int arrowMarginStart, int top, boolean shouldAutoClose)210 public ArrowTipView show( 211 String text, int gravity, int arrowMarginStart, int top, boolean shouldAutoClose) { 212 ((TextView) findViewById(R.id.text)).setText(text); 213 ViewGroup parent = mActivityContext.getDragLayer(); 214 parent.addView(this); 215 216 DeviceProfile grid = mActivityContext.getDeviceProfile(); 217 218 DragLayer.LayoutParams params = (DragLayer.LayoutParams) getLayoutParams(); 219 params.gravity = gravity; 220 params.leftMargin = mArrowMinOffset + grid.getInsets().left; 221 params.rightMargin = mArrowMinOffset + grid.getInsets().right; 222 params.width = LayoutParams.MATCH_PARENT; 223 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mArrowView.getLayoutParams(); 224 225 lp.gravity = gravity; 226 227 if (parent.getLayoutDirection() == LAYOUT_DIRECTION_RTL) { 228 arrowMarginStart = parent.getMeasuredWidth() - arrowMarginStart; 229 } 230 if (gravity == Gravity.END) { 231 lp.setMarginEnd(Math.max(mArrowMinOffset, 232 parent.getMeasuredWidth() - params.rightMargin - arrowMarginStart 233 - mArrowWidth / 2)); 234 } else if (gravity == Gravity.START) { 235 lp.setMarginStart(Math.max(mArrowMinOffset, 236 arrowMarginStart - params.leftMargin - mArrowWidth / 2)); 237 } 238 requestLayout(); 239 post(() -> setY(top - (mIsPointingUp ? 0 : getHeight()))); 240 241 mIsOpen = true; 242 if (shouldAutoClose) { 243 mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS); 244 } 245 246 mOpenAnimator.start(); 247 return this; 248 } 249 250 /** 251 * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it 252 * cannot fit on screen in the requested orientation. 253 * 254 * @param text The text to be shown in the tooltip. 255 * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the 256 * center of tooltip unless the tooltip goes beyond screen margin. 257 * @param yCoord The Y coordinate of the pointed tip end of the tooltip. 258 * @return The tool tip view. {@code null} if the tip can not be shown. 259 */ showAtLocation(String text, @Px int arrowXCoord, @Px int yCoord)260 @Nullable public ArrowTipView showAtLocation(String text, @Px int arrowXCoord, @Px int yCoord) { 261 return showAtLocation( 262 text, 263 arrowXCoord, 264 /* yCoordDownPointingTip= */ yCoord, 265 /* yCoordUpPointingTip= */ yCoord, 266 /* shouldAutoClose= */ true); 267 } 268 269 /** 270 * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it 271 * cannot fit on screen in the requested orientation. 272 * 273 * @param text The text to be shown in the tooltip. 274 * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the 275 * center of tooltip unless the tooltip goes beyond screen margin. 276 * @param yCoord The Y coordinate of the pointed tip end of the tooltip. 277 * @param shouldAutoClose If Tooltip should be auto close. 278 * @return The tool tip view. {@code null} if the tip can not be shown. 279 */ showAtLocation( String text, @Px int arrowXCoord, @Px int yCoord, boolean shouldAutoClose)280 @Nullable public ArrowTipView showAtLocation( 281 String text, @Px int arrowXCoord, @Px int yCoord, boolean shouldAutoClose) { 282 return showAtLocation( 283 text, 284 arrowXCoord, 285 /* yCoordDownPointingTip= */ yCoord, 286 /* yCoordUpPointingTip= */ yCoord, 287 /* shouldAutoClose= */ shouldAutoClose); 288 } 289 290 /** 291 * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it 292 * cannot fit on screen in the requested orientation. 293 * 294 * @param text The text to be shown in the tooltip. 295 * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the 296 * center of tooltip unless the tooltip goes beyond screen margin. 297 * @param rect The coordinates of the view which requests the tooltip to be shown. 298 * @param margin The margin between {@param rect} and the tooltip. 299 * @return The tool tip view. {@code null} if the tip can not be shown. 300 */ showAroundRect( String text, @Px int arrowXCoord, Rect rect, @Px int margin)301 @Nullable public ArrowTipView showAroundRect( 302 String text, @Px int arrowXCoord, Rect rect, @Px int margin) { 303 return showAtLocation( 304 text, 305 arrowXCoord, 306 /* yCoordDownPointingTip= */ rect.top - margin, 307 /* yCoordUpPointingTip= */ rect.bottom + margin, 308 /* shouldAutoClose= */ true); 309 } 310 311 /** 312 * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it 313 * cannot fit on screen in the requested orientation. 314 * 315 * @param text The text to be shown in the tooltip. 316 * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the 317 * center of tooltip unless the tooltip goes beyond screen margin. 318 * @param yCoordDownPointingTip The Y coordinate of the pointed tip end of the tooltip when the 319 * tooltip is placed pointing downwards. 320 * @param yCoordUpPointingTip The Y coordinate of the pointed tip end of the tooltip when the 321 * tooltip is placed pointing upwards. 322 * @param shouldAutoClose If Tooltip should be auto close. 323 * @return The tool tip view. {@code null} if the tip can not be shown. 324 */ showAtLocation(String text, @Px int arrowXCoord, @Px int yCoordDownPointingTip, @Px int yCoordUpPointingTip, boolean shouldAutoClose)325 @Nullable private ArrowTipView showAtLocation(String text, @Px int arrowXCoord, 326 @Px int yCoordDownPointingTip, @Px int yCoordUpPointingTip, boolean shouldAutoClose) { 327 ViewGroup parent = mActivityContext.getDragLayer(); 328 @Px int parentViewWidth = parent.getWidth(); 329 @Px int parentViewHeight = parent.getHeight(); 330 @Px int maxTextViewWidth = getContext().getResources() 331 .getDimensionPixelSize(R.dimen.widget_picker_education_tip_max_width); 332 @Px int minViewMargin = getContext().getResources() 333 .getDimensionPixelSize(R.dimen.widget_picker_education_tip_min_margin); 334 if (parentViewWidth < maxTextViewWidth + 2 * minViewMargin) { 335 Log.w(TAG, "Cannot display tip on a small screen of size: " + parentViewWidth); 336 return null; 337 } 338 339 TextView textView = findViewById(R.id.text); 340 textView.setText(text); 341 textView.setMaxWidth(maxTextViewWidth); 342 if (parent.indexOfChild(this) < 0) { 343 parent.addView(this); 344 requestLayout(); 345 } 346 347 post(() -> { 348 // Adjust the tooltip horizontally. 349 float halfWidth = getWidth() / 2f; 350 float xCoord; 351 if (arrowXCoord - halfWidth < minViewMargin) { 352 // If the tooltip is estimated to go beyond the left margin, place its start just at 353 // the left margin. 354 xCoord = minViewMargin; 355 } else if (arrowXCoord + halfWidth > parentViewWidth - minViewMargin) { 356 // If the tooltip is estimated to go beyond the right margin, place it such that its 357 // end is just at the right margin. 358 xCoord = parentViewWidth - minViewMargin - getWidth(); 359 } else { 360 // Place the tooltip such that its center is at arrowXCoord. 361 xCoord = arrowXCoord - halfWidth; 362 } 363 setX(xCoord); 364 365 // Adjust the tooltip vertically. 366 @Px int viewHeight = getHeight(); 367 boolean isPointingUp = mIsPointingUp; 368 if (mIsPointingUp 369 ? (yCoordUpPointingTip + viewHeight > parentViewHeight) 370 : (yCoordDownPointingTip - viewHeight < 0)) { 371 // Flip the view if it exceeds the vertical bounds of screen. 372 isPointingUp = !mIsPointingUp; 373 } 374 updateArrowTipInView(isPointingUp); 375 // Place the tooltip such that its top is at yCoordUpPointingTip if arrow is displayed 376 // pointing upwards, otherwise place it such that its bottom is at 377 // yCoordDownPointingTip. 378 setY(isPointingUp ? yCoordUpPointingTip : yCoordDownPointingTip - viewHeight); 379 380 // Adjust the arrow's relative position on tooltip to make sure the actual position of 381 // arrow's pointed tip is always at arrowXCoord. 382 mArrowView.setX(arrowXCoord - xCoord - mArrowView.getWidth() / 2f); 383 requestLayout(); 384 }); 385 386 mIsOpen = true; 387 if (shouldAutoClose) { 388 mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS); 389 } 390 391 mOpenAnimator.start(); 392 return this; 393 } 394 updateArrowTipInView(boolean isPointingUp)395 private void updateArrowTipInView(boolean isPointingUp) { 396 ViewGroup.LayoutParams arrowLp = mArrowView.getLayoutParams(); 397 ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create( 398 arrowLp.width, arrowLp.height, isPointingUp)); 399 Paint arrowPaint = arrowDrawable.getPaint(); 400 @Px int arrowTipRadius = getContext().getResources() 401 .getDimensionPixelSize(R.dimen.arrow_toast_corner_radius); 402 arrowPaint.setColor(mArrowViewPaintColor); 403 arrowPaint.setPathEffect(new CornerPathEffect(arrowTipRadius)); 404 mArrowView.setBackground(arrowDrawable); 405 // Add negative margin so that the rounded corners on base of arrow are not visible. 406 removeView(mArrowView); 407 if (isPointingUp) { 408 addView(mArrowView, 0); 409 ((ViewGroup.MarginLayoutParams) arrowLp).setMargins(0, 0, 0, -1 * arrowTipRadius); 410 } else { 411 addView(mArrowView, 1); 412 ((ViewGroup.MarginLayoutParams) arrowLp).setMargins(0, -1 * arrowTipRadius, 0, 0); 413 } 414 } 415 416 /** 417 * Register a callback fired when toast is hidden 418 */ setOnClosedCallback(Runnable runnable)419 public ArrowTipView setOnClosedCallback(Runnable runnable) { 420 mOnClosed = runnable; 421 return this; 422 } 423 424 @Override onConfigurationChanged(Configuration newConfig)425 protected void onConfigurationChanged(Configuration newConfig) { 426 super.onConfigurationChanged(newConfig); 427 close(/* animate= */ false); 428 } 429 430 /** 431 * Sets a custom animation to run on open of the ArrowTipView. 432 */ setCustomOpenAnimation(AnimatorSet animator)433 public void setCustomOpenAnimation(AnimatorSet animator) { 434 mOpenAnimator = animator; 435 } 436 437 /** 438 * Sets a custom animation to run on close of the ArrowTipView. 439 */ setCustomCloseAnimation(AnimatorSet animator)440 public void setCustomCloseAnimation(AnimatorSet animator) { 441 mCloseAnimator = animator; 442 } 443 setTextAlpha(int textAlpha)444 private void setTextAlpha(int textAlpha) { 445 if (mTextAlpha != textAlpha) { 446 mTextAlpha = textAlpha; 447 TextView textView = findViewById(R.id.text); 448 textView.setTextColor(textView.getTextColors().withAlpha(mTextAlpha)); 449 } 450 } 451 getTextAlpha()452 private int getTextAlpha() { 453 return mTextAlpha; 454 } 455 } 456