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