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.R; 41 import com.android.launcher3.anim.Interpolators; 42 import com.android.launcher3.dragndrop.DragLayer; 43 import com.android.launcher3.graphics.TriangleShape; 44 45 /** 46 * A base class for arrow tip view in launcher 47 */ 48 public class ArrowTipView extends AbstractFloatingView { 49 50 private static final String TAG = ArrowTipView.class.getSimpleName(); 51 private static final long AUTO_CLOSE_TIMEOUT_MILLIS = 10 * 1000; 52 private static final long SHOW_DELAY_MS = 200; 53 private static final long SHOW_DURATION_MS = 300; 54 private static final long HIDE_DURATION_MS = 100; 55 56 protected final BaseDraggingActivity mActivity; 57 private final Handler mHandler = new Handler(); 58 private final int mArrowWidth; 59 private boolean mIsPointingUp; 60 private Runnable mOnClosed; 61 private View mArrowView; 62 ArrowTipView(Context context)63 public ArrowTipView(Context context) { 64 this(context, false); 65 } 66 ArrowTipView(Context context, boolean isPointingUp)67 public ArrowTipView(Context context, boolean isPointingUp) { 68 super(context, null, 0); 69 mActivity = BaseDraggingActivity.fromContext(context); 70 mIsPointingUp = isPointingUp; 71 mArrowWidth = context.getResources().getDimensionPixelSize(R.dimen.arrow_toast_arrow_width); 72 init(context); 73 } 74 75 @Override onControllerInterceptTouchEvent(MotionEvent ev)76 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 77 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 78 close(true); 79 if (mActivity.getDragLayer().isEventOverView(this, ev)) { 80 return true; 81 } 82 } 83 return false; 84 } 85 86 @Override handleClose(boolean animate)87 protected void handleClose(boolean animate) { 88 if (mIsOpen) { 89 if (animate) { 90 animate().alpha(0f) 91 .withLayer() 92 .setStartDelay(0) 93 .setDuration(HIDE_DURATION_MS) 94 .setInterpolator(Interpolators.ACCEL) 95 .withEndAction(() -> mActivity.getDragLayer().removeView(this)) 96 .start(); 97 } else { 98 animate().cancel(); 99 mActivity.getDragLayer().removeView(this); 100 } 101 if (mOnClosed != null) mOnClosed.run(); 102 mIsOpen = false; 103 } 104 } 105 106 @Override isOfType(int type)107 protected boolean isOfType(int type) { 108 return (type & TYPE_ON_BOARD_POPUP) != 0; 109 } 110 init(Context context)111 private void init(Context context) { 112 inflate(context, R.layout.arrow_toast, this); 113 setOrientation(LinearLayout.VERTICAL); 114 115 mArrowView = findViewById(R.id.arrow); 116 updateArrowTipInView(); 117 } 118 119 /** 120 * Show Tip with specified string and Y location 121 */ show(String text, int top)122 public ArrowTipView show(String text, int top) { 123 return show(text, Gravity.CENTER_HORIZONTAL, 0, top); 124 } 125 126 /** 127 * Show the ArrowTipView (tooltip) center, start, or end aligned. 128 * 129 * @param text The text to be shown in the tooltip. 130 * @param gravity The gravity aligns the tooltip center, start, or end. 131 * @param arrowMarginStart The margin from start to place arrow (ignored if center) 132 * @param top The Y coordinate of the bottom of tooltip. 133 * @return The tooltip. 134 */ show(String text, int gravity, int arrowMarginStart, int top)135 public ArrowTipView show(String text, int gravity, int arrowMarginStart, int top) { 136 ((TextView) findViewById(R.id.text)).setText(text); 137 ViewGroup parent = mActivity.getDragLayer(); 138 parent.addView(this); 139 140 DragLayer.LayoutParams params = (DragLayer.LayoutParams) getLayoutParams(); 141 params.gravity = gravity; 142 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mArrowView.getLayoutParams(); 143 lp.gravity = gravity; 144 145 if (parent.getLayoutDirection() == LAYOUT_DIRECTION_RTL) { 146 arrowMarginStart = parent.getMeasuredWidth() - arrowMarginStart; 147 } 148 if (gravity == Gravity.END) { 149 lp.setMarginEnd(parent.getMeasuredWidth() - arrowMarginStart - mArrowWidth); 150 } else if (gravity == Gravity.START) { 151 lp.setMarginStart(arrowMarginStart - mArrowWidth / 2); 152 } 153 requestLayout(); 154 155 params.leftMargin = mActivity.getDeviceProfile().workspacePadding.left; 156 params.rightMargin = mActivity.getDeviceProfile().workspacePadding.right; 157 post(() -> setY(top - (mIsPointingUp ? 0 : getHeight()))); 158 159 mIsOpen = true; 160 mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS); 161 setAlpha(0); 162 animate() 163 .alpha(1f) 164 .withLayer() 165 .setStartDelay(SHOW_DELAY_MS) 166 .setDuration(SHOW_DURATION_MS) 167 .setInterpolator(Interpolators.DEACCEL) 168 .start(); 169 return this; 170 } 171 172 /** 173 * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it 174 * cannot fit on screen in the requested orientation. 175 * 176 * @param text The text to be shown in the tooltip. 177 * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the 178 * center of tooltip unless the tooltip goes beyond screen margin. 179 * @param yCoord The Y coordinate of the pointed tip end of the tooltip. 180 * @return The tool tip view. {@code null} if the tip can not be shown. 181 */ showAtLocation(String text, @Px int arrowXCoord, @Px int yCoord)182 @Nullable public ArrowTipView showAtLocation(String text, @Px int arrowXCoord, @Px int yCoord) { 183 return showAtLocation( 184 text, 185 arrowXCoord, 186 /* yCoordDownPointingTip= */ yCoord, 187 /* yCoordUpPointingTip= */ yCoord); 188 } 189 190 /** 191 * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it 192 * cannot fit on screen in the requested orientation. 193 * 194 * @param text The text to be shown in the tooltip. 195 * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the 196 * center of tooltip unless the tooltip goes beyond screen margin. 197 * @param rect The coordinates of the view which requests the tooltip to be shown. 198 * @param margin The margin between {@param rect} and the tooltip. 199 * @return The tool tip view. {@code null} if the tip can not be shown. 200 */ showAroundRect( String text, @Px int arrowXCoord, Rect rect, @Px int margin)201 @Nullable public ArrowTipView showAroundRect( 202 String text, @Px int arrowXCoord, Rect rect, @Px int margin) { 203 return showAtLocation( 204 text, 205 arrowXCoord, 206 /* yCoordDownPointingTip= */ rect.top - margin, 207 /* yCoordUpPointingTip= */ rect.bottom + margin); 208 } 209 210 /** 211 * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it 212 * cannot fit on screen in the requested orientation. 213 * 214 * @param text The text to be shown in the tooltip. 215 * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the 216 * center of tooltip unless the tooltip goes beyond screen margin. 217 * @param yCoordDownPointingTip The Y coordinate of the pointed tip end of the tooltip when the 218 * tooltip is placed pointing downwards. 219 * @param yCoordUpPointingTip The Y coordinate of the pointed tip end of the tooltip when the 220 * tooltip is placed pointing upwards. 221 * @return The tool tip view. {@code null} if the tip can not be shown. 222 */ showAtLocation(String text, @Px int arrowXCoord, @Px int yCoordDownPointingTip, @Px int yCoordUpPointingTip)223 @Nullable private ArrowTipView showAtLocation(String text, @Px int arrowXCoord, 224 @Px int yCoordDownPointingTip, @Px int yCoordUpPointingTip) { 225 ViewGroup parent = mActivity.getDragLayer(); 226 @Px int parentViewWidth = parent.getWidth(); 227 @Px int parentViewHeight = parent.getHeight(); 228 @Px int maxTextViewWidth = getContext().getResources() 229 .getDimensionPixelSize(R.dimen.widget_picker_education_tip_max_width); 230 @Px int minViewMargin = getContext().getResources() 231 .getDimensionPixelSize(R.dimen.widget_picker_education_tip_min_margin); 232 if (parentViewWidth < maxTextViewWidth + 2 * minViewMargin) { 233 Log.w(TAG, "Cannot display tip on a small screen of size: " + parentViewWidth); 234 return null; 235 } 236 237 TextView textView = findViewById(R.id.text); 238 textView.setText(text); 239 textView.setMaxWidth(maxTextViewWidth); 240 parent.addView(this); 241 requestLayout(); 242 243 post(() -> { 244 // Adjust the tooltip horizontally. 245 float halfWidth = getWidth() / 2f; 246 float xCoord; 247 if (arrowXCoord - halfWidth < minViewMargin) { 248 // If the tooltip is estimated to go beyond the left margin, place its start just at 249 // the left margin. 250 xCoord = minViewMargin; 251 } else if (arrowXCoord + halfWidth > parentViewWidth - minViewMargin) { 252 // If the tooltip is estimated to go beyond the right margin, place it such that its 253 // end is just at the right margin. 254 xCoord = parentViewWidth - minViewMargin - getWidth(); 255 } else { 256 // Place the tooltip such that its center is at arrowXCoord. 257 xCoord = arrowXCoord - halfWidth; 258 } 259 setX(xCoord); 260 261 // Adjust the tooltip vertically. 262 @Px int viewHeight = getHeight(); 263 if (mIsPointingUp 264 ? (yCoordUpPointingTip + viewHeight > parentViewHeight) 265 : (yCoordDownPointingTip - viewHeight < 0)) { 266 // Flip the view if it exceeds the vertical bounds of screen. 267 mIsPointingUp = !mIsPointingUp; 268 updateArrowTipInView(); 269 } 270 // Place the tooltip such that its top is at yCoordUpPointingTip if arrow is displayed 271 // pointing upwards, otherwise place it such that its bottom is at 272 // yCoordDownPointingTip. 273 setY(mIsPointingUp ? yCoordUpPointingTip : yCoordDownPointingTip - viewHeight); 274 275 // Adjust the arrow's relative position on tooltip to make sure the actual position of 276 // arrow's pointed tip is always at arrowXCoord. 277 mArrowView.setX(arrowXCoord - xCoord - mArrowView.getWidth() / 2f); 278 requestLayout(); 279 }); 280 281 mIsOpen = true; 282 mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS); 283 setAlpha(0); 284 animate() 285 .alpha(1f) 286 .withLayer() 287 .setStartDelay(SHOW_DELAY_MS) 288 .setDuration(SHOW_DURATION_MS) 289 .setInterpolator(Interpolators.DEACCEL) 290 .start(); 291 return this; 292 } 293 updateArrowTipInView()294 private void updateArrowTipInView() { 295 ViewGroup.LayoutParams arrowLp = mArrowView.getLayoutParams(); 296 ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create( 297 arrowLp.width, arrowLp.height, mIsPointingUp)); 298 Paint arrowPaint = arrowDrawable.getPaint(); 299 @Px int arrowTipRadius = getContext().getResources() 300 .getDimensionPixelSize(R.dimen.arrow_toast_corner_radius); 301 arrowPaint.setColor(ContextCompat.getColor(getContext(), R.color.arrow_tip_view_bg)); 302 arrowPaint.setPathEffect(new CornerPathEffect(arrowTipRadius)); 303 mArrowView.setBackground(arrowDrawable); 304 // Add negative margin so that the rounded corners on base of arrow are not visible. 305 removeView(mArrowView); 306 if (mIsPointingUp) { 307 addView(mArrowView, 0); 308 ((ViewGroup.MarginLayoutParams) arrowLp).setMargins(0, 0, 0, -1 * arrowTipRadius); 309 } else { 310 addView(mArrowView, 1); 311 ((ViewGroup.MarginLayoutParams) arrowLp).setMargins(0, -1 * arrowTipRadius, 0, 0); 312 } 313 } 314 315 /** 316 * Register a callback fired when toast is hidden 317 */ setOnClosedCallback(Runnable runnable)318 public ArrowTipView setOnClosedCallback(Runnable runnable) { 319 mOnClosed = runnable; 320 return this; 321 } 322 323 @Override onConfigurationChanged(Configuration newConfig)324 protected void onConfigurationChanged(Configuration newConfig) { 325 super.onConfigurationChanged(newConfig); 326 close(/* animate= */ false); 327 } 328 } 329