1 package com.android.launcher3; 2 3 import static com.android.launcher3.CellLayout.SPRING_LOADED_PROGRESS; 4 import static com.android.launcher3.LauncherAnimUtils.LAYOUT_HEIGHT; 5 import static com.android.launcher3.LauncherAnimUtils.LAYOUT_WIDTH; 6 import static com.android.launcher3.LauncherPrefs.RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN; 7 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_RESIZE_COMPLETED; 8 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_RESIZE_STARTED; 9 import static com.android.launcher3.views.BaseDragLayer.LAYOUT_X; 10 import static com.android.launcher3.views.BaseDragLayer.LAYOUT_Y; 11 12 import android.animation.Animator; 13 import android.animation.AnimatorListenerAdapter; 14 import android.animation.AnimatorSet; 15 import android.animation.LayoutTransition; 16 import android.animation.ObjectAnimator; 17 import android.animation.PropertyValuesHolder; 18 import android.appwidget.AppWidgetProviderInfo; 19 import android.content.Context; 20 import android.graphics.Rect; 21 import android.graphics.drawable.Drawable; 22 import android.graphics.drawable.GradientDrawable; 23 import android.util.AttributeSet; 24 import android.view.KeyEvent; 25 import android.view.MotionEvent; 26 import android.view.View; 27 import android.view.ViewGroup; 28 import android.widget.ImageButton; 29 import android.widget.ImageView; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 import androidx.annotation.Px; 34 35 import com.android.launcher3.LauncherConstants.ActivityCodes; 36 import com.android.launcher3.accessibility.DragViewStateAnnouncer; 37 import com.android.launcher3.celllayout.CellLayoutLayoutParams; 38 import com.android.launcher3.celllayout.CellPosMapper.CellPos; 39 import com.android.launcher3.config.FeatureFlags; 40 import com.android.launcher3.dragndrop.DragLayer; 41 import com.android.launcher3.keyboard.ViewGroupFocusHelper; 42 import com.android.launcher3.logging.InstanceId; 43 import com.android.launcher3.logging.InstanceIdSequence; 44 import com.android.launcher3.model.data.ItemInfo; 45 import com.android.launcher3.util.PendingRequestArgs; 46 import com.android.launcher3.views.ArrowTipView; 47 import com.android.launcher3.widget.LauncherAppWidgetHostView; 48 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; 49 import com.android.launcher3.widget.PendingAppWidgetHostView; 50 import com.android.launcher3.widget.util.WidgetSizes; 51 52 import java.util.ArrayList; 53 import java.util.List; 54 55 public class AppWidgetResizeFrame extends AbstractFloatingView implements View.OnKeyListener { 56 private static final int SNAP_DURATION_MS = 150; 57 private static final float DIMMED_HANDLE_ALPHA = 0f; 58 private static final float RESIZE_THRESHOLD = 0.66f; 59 private static final int RESIZE_TRANSITION_DURATION_MS = 150; 60 61 private static final Rect sTmpRect = new Rect(); 62 private static final Rect sTmpRect2 = new Rect(); 63 64 private static final int[] sDragLayerLoc = new int[2]; 65 66 private static final int HANDLE_COUNT = 4; 67 private static final int INDEX_LEFT = 0; 68 private static final int INDEX_TOP = 1; 69 private static final int INDEX_RIGHT = 2; 70 private static final int INDEX_BOTTOM = 3; 71 private static final float MIN_OPACITY_FOR_CELL_LAYOUT_DURING_INVALID_RESIZE = 0.5f; 72 73 private final Launcher mLauncher; 74 private final DragViewStateAnnouncer mStateAnnouncer; 75 private final FirstFrameAnimatorHelper mFirstFrameAnimatorHelper; 76 77 private final View[] mDragHandles = new View[HANDLE_COUNT]; 78 private final List<Rect> mSystemGestureExclusionRects = new ArrayList<>(HANDLE_COUNT); 79 80 private LauncherAppWidgetHostView mWidgetView; 81 private CellLayout mCellLayout; 82 private DragLayer mDragLayer; 83 private ImageButton mReconfigureButton; 84 85 private final int mBackgroundPadding; 86 private final int mTouchTargetWidth; 87 88 private final int[] mDirectionVector = new int[2]; 89 private final int[] mLastDirectionVector = new int[2]; 90 91 private final IntRange mTempRange1 = new IntRange(); 92 private final IntRange mTempRange2 = new IntRange(); 93 94 private final IntRange mDeltaXRange = new IntRange(); 95 private final IntRange mBaselineX = new IntRange(); 96 97 private final IntRange mDeltaYRange = new IntRange(); 98 private final IntRange mBaselineY = new IntRange(); 99 100 private final InstanceId logInstanceId = new InstanceIdSequence().newInstanceId(); 101 102 private final ViewGroupFocusHelper mDragLayerRelativeCoordinateHelper; 103 104 /** 105 * In the two panel UI, it is not possible to resize a widget to cross its host 106 * {@link CellLayout}'s sibling. When this happens, we gradually reduce the opacity of the 107 * sibling {@link CellLayout} from 1f to 108 * {@link #MIN_OPACITY_FOR_CELL_LAYOUT_DURING_INVALID_RESIZE}. 109 */ 110 private final float mDragAcrossTwoPanelOpacityMargin; 111 112 private boolean mLeftBorderActive; 113 private boolean mRightBorderActive; 114 private boolean mTopBorderActive; 115 private boolean mBottomBorderActive; 116 117 private boolean mHorizontalResizeActive; 118 private boolean mVerticalResizeActive; 119 120 private int mRunningHInc; 121 private int mRunningVInc; 122 private int mMinHSpan; 123 private int mMinVSpan; 124 private int mMaxHSpan; 125 private int mMaxVSpan; 126 private int mDeltaX; 127 private int mDeltaY; 128 private int mDeltaXAddOn; 129 private int mDeltaYAddOn; 130 131 private int mTopTouchRegionAdjustment = 0; 132 private int mBottomTouchRegionAdjustment = 0; 133 134 private int[] mWidgetViewWindowPos; 135 private final Rect mWidgetViewOldRect = new Rect(); 136 private final Rect mWidgetViewNewRect = new Rect(); 137 private final @Nullable LauncherAppWidgetHostView.CellChildViewPreLayoutListener 138 mCellChildViewPreLayoutListener; 139 private final @NonNull OnLayoutChangeListener mWidgetViewLayoutListener; 140 141 private int mXDown, mYDown; 142 AppWidgetResizeFrame(Context context)143 public AppWidgetResizeFrame(Context context) { 144 this(context, null); 145 } 146 AppWidgetResizeFrame(Context context, AttributeSet attrs)147 public AppWidgetResizeFrame(Context context, AttributeSet attrs) { 148 this(context, attrs, 0); 149 } 150 AppWidgetResizeFrame(Context context, AttributeSet attrs, int defStyleAttr)151 public AppWidgetResizeFrame(Context context, AttributeSet attrs, int defStyleAttr) { 152 super(context, attrs, defStyleAttr); 153 154 mLauncher = Launcher.getLauncher(context); 155 mStateAnnouncer = DragViewStateAnnouncer.createFor(this); 156 157 mCellChildViewPreLayoutListener = FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get() 158 ? (v, left, top, right, bottom) -> { 159 if (mWidgetViewWindowPos == null) { 160 mWidgetViewWindowPos = new int[2]; 161 } 162 v.getLocationInWindow(mWidgetViewWindowPos); 163 mWidgetViewOldRect.set(v.getLeft(), v.getTop(), v.getRight(), 164 v.getBottom()); 165 mWidgetViewNewRect.set(left, top, right, bottom); 166 } 167 : null; 168 169 mBackgroundPadding = getResources() 170 .getDimensionPixelSize(R.dimen.resize_frame_background_padding); 171 mTouchTargetWidth = 2 * mBackgroundPadding; 172 mFirstFrameAnimatorHelper = new FirstFrameAnimatorHelper(this); 173 174 for (int i = 0; i < HANDLE_COUNT; i++) { 175 mSystemGestureExclusionRects.add(new Rect()); 176 } 177 178 mDragAcrossTwoPanelOpacityMargin = mLauncher.getResources().getDimensionPixelSize( 179 R.dimen.resize_frame_invalid_drag_across_two_panel_opacity_margin); 180 mDragLayerRelativeCoordinateHelper = new ViewGroupFocusHelper(mLauncher.getDragLayer()); 181 182 mWidgetViewLayoutListener = 183 (v, l, t, r, b, oldL, oldT, oldR, oldB) -> setCornerRadiusFromWidget(); 184 } 185 186 @Override onFinishInflate()187 protected void onFinishInflate() { 188 super.onFinishInflate(); 189 190 mDragHandles[INDEX_LEFT] = findViewById(R.id.widget_resize_left_handle); 191 mDragHandles[INDEX_TOP] = findViewById(R.id.widget_resize_top_handle); 192 mDragHandles[INDEX_RIGHT] = findViewById(R.id.widget_resize_right_handle); 193 mDragHandles[INDEX_BOTTOM] = findViewById(R.id.widget_resize_bottom_handle); 194 } 195 196 @Override onLayout(boolean changed, int l, int t, int r, int b)197 protected void onLayout(boolean changed, int l, int t, int r, int b) { 198 super.onLayout(changed, l, t, r, b); 199 for (int i = 0; i < HANDLE_COUNT; i++) { 200 View dragHandle = mDragHandles[i]; 201 mSystemGestureExclusionRects.get(i).set(dragHandle.getLeft(), dragHandle.getTop(), 202 dragHandle.getRight(), dragHandle.getBottom()); 203 } 204 setSystemGestureExclusionRects(mSystemGestureExclusionRects); 205 } 206 showForWidget(LauncherAppWidgetHostView widget, CellLayout cellLayout)207 public static void showForWidget(LauncherAppWidgetHostView widget, CellLayout cellLayout) { 208 // If widget is not added to view hierarchy, we cannot show resize frame at correct location 209 if (widget.getParent() == null) { 210 return; 211 } 212 Launcher launcher = Launcher.getLauncher(cellLayout.getContext()); 213 AbstractFloatingView.closeAllOpenViews(launcher); 214 215 DragLayer dl = launcher.getDragLayer(); 216 AppWidgetResizeFrame frame = (AppWidgetResizeFrame) launcher.getLayoutInflater() 217 .inflate(R.layout.app_widget_resize_frame, dl, false); 218 frame.setupForWidget(widget, cellLayout, dl); 219 ((DragLayer.LayoutParams) frame.getLayoutParams()).customPosition = true; 220 221 dl.addView(frame); 222 frame.mIsOpen = true; 223 frame.post(() -> frame.snapToWidget(false)); 224 } 225 setCornerRadiusFromWidget()226 private void setCornerRadiusFromWidget() { 227 if (mWidgetView != null && mWidgetView.hasEnforcedCornerRadius()) { 228 float enforcedCornerRadius = mWidgetView.getEnforcedCornerRadius(); 229 ImageView imageView = findViewById(R.id.widget_resize_frame); 230 Drawable d = imageView.getDrawable(); 231 if (d instanceof GradientDrawable) { 232 GradientDrawable gd = (GradientDrawable) d.mutate(); 233 gd.setCornerRadius(enforcedCornerRadius); 234 } 235 } 236 } 237 setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout, DragLayer dragLayer)238 private void setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout, 239 DragLayer dragLayer) { 240 mCellLayout = cellLayout; 241 mWidgetView = widgetView; 242 LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) 243 widgetView.getAppWidgetInfo(); 244 mDragLayer = dragLayer; 245 246 mMinHSpan = info.minSpanX; 247 mMinVSpan = info.minSpanY; 248 mMaxHSpan = info.maxSpanX; 249 mMaxVSpan = info.maxSpanY; 250 251 // Only show resize handles for the directions in which resizing is possible. 252 InvariantDeviceProfile idp = LauncherAppState.getIDP(cellLayout.getContext()); 253 mVerticalResizeActive = (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0 254 && mMinVSpan < idp.numRows && mMaxVSpan > 1 255 && mMinVSpan < mMaxVSpan; 256 if (!mVerticalResizeActive) { 257 mDragHandles[INDEX_TOP].setVisibility(GONE); 258 mDragHandles[INDEX_BOTTOM].setVisibility(GONE); 259 } 260 mHorizontalResizeActive = (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0 261 && mMinHSpan < idp.numColumns && mMaxHSpan > 1 262 && mMinHSpan < mMaxHSpan; 263 if (!mHorizontalResizeActive) { 264 mDragHandles[INDEX_LEFT].setVisibility(GONE); 265 mDragHandles[INDEX_RIGHT].setVisibility(GONE); 266 } 267 268 mReconfigureButton = (ImageButton) findViewById(R.id.widget_reconfigure_button); 269 if (info.isReconfigurable()) { 270 mReconfigureButton.setVisibility(VISIBLE); 271 mReconfigureButton.setOnClickListener(view -> { 272 mLauncher.setWaitingForResult( 273 PendingRequestArgs.forWidgetInfo( 274 mWidgetView.getAppWidgetId(), 275 // Widget add handler is null since we're reconfiguring an existing 276 // widget. 277 /* widgetHandler= */ null, 278 (ItemInfo) mWidgetView.getTag())); 279 mLauncher 280 .getAppWidgetHolder() 281 .startConfigActivity( 282 mLauncher, 283 mWidgetView.getAppWidgetId(), 284 ActivityCodes.REQUEST_RECONFIGURE_APPWIDGET); 285 }); 286 if (!hasSeenReconfigurableWidgetEducationTip()) { 287 post(() -> { 288 if (showReconfigurableWidgetEducationTip() != null) { 289 LauncherPrefs.get(getContext()).put( 290 RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN, true); 291 } 292 }); 293 } 294 } 295 296 if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()) { 297 mWidgetView.setCellChildViewPreLayoutListener(mCellChildViewPreLayoutListener); 298 mWidgetViewOldRect.set(mWidgetView.getLeft(), mWidgetView.getTop(), 299 mWidgetView.getRight(), 300 mWidgetView.getBottom()); 301 mWidgetViewNewRect.set(mWidgetViewOldRect); 302 } 303 304 CellLayoutLayoutParams lp = (CellLayoutLayoutParams) mWidgetView.getLayoutParams(); 305 ItemInfo widgetInfo = (ItemInfo) mWidgetView.getTag(); 306 CellPos presenterPos = mLauncher.getCellPosMapper().mapModelToPresenter(widgetInfo); 307 lp.setCellX(presenterPos.cellX); 308 lp.setTmpCellX(presenterPos.cellX); 309 lp.setCellY(presenterPos.cellY); 310 lp.setTmpCellY(presenterPos.cellY); 311 lp.cellHSpan = widgetInfo.spanX; 312 lp.cellVSpan = widgetInfo.spanY; 313 lp.isLockedToGrid = true; 314 315 // When we create the resize frame, we first mark all cells as unoccupied. The appropriate 316 // cells (same if not resized, or different) will be marked as occupied when the resize 317 // frame is dismissed. 318 mCellLayout.markCellsAsUnoccupiedForView(mWidgetView); 319 320 mLauncher.getStatsLogManager() 321 .logger() 322 .withInstanceId(logInstanceId) 323 .withItemInfo(widgetInfo) 324 .log(LAUNCHER_WIDGET_RESIZE_STARTED); 325 326 setOnKeyListener(this); 327 328 setCornerRadiusFromWidget(); 329 mWidgetView.addOnLayoutChangeListener(mWidgetViewLayoutListener); 330 } 331 332 public boolean beginResizeIfPointInRegion(int x, int y) { 333 mLeftBorderActive = (x < mTouchTargetWidth) && mHorizontalResizeActive; 334 mRightBorderActive = (x > getWidth() - mTouchTargetWidth) && mHorizontalResizeActive; 335 mTopBorderActive = (y < mTouchTargetWidth + mTopTouchRegionAdjustment) 336 && mVerticalResizeActive; 337 mBottomBorderActive = (y > getHeight() - mTouchTargetWidth + mBottomTouchRegionAdjustment) 338 && mVerticalResizeActive; 339 340 boolean anyBordersActive = mLeftBorderActive || mRightBorderActive 341 || mTopBorderActive || mBottomBorderActive; 342 343 if (anyBordersActive) { 344 mDragHandles[INDEX_LEFT].setAlpha(mLeftBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); 345 mDragHandles[INDEX_RIGHT].setAlpha(mRightBorderActive ? 1.0f :DIMMED_HANDLE_ALPHA); 346 mDragHandles[INDEX_TOP].setAlpha(mTopBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); 347 mDragHandles[INDEX_BOTTOM].setAlpha(mBottomBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); 348 } 349 350 if (mLeftBorderActive) { 351 mDeltaXRange.set(-getLeft(), getWidth() - 2 * mTouchTargetWidth); 352 } else if (mRightBorderActive) { 353 mDeltaXRange.set(2 * mTouchTargetWidth - getWidth(), mDragLayer.getWidth() - getRight()); 354 } else { 355 mDeltaXRange.set(0, 0); 356 } 357 mBaselineX.set(getLeft(), getRight()); 358 359 if (mTopBorderActive) { 360 mDeltaYRange.set(-getTop(), getHeight() - 2 * mTouchTargetWidth); 361 } else if (mBottomBorderActive) { 362 mDeltaYRange.set(2 * mTouchTargetWidth - getHeight(), mDragLayer.getHeight() - getBottom()); 363 } else { 364 mDeltaYRange.set(0, 0); 365 } 366 mBaselineY.set(getTop(), getBottom()); 367 368 return anyBordersActive; 369 } 370 371 /** 372 * Based on the deltas, we resize the frame. 373 */ 374 public void visualizeResizeForDelta(int deltaX, int deltaY) { 375 mDeltaX = mDeltaXRange.clamp(deltaX); 376 mDeltaY = mDeltaYRange.clamp(deltaY); 377 378 DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); 379 mDeltaX = mDeltaXRange.clamp(deltaX); 380 mBaselineX.applyDelta(mLeftBorderActive, mRightBorderActive, mDeltaX, mTempRange1); 381 lp.x = mTempRange1.start; 382 lp.width = mTempRange1.size(); 383 384 mDeltaY = mDeltaYRange.clamp(deltaY); 385 mBaselineY.applyDelta(mTopBorderActive, mBottomBorderActive, mDeltaY, mTempRange1); 386 lp.y = mTempRange1.start; 387 lp.height = mTempRange1.size(); 388 389 resizeWidgetIfNeeded(false); 390 391 // Handle invalid resize across CellLayouts in the two panel UI. 392 if (mCellLayout.getParent() instanceof Workspace) { 393 Workspace<?> workspace = (Workspace<?>) mCellLayout.getParent(); 394 CellLayout pairedCellLayout = workspace.getScreenPair(mCellLayout); 395 if (pairedCellLayout != null) { 396 Rect focusedCellLayoutBound = sTmpRect; 397 mDragLayerRelativeCoordinateHelper.viewToRect(mCellLayout, focusedCellLayoutBound); 398 Rect resizeFrameBound = sTmpRect2; 399 findViewById(R.id.widget_resize_frame).getGlobalVisibleRect(resizeFrameBound); 400 float progress = 1f; 401 if (workspace.indexOfChild(pairedCellLayout) < workspace.indexOfChild(mCellLayout) 402 && mDeltaX < 0 403 && resizeFrameBound.left < focusedCellLayoutBound.left) { 404 // Resize from right to left. 405 progress = (mDragAcrossTwoPanelOpacityMargin + mDeltaX) 406 / mDragAcrossTwoPanelOpacityMargin; 407 } else if (workspace.indexOfChild(pairedCellLayout) 408 > workspace.indexOfChild(mCellLayout) 409 && mDeltaX > 0 410 && resizeFrameBound.right > focusedCellLayoutBound.right) { 411 // Resize from left to right. 412 progress = (mDragAcrossTwoPanelOpacityMargin - mDeltaX) 413 / mDragAcrossTwoPanelOpacityMargin; 414 } 415 float alpha = Math.max(MIN_OPACITY_FOR_CELL_LAYOUT_DURING_INVALID_RESIZE, progress); 416 float springLoadedProgress = Math.min(1f, 1f - progress); 417 updateInvalidResizeEffect(mCellLayout, pairedCellLayout, alpha, 418 springLoadedProgress); 419 } 420 } 421 422 requestLayout(); 423 } 424 425 private static int getSpanIncrement(float deltaFrac) { 426 return Math.abs(deltaFrac) > RESIZE_THRESHOLD ? Math.round(deltaFrac) : 0; 427 } 428 429 /** 430 * Based on the current deltas, we determine if and how to resize the widget. 431 */ 432 private void resizeWidgetIfNeeded(boolean onDismiss) { 433 ViewGroup.LayoutParams wlp = mWidgetView.getLayoutParams(); 434 if (!(wlp instanceof CellLayoutLayoutParams)) { 435 return; 436 } 437 DeviceProfile dp = mLauncher.getDeviceProfile(); 438 float xThreshold = mCellLayout.getCellWidth() + dp.cellLayoutBorderSpacePx.x; 439 float yThreshold = mCellLayout.getCellHeight() + dp.cellLayoutBorderSpacePx.y; 440 441 int hSpanInc = getSpanIncrement((mDeltaX + mDeltaXAddOn) / xThreshold - mRunningHInc); 442 int vSpanInc = getSpanIncrement((mDeltaY + mDeltaYAddOn) / yThreshold - mRunningVInc); 443 444 if (!onDismiss && (hSpanInc == 0 && vSpanInc == 0)) return; 445 446 mDirectionVector[0] = 0; 447 mDirectionVector[1] = 0; 448 449 CellLayoutLayoutParams lp = (CellLayoutLayoutParams) wlp; 450 451 int spanX = lp.cellHSpan; 452 int spanY = lp.cellVSpan; 453 int cellX = lp.useTmpCoords ? lp.getTmpCellX() : lp.getCellX(); 454 int cellY = lp.useTmpCoords ? lp.getTmpCellY() : lp.getCellY(); 455 456 // For each border, we bound the resizing based on the minimum width, and the maximum 457 // expandability. 458 mTempRange1.set(cellX, spanX + cellX); 459 int hSpanDelta = mTempRange1.applyDeltaAndBound(mLeftBorderActive, mRightBorderActive, 460 hSpanInc, mMinHSpan, mMaxHSpan, mCellLayout.getCountX(), mTempRange2); 461 cellX = mTempRange2.start; 462 spanX = mTempRange2.size(); 463 if (hSpanDelta != 0) { 464 mDirectionVector[0] = mLeftBorderActive ? -1 : 1; 465 } 466 467 mTempRange1.set(cellY, spanY + cellY); 468 int vSpanDelta = mTempRange1.applyDeltaAndBound(mTopBorderActive, mBottomBorderActive, 469 vSpanInc, mMinVSpan, mMaxVSpan, mCellLayout.getCountY(), mTempRange2); 470 cellY = mTempRange2.start; 471 spanY = mTempRange2.size(); 472 if (vSpanDelta != 0) { 473 mDirectionVector[1] = mTopBorderActive ? -1 : 1; 474 } 475 476 if (!onDismiss && vSpanDelta == 0 && hSpanDelta == 0) return; 477 478 // We always want the final commit to match the feedback, so we make sure to use the 479 // last used direction vector when committing the resize / reorder. 480 if (onDismiss) { 481 mDirectionVector[0] = mLastDirectionVector[0]; 482 mDirectionVector[1] = mLastDirectionVector[1]; 483 } else { 484 mLastDirectionVector[0] = mDirectionVector[0]; 485 mLastDirectionVector[1] = mDirectionVector[1]; 486 } 487 488 // We don't want to evaluate resize if a widget was pending config activity and was already 489 // occupying a space on the screen. This otherwise will cause reorder algorithm evaluate a 490 // different location for the widget and cause a jump. 491 if (!(mWidgetView instanceof PendingAppWidgetHostView) && mCellLayout.createAreaForResize( 492 cellX, cellY, spanX, spanY, mWidgetView, mDirectionVector, onDismiss)) { 493 if (mStateAnnouncer != null && (lp.cellHSpan != spanX || lp.cellVSpan != spanY) ) { 494 mStateAnnouncer.announce( 495 mLauncher.getString(R.string.widget_resized, spanX, spanY)); 496 } 497 498 lp.setTmpCellX(cellX); 499 lp.setTmpCellY(cellY); 500 lp.cellHSpan = spanX; 501 lp.cellVSpan = spanY; 502 mRunningVInc += vSpanDelta; 503 mRunningHInc += hSpanDelta; 504 505 if (!onDismiss) { 506 WidgetSizes.updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY); 507 } 508 } 509 mWidgetView.requestLayout(); 510 } 511 512 @Override 513 protected void onDetachedFromWindow() { 514 super.onDetachedFromWindow(); 515 516 // We are done with resizing the widget. Save the widget size & position to LauncherModel 517 resizeWidgetIfNeeded(true); 518 mLauncher.getStatsLogManager() 519 .logger() 520 .withInstanceId(logInstanceId) 521 .withItemInfo((ItemInfo) mWidgetView.getTag()) 522 .log(LAUNCHER_WIDGET_RESIZE_COMPLETED); 523 } 524 525 private void onTouchUp() { 526 DeviceProfile dp = mLauncher.getDeviceProfile(); 527 int xThreshold = mCellLayout.getCellWidth() + dp.cellLayoutBorderSpacePx.x; 528 int yThreshold = mCellLayout.getCellHeight() + dp.cellLayoutBorderSpacePx.y; 529 530 mDeltaXAddOn = mRunningHInc * xThreshold; 531 mDeltaYAddOn = mRunningVInc * yThreshold; 532 mDeltaX = 0; 533 mDeltaY = 0; 534 535 post(() -> snapToWidget(true)); 536 } 537 538 /** 539 * Returns the rect of this view when the frame is snapped around the widget, with the bounds 540 * relative to the {@link DragLayer}. 541 */ 542 private void getSnappedRectRelativeToDragLayer(@NonNull Rect out) { 543 float scale = mWidgetView.getScaleToFit(); 544 if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()) { 545 getViewRectRelativeToDragLayer(out); 546 } else { 547 mDragLayer.getViewRectRelativeToSelf(mWidgetView, out); 548 } 549 550 int width = 2 * mBackgroundPadding + Math.round(scale * out.width()); 551 int height = 2 * mBackgroundPadding + Math.round(scale * out.height()); 552 int x = out.left - mBackgroundPadding; 553 int y = out.top - mBackgroundPadding; 554 555 out.left = x; 556 out.top = y; 557 out.right = out.left + width; 558 out.bottom = out.top + height; 559 } 560 561 private void getViewRectRelativeToDragLayer(@NonNull Rect out) { 562 int[] afterPos = getViewPosRelativeToDragLayer(); 563 out.set(afterPos[0], afterPos[1], afterPos[0] + mWidgetViewNewRect.width(), 564 afterPos[1] + mWidgetViewNewRect.height()); 565 } 566 567 /** Returns the relative x and y values of the widget view after the layout transition */ 568 private int[] getViewPosRelativeToDragLayer() { 569 mDragLayer.getLocationInWindow(sDragLayerLoc); 570 int x = sDragLayerLoc[0]; 571 int y = sDragLayerLoc[1]; 572 573 if (mWidgetViewWindowPos == null) { 574 mWidgetViewWindowPos = new int[2]; 575 mWidgetView.getLocationInWindow(mWidgetViewWindowPos); 576 } 577 578 int leftOffset = mWidgetViewNewRect.left - mWidgetViewOldRect.left; 579 int topOffset = mWidgetViewNewRect.top - mWidgetViewOldRect.top; 580 581 return new int[] {mWidgetViewWindowPos[0] - x + leftOffset, 582 mWidgetViewWindowPos[1] - y + topOffset}; 583 } 584 585 private void snapToWidget(boolean animate) { 586 // The widget is guaranteed to be attached to the cell layout at this point, thus setting 587 // the transition here 588 if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get() 589 && mWidgetView.getLayoutTransition() == null) { 590 final LayoutTransition transition = new LayoutTransition(); 591 transition.setDuration(RESIZE_TRANSITION_DURATION_MS); 592 transition.enableTransitionType(LayoutTransition.CHANGING); 593 mWidgetView.setLayoutTransition(transition); 594 } 595 596 getSnappedRectRelativeToDragLayer(sTmpRect); 597 int newWidth = sTmpRect.width(); 598 int newHeight = sTmpRect.height(); 599 int newX = sTmpRect.left; 600 int newY = sTmpRect.top; 601 602 // We need to make sure the frame's touchable regions lie fully within the bounds of the 603 // DragLayer. We allow the actual handles to be clipped, but we shift the touch regions 604 // down accordingly to provide a proper touch target. 605 if (newY < 0) { 606 // In this case we shift the touch region down to start at the top of the DragLayer 607 mTopTouchRegionAdjustment = -newY; 608 } else { 609 mTopTouchRegionAdjustment = 0; 610 } 611 if (newY + newHeight > mDragLayer.getHeight()) { 612 // In this case we shift the touch region up to end at the bottom of the DragLayer 613 mBottomTouchRegionAdjustment = -(newY + newHeight - mDragLayer.getHeight()); 614 } else { 615 mBottomTouchRegionAdjustment = 0; 616 } 617 618 final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); 619 final CellLayout pairedCellLayout; 620 if (mCellLayout.getParent() instanceof Workspace) { 621 Workspace<?> workspace = (Workspace<?>) mCellLayout.getParent(); 622 pairedCellLayout = workspace.getScreenPair(mCellLayout); 623 } else { 624 pairedCellLayout = null; 625 } 626 if (!animate) { 627 lp.width = newWidth; 628 lp.height = newHeight; 629 lp.x = newX; 630 lp.y = newY; 631 for (int i = 0; i < HANDLE_COUNT; i++) { 632 mDragHandles[i].setAlpha(1f); 633 } 634 if (pairedCellLayout != null) { 635 updateInvalidResizeEffect(mCellLayout, pairedCellLayout, /* alpha= */ 1f, 636 /* springLoadedProgress= */ 0f); 637 } 638 requestLayout(); 639 } else { 640 ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(lp, 641 PropertyValuesHolder.ofInt(LAYOUT_WIDTH, lp.width, newWidth), 642 PropertyValuesHolder.ofInt(LAYOUT_HEIGHT, lp.height, newHeight), 643 PropertyValuesHolder.ofInt(LAYOUT_X, lp.x, newX), 644 PropertyValuesHolder.ofInt(LAYOUT_Y, lp.y, newY)); 645 mFirstFrameAnimatorHelper.addTo(oa).addUpdateListener(a -> requestLayout()); 646 647 AnimatorSet set = new AnimatorSet(); 648 set.play(oa); 649 for (int i = 0; i < HANDLE_COUNT; i++) { 650 set.play(mFirstFrameAnimatorHelper.addTo( 651 ObjectAnimator.ofFloat(mDragHandles[i], ALPHA, 1f))); 652 } 653 if (pairedCellLayout != null) { 654 updateInvalidResizeEffect(mCellLayout, pairedCellLayout, /* alpha= */ 1f, 655 /* springLoadedProgress= */ 0f, /* animatorSet= */ set); 656 } 657 set.setDuration(SNAP_DURATION_MS); 658 set.start(); 659 } 660 661 setFocusableInTouchMode(true); 662 requestFocus(); 663 } 664 665 @Override 666 public boolean onKey(View v, int keyCode, KeyEvent event) { 667 // Clear the frame and give focus to the widget host view when a directional key is pressed. 668 if (shouldConsume(keyCode)) { 669 close(false); 670 mWidgetView.requestFocus(); 671 return true; 672 } 673 return false; 674 } 675 676 private boolean handleTouchDown(MotionEvent ev) { 677 Rect hitRect = new Rect(); 678 int x = (int) ev.getX(); 679 int y = (int) ev.getY(); 680 681 getHitRect(hitRect); 682 if (hitRect.contains(x, y)) { 683 if (beginResizeIfPointInRegion(x - getLeft(), y - getTop())) { 684 mXDown = x; 685 mYDown = y; 686 return true; 687 } 688 } 689 return false; 690 } 691 692 private boolean isTouchOnReconfigureButton(MotionEvent ev) { 693 int xFrame = (int) ev.getX() - getLeft(); 694 int yFrame = (int) ev.getY() - getTop(); 695 mReconfigureButton.getHitRect(sTmpRect); 696 return sTmpRect.contains(xFrame, yFrame); 697 } 698 699 @Override 700 public boolean onControllerTouchEvent(MotionEvent ev) { 701 int action = ev.getAction(); 702 int x = (int) ev.getX(); 703 int y = (int) ev.getY(); 704 705 switch (action) { 706 case MotionEvent.ACTION_DOWN: 707 return handleTouchDown(ev); 708 case MotionEvent.ACTION_MOVE: 709 visualizeResizeForDelta(x - mXDown, y - mYDown); 710 break; 711 case MotionEvent.ACTION_CANCEL: 712 case MotionEvent.ACTION_UP: 713 visualizeResizeForDelta(x - mXDown, y - mYDown); 714 onTouchUp(); 715 mXDown = mYDown = 0; 716 break; 717 } 718 return true; 719 } 720 721 @Override 722 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 723 if (ev.getAction() == MotionEvent.ACTION_DOWN && handleTouchDown(ev)) { 724 return true; 725 } 726 // Keep the resize frame open but let a click on the reconfigure button fall through to the 727 // button's OnClickListener. 728 if (isTouchOnReconfigureButton(ev)) { 729 return false; 730 } 731 close(false); 732 return false; 733 } 734 735 @Override 736 protected void handleClose(boolean animate) { 737 if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()) { 738 mWidgetView.clearCellChildViewPreLayoutListener(); 739 mWidgetView.setLayoutTransition(null); 740 } 741 mDragLayer.removeView(this); 742 mWidgetView.removeOnLayoutChangeListener(mWidgetViewLayoutListener); 743 } 744 745 private void updateInvalidResizeEffect(CellLayout cellLayout, CellLayout pairedCellLayout, 746 float alpha, float springLoadedProgress) { 747 updateInvalidResizeEffect(cellLayout, pairedCellLayout, alpha, 748 springLoadedProgress, /* animatorSet= */ null); 749 } 750 751 private void updateInvalidResizeEffect(CellLayout cellLayout, CellLayout pairedCellLayout, 752 float alpha, float springLoadedProgress, @Nullable AnimatorSet animatorSet) { 753 int childCount = pairedCellLayout.getChildCount(); 754 for (int i = 0; i < childCount; i++) { 755 View child = pairedCellLayout.getChildAt(i); 756 if (animatorSet != null) { 757 animatorSet.play( 758 mFirstFrameAnimatorHelper.addTo( 759 ObjectAnimator.ofFloat(child, ALPHA, alpha))); 760 } else { 761 child.setAlpha(alpha); 762 } 763 } 764 if (animatorSet != null) { 765 animatorSet.play(mFirstFrameAnimatorHelper.addTo( 766 ObjectAnimator.ofFloat(cellLayout, SPRING_LOADED_PROGRESS, 767 springLoadedProgress))); 768 animatorSet.play(mFirstFrameAnimatorHelper.addTo( 769 ObjectAnimator.ofFloat(pairedCellLayout, SPRING_LOADED_PROGRESS, 770 springLoadedProgress))); 771 } else { 772 cellLayout.setSpringLoadedProgress(springLoadedProgress); 773 pairedCellLayout.setSpringLoadedProgress(springLoadedProgress); 774 } 775 776 boolean shouldShowCellLayoutBorder = springLoadedProgress > 0f; 777 if (animatorSet != null) { 778 animatorSet.addListener(new AnimatorListenerAdapter() { 779 @Override 780 public void onAnimationEnd(Animator animator) { 781 cellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder); 782 pairedCellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder); 783 } 784 }); 785 } else { 786 cellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder); 787 pairedCellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder); 788 } 789 } 790 791 @Override 792 protected boolean isOfType(int type) { 793 return (type & TYPE_WIDGET_RESIZE_FRAME) != 0; 794 } 795 796 /** 797 * A mutable class for describing the range of two int values. 798 */ 799 private static class IntRange { 800 801 public int start, end; 802 803 public int clamp(int value) { 804 return Utilities.boundToRange(value, start, end); 805 } 806 807 public void set(int s, int e) { 808 start = s; 809 end = e; 810 } 811 812 public int size() { 813 return end - start; 814 } 815 816 /** 817 * Moves either the start or end edge (but never both) by {@param delta} and sets the 818 * result in {@param out} 819 */ 820 public void applyDelta(boolean moveStart, boolean moveEnd, int delta, IntRange out) { 821 out.start = moveStart ? start + delta : start; 822 out.end = moveEnd ? end + delta : end; 823 } 824 825 /** 826 * Applies delta similar to {@link #applyDelta(boolean, boolean, int, IntRange)}, 827 * with extra conditions. 828 * @param minSize minimum size after with the moving edge should not be shifted any further. 829 * For eg, if delta = -3 when moving the endEdge brings the size to less than 830 * minSize, only delta = -2 will applied 831 * @param maxSize maximum size after with the moving edge should not be shifted any further. 832 * For eg, if delta = -3 when moving the endEdge brings the size to greater 833 * than maxSize, only delta = -2 will applied 834 * @param maxEnd The maximum value to the end edge (start edge is always restricted to 0) 835 * @return the amount of increase when endEdge was moves and the amount of decrease when 836 * the start edge was moved. 837 */ 838 public int applyDeltaAndBound(boolean moveStart, boolean moveEnd, int delta, 839 int minSize, int maxSize, int maxEnd, IntRange out) { 840 applyDelta(moveStart, moveEnd, delta, out); 841 if (out.start < 0) { 842 out.start = 0; 843 } 844 if (out.end > maxEnd) { 845 out.end = maxEnd; 846 } 847 if (out.size() < minSize) { 848 if (moveStart) { 849 out.start = out.end - minSize; 850 } else if (moveEnd) { 851 out.end = out.start + minSize; 852 } 853 } 854 if (out.size() > maxSize) { 855 if (moveStart) { 856 out.start = out.end - maxSize; 857 } else if (moveEnd) { 858 out.end = out.start + maxSize; 859 } 860 } 861 return moveEnd ? out.size() - size() : size() - out.size(); 862 } 863 } 864 865 /** 866 * Returns true only if this utility class handles the key code. 867 */ 868 public static boolean shouldConsume(int keyCode) { 869 return (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT 870 || keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN 871 || keyCode == KeyEvent.KEYCODE_MOVE_HOME || keyCode == KeyEvent.KEYCODE_MOVE_END 872 || keyCode == KeyEvent.KEYCODE_PAGE_UP || keyCode == KeyEvent.KEYCODE_PAGE_DOWN); 873 } 874 875 @Nullable private ArrowTipView showReconfigurableWidgetEducationTip() { 876 Rect rect = new Rect(); 877 if (!mReconfigureButton.getGlobalVisibleRect(rect)) { 878 return null; 879 } 880 @Px int tipMargin = mLauncher.getResources() 881 .getDimensionPixelSize(R.dimen.widget_reconfigure_tip_top_margin); 882 return new ArrowTipView(mLauncher, /* isPointingUp= */ true) 883 .showAroundRect( 884 getContext().getString(R.string.reconfigurable_widget_education_tip), 885 /* arrowXCoord= */ rect.left + mReconfigureButton.getWidth() / 2, 886 /* rect= */ rect, 887 /* margin= */ tipMargin); 888 } 889 890 private boolean hasSeenReconfigurableWidgetEducationTip() { 891 return LauncherPrefs.get(getContext()).get(RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN) 892 || Utilities.isRunningInTestHarness(); 893 } 894 } 895