1 /* 2 * Copyright (C) 2017 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 package com.android.launcher3.widget; 17 18 import static com.android.app.animation.Interpolators.EMPHASIZED; 19 import static com.android.launcher3.config.FeatureFlags.LARGE_SCREEN_WIDGET_PICKER; 20 21 import android.content.Context; 22 import android.graphics.Canvas; 23 import android.graphics.Paint; 24 import android.graphics.Point; 25 import android.graphics.Rect; 26 import android.util.AttributeSet; 27 import android.view.View; 28 import android.view.View.OnClickListener; 29 import android.view.View.OnLongClickListener; 30 import android.view.WindowInsets; 31 import android.view.animation.Interpolator; 32 import android.widget.Toast; 33 34 import androidx.annotation.Nullable; 35 import androidx.annotation.Px; 36 import androidx.core.view.ViewCompat; 37 38 import com.android.launcher3.DeviceProfile; 39 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; 40 import com.android.launcher3.DragSource; 41 import com.android.launcher3.DropTarget.DragObject; 42 import com.android.launcher3.Insettable; 43 import com.android.launcher3.Launcher; 44 import com.android.launcher3.R; 45 import com.android.launcher3.Utilities; 46 import com.android.launcher3.dragndrop.DragOptions; 47 import com.android.launcher3.popup.PopupDataProvider; 48 import com.android.launcher3.testing.TestLogging; 49 import com.android.launcher3.testing.shared.TestProtocol; 50 import com.android.launcher3.touch.ItemLongClickListener; 51 import com.android.launcher3.util.SystemUiController; 52 import com.android.launcher3.util.Themes; 53 import com.android.launcher3.util.window.WindowManagerProxy; 54 import com.android.launcher3.views.AbstractSlideInView; 55 import com.android.launcher3.views.ActivityContext; 56 import com.android.launcher3.views.ArrowTipView; 57 58 /** 59 * Base class for various widgets popup 60 */ 61 public abstract class BaseWidgetSheet extends AbstractSlideInView<Launcher> 62 implements OnClickListener, OnLongClickListener, DragSource, 63 PopupDataProvider.PopupDataChangeListener, Insettable, OnDeviceProfileChangeListener { 64 /** The default number of cells that can fit horizontally in a widget sheet. */ 65 public static final int DEFAULT_MAX_HORIZONTAL_SPANS = 4; 66 67 protected static final String KEY_WIDGETS_EDUCATION_TIP_SEEN = 68 "launcher.widgets_education_tip_seen"; 69 protected final Rect mInsets = new Rect(); 70 71 /* Touch handling related member variables. */ 72 private Toast mWidgetInstructionToast; 73 74 @Px protected int mContentHorizontalMargin; 75 @Px protected int mWidgetCellHorizontalPadding; 76 77 protected int mNavBarScrimHeight; 78 private final Paint mNavBarScrimPaint; 79 BaseWidgetSheet(Context context, AttributeSet attrs, int defStyleAttr)80 public BaseWidgetSheet(Context context, AttributeSet attrs, int defStyleAttr) { 81 super(context, attrs, defStyleAttr); 82 mContentHorizontalMargin = getResources().getDimensionPixelSize( 83 R.dimen.widget_list_horizontal_margin); 84 mWidgetCellHorizontalPadding = getResources().getDimensionPixelSize( 85 R.dimen.widget_cell_horizontal_padding); 86 mNavBarScrimPaint = new Paint(); 87 mNavBarScrimPaint.setColor(Themes.getNavBarScrimColor(mActivityContext)); 88 } 89 getScrimColor(Context context)90 protected int getScrimColor(Context context) { 91 return context.getResources().getColor(R.color.widgets_picker_scrim); 92 } 93 94 @Override onAttachedToWindow()95 protected void onAttachedToWindow() { 96 super.onAttachedToWindow(); 97 WindowInsets windowInsets = WindowManagerProxy.INSTANCE.get(getContext()) 98 .normalizeWindowInsets(getContext(), getRootWindowInsets(), new Rect()); 99 mNavBarScrimHeight = getNavBarScrimHeight(windowInsets); 100 mActivityContext.getPopupDataProvider().setChangeListener(this); 101 mActivityContext.addOnDeviceProfileChangeListener(this); 102 } 103 104 @Override onDetachedFromWindow()105 protected void onDetachedFromWindow() { 106 super.onDetachedFromWindow(); 107 mActivityContext.getPopupDataProvider().setChangeListener(null); 108 mActivityContext.removeOnDeviceProfileChangeListener(this); 109 } 110 111 @Override onDeviceProfileChanged(DeviceProfile dp)112 public void onDeviceProfileChanged(DeviceProfile dp) { 113 int navBarScrimColor = Themes.getNavBarScrimColor(mActivityContext); 114 if (mNavBarScrimPaint.getColor() != navBarScrimColor) { 115 mNavBarScrimPaint.setColor(navBarScrimColor); 116 invalidate(); 117 } 118 } 119 120 @Override onClick(View v)121 public final void onClick(View v) { 122 Object tag = null; 123 if (v instanceof WidgetCell) { 124 tag = v.getTag(); 125 } else if (v.getParent() instanceof WidgetCell) { 126 tag = ((WidgetCell) v.getParent()).getTag(); 127 } 128 if (tag instanceof PendingAddShortcutInfo) { 129 mWidgetInstructionToast = showShortcutToast(getContext(), mWidgetInstructionToast); 130 } else { 131 mWidgetInstructionToast = showWidgetToast(getContext(), mWidgetInstructionToast); 132 } 133 134 } 135 136 @Override onLongClick(View v)137 public boolean onLongClick(View v) { 138 TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "Widgets.onLongClick"); 139 v.cancelLongPress(); 140 if (!ItemLongClickListener.canStartDrag(mActivityContext)) return false; 141 142 if (v instanceof WidgetCell) { 143 return beginDraggingWidget((WidgetCell) v); 144 } else if (v.getParent() instanceof WidgetCell) { 145 return beginDraggingWidget((WidgetCell) v.getParent()); 146 } 147 return true; 148 } 149 150 @Override setInsets(Rect insets)151 public void setInsets(Rect insets) { 152 mInsets.set(insets); 153 @Px int contentHorizontalMargin = getResources().getDimensionPixelSize( 154 R.dimen.widget_list_horizontal_margin); 155 if (contentHorizontalMargin != mContentHorizontalMargin) { 156 onContentHorizontalMarginChanged(contentHorizontalMargin); 157 mContentHorizontalMargin = contentHorizontalMargin; 158 } 159 } 160 getNavBarScrimHeight(WindowInsets insets)161 private int getNavBarScrimHeight(WindowInsets insets) { 162 if (Utilities.ATLEAST_Q) { 163 return insets.getTappableElementInsets().bottom; 164 } else { 165 return insets.getStableInsetBottom(); 166 } 167 } 168 169 @Override onApplyWindowInsets(WindowInsets insets)170 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 171 mNavBarScrimHeight = getNavBarScrimHeight(insets); 172 return super.onApplyWindowInsets(insets); 173 } 174 175 @Override dispatchDraw(Canvas canvas)176 protected void dispatchDraw(Canvas canvas) { 177 super.dispatchDraw(canvas); 178 179 if (mNavBarScrimHeight > 0) { 180 canvas.drawRect(0, getHeight() - mNavBarScrimHeight, getWidth(), getHeight(), 181 mNavBarScrimPaint); 182 } 183 } 184 185 /** Called when the horizontal margin of the content view has changed. */ onContentHorizontalMarginChanged(int contentHorizontalMarginInPx)186 protected abstract void onContentHorizontalMarginChanged(int contentHorizontalMarginInPx); 187 188 /** 189 * Measures the dimension of this view and its children by taking system insets, navigation bar, 190 * status bar, into account. 191 */ doMeasure(int widthMeasureSpec, int heightMeasureSpec)192 protected void doMeasure(int widthMeasureSpec, int heightMeasureSpec) { 193 DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); 194 int widthUsed; 195 if (deviceProfile.isTablet) { 196 int margin = deviceProfile.allAppsLeftRightMargin; 197 if (deviceProfile.isLandscape 198 && LARGE_SCREEN_WIDGET_PICKER.get() 199 && !deviceProfile.isTwoPanels) { 200 margin = getResources().getDimensionPixelSize( 201 R.dimen.widget_picker_landscape_tablet_left_right_margin); 202 } 203 widthUsed = Math.max(2 * margin, 2 * (mInsets.left + mInsets.right)); 204 } else if (mInsets.bottom > 0) { 205 widthUsed = mInsets.left + mInsets.right; 206 } else { 207 Rect padding = deviceProfile.workspacePadding; 208 widthUsed = Math.max(padding.left + padding.right, 209 2 * (mInsets.left + mInsets.right)); 210 } 211 212 measureChildWithMargins(mContent, widthMeasureSpec, 213 widthUsed, heightMeasureSpec, deviceProfile.bottomSheetTopPadding); 214 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), 215 MeasureSpec.getSize(heightMeasureSpec)); 216 } 217 beginDraggingWidget(WidgetCell v)218 private boolean beginDraggingWidget(WidgetCell v) { 219 // Get the widget preview as the drag representation 220 WidgetImageView image = v.getWidgetView(); 221 222 // If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and 223 // we abort the drag. 224 if (image.getDrawable() == null && v.getAppWidgetHostViewPreview() == null) { 225 return false; 226 } 227 228 PendingItemDragHelper dragHelper = new PendingItemDragHelper(v); 229 // RemoteViews are being rendered in AppWidgetHostView in WidgetCell. And thus, the scale of 230 // RemoteViews is equivalent to the AppWidgetHostView scale. 231 dragHelper.setRemoteViewsPreview(v.getRemoteViewsPreview(), v.getAppWidgetHostViewScale()); 232 dragHelper.setAppWidgetHostViewPreview(v.getAppWidgetHostViewPreview()); 233 234 if (image.getDrawable() != null) { 235 int[] loc = new int[2]; 236 getPopupContainer().getLocationInDragLayer(image, loc); 237 238 dragHelper.startDrag(image.getBitmapBounds(), image.getDrawable().getIntrinsicWidth(), 239 image.getWidth(), new Point(loc[0], loc[1]), this, new DragOptions()); 240 } else { 241 NavigableAppWidgetHostView preview = v.getAppWidgetHostViewPreview(); 242 int[] loc = new int[2]; 243 getPopupContainer().getLocationInDragLayer(preview, loc); 244 Rect r = new Rect(); 245 preview.getWorkspaceVisualDragBounds(r); 246 dragHelper.startDrag(r, preview.getMeasuredWidth(), preview.getMeasuredWidth(), 247 new Point(loc[0], loc[1]), this, new DragOptions()); 248 } 249 close(true); 250 return true; 251 } 252 253 @Override getIdleInterpolator()254 protected Interpolator getIdleInterpolator() { 255 return mActivityContext.getDeviceProfile().isTablet 256 ? EMPHASIZED : super.getIdleInterpolator(); 257 } 258 259 // 260 // Drag related handling methods that implement {@link DragSource} interface. 261 // 262 263 @Override onDropCompleted(View target, DragObject d, boolean success)264 public void onDropCompleted(View target, DragObject d, boolean success) { } 265 266 onCloseComplete()267 protected void onCloseComplete() { 268 super.onCloseComplete(); 269 clearNavBarColor(); 270 } 271 clearNavBarColor()272 protected void clearNavBarColor() { 273 getSystemUiController().updateUiState( 274 SystemUiController.UI_STATE_WIDGET_BOTTOM_SHEET, 0); 275 } 276 setupNavBarColor()277 protected void setupNavBarColor() { 278 boolean isSheetDark = Themes.getAttrBoolean(getContext(), R.attr.isMainColorDark); 279 getSystemUiController().updateUiState( 280 SystemUiController.UI_STATE_WIDGET_BOTTOM_SHEET, 281 isSheetDark ? SystemUiController.FLAG_DARK_NAV : SystemUiController.FLAG_LIGHT_NAV); 282 } 283 getSystemUiController()284 protected SystemUiController getSystemUiController() { 285 return mActivityContext.getSystemUiController(); 286 } 287 288 /** 289 * Show Widget tap toast prompting user to drag instead 290 */ showWidgetToast(Context context, Toast toast)291 public static Toast showWidgetToast(Context context, Toast toast) { 292 // Let the user know that they have to long press to add a widget 293 if (toast != null) { 294 toast.cancel(); 295 } 296 297 CharSequence msg = Utilities.wrapForTts( 298 context.getText(R.string.long_press_widget_to_add), 299 context.getString(R.string.long_accessible_way_to_add)); 300 toast = Toast.makeText(context, msg, Toast.LENGTH_SHORT); 301 toast.show(); 302 return toast; 303 } 304 305 /** 306 * Show shortcut tap toast prompting user to drag instead. 307 */ showShortcutToast(Context context, Toast toast)308 private static Toast showShortcutToast(Context context, Toast toast) { 309 // Let the user know that they have to long press to add a widget 310 if (toast != null) { 311 toast.cancel(); 312 } 313 314 CharSequence msg = Utilities.wrapForTts( 315 context.getText(R.string.long_press_shortcut_to_add), 316 context.getString(R.string.long_accessible_way_to_add_shortcut)); 317 toast = Toast.makeText(context, msg, Toast.LENGTH_SHORT); 318 toast.show(); 319 return toast; 320 } 321 322 /** Shows education tip on top center of {@code view} if view is laid out. */ 323 @Nullable showEducationTipOnViewIfPossible(@ullable View view)324 protected ArrowTipView showEducationTipOnViewIfPossible(@Nullable View view) { 325 if (view == null || !ViewCompat.isLaidOut(view)) { 326 return null; 327 } 328 int[] coords = new int[2]; 329 view.getLocationOnScreen(coords); 330 ArrowTipView arrowTipView = 331 new ArrowTipView(mActivityContext, /* isPointingUp= */ false).showAtLocation( 332 getContext().getString(R.string.long_press_widget_to_add), 333 /* arrowXCoord= */coords[0] + view.getWidth() / 2, 334 /* yCoord= */coords[1]); 335 if (arrowTipView != null) { 336 mActivityContext.getSharedPrefs().edit() 337 .putBoolean(KEY_WIDGETS_EDUCATION_TIP_SEEN, true).apply(); 338 } 339 return arrowTipView; 340 } 341 342 /** Returns {@code true} if tip has previously been shown on any of {@link BaseWidgetSheet}. */ hasSeenEducationTip()343 protected boolean hasSeenEducationTip() { 344 return mActivityContext.getSharedPrefs().getBoolean(KEY_WIDGETS_EDUCATION_TIP_SEEN, false) 345 || Utilities.isRunningInTestHarness(); 346 } 347 348 @Override setTranslationShift(float translationShift)349 protected void setTranslationShift(float translationShift) { 350 super.setTranslationShift(translationShift); 351 Launcher launcher = ActivityContext.lookupContext(getContext()); 352 launcher.onWidgetsTransition(1 - translationShift); 353 } 354 } 355