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 // Save widget item info as tag on resize frame; so that, the accessibility delegate can 220 // attach actions that typically happen on widget (e.g. resize, move) also on the resize 221 // frame. 222 frame.setTag(widget.getTag()); 223 frame.setAccessibilityDelegate(launcher.getAccessibilityDelegate()); 224 frame.setContentDescription(launcher.asContext().getString(R.string.widget_frame_name, 225 widget.getContentDescription())); 226 ((DragLayer.LayoutParams) frame.getLayoutParams()).customPosition = true; 227 228 dl.addView(frame); 229 frame.mIsOpen = true; 230 frame.post(() -> frame.snapToWidget(false)); 231 } 232 setCornerRadiusFromWidget()233 private void setCornerRadiusFromWidget() { 234 if (mWidgetView != null && mWidgetView.hasEnforcedCornerRadius()) { 235 float enforcedCornerRadius = mWidgetView.getEnforcedCornerRadius(); 236 ImageView imageView = findViewById(R.id.widget_resize_frame); 237 Drawable d = imageView.getDrawable(); 238 if (d instanceof GradientDrawable) { 239 GradientDrawable gd = (GradientDrawable) d.mutate(); 240 gd.setCornerRadius(enforcedCornerRadius); 241 } 242 } 243 } 244 245 /** 246 * Retrieves the view where accessibility actions happen. 247 */ getViewForAccessibility()248 public View getViewForAccessibility() { 249 return mWidgetView; 250 } 251 setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout, DragLayer dragLayer)252 private void setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout, 253 DragLayer dragLayer) { 254 mCellLayout = cellLayout; 255 mWidgetView = widgetView; 256 LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) 257 widgetView.getAppWidgetInfo(); 258 mDragLayer = dragLayer; 259 260 mMinHSpan = info.minSpanX; 261 mMinVSpan = info.minSpanY; 262 mMaxHSpan = info.maxSpanX; 263 mMaxVSpan = info.maxSpanY; 264 265 // Only show resize handles for the directions in which resizing is possible. 266 InvariantDeviceProfile idp = LauncherAppState.getIDP(cellLayout.getContext()); 267 mVerticalResizeActive = (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0 268 && mMinVSpan < idp.numRows && mMaxVSpan > 1 269 && mMinVSpan < mMaxVSpan; 270 if (!mVerticalResizeActive) { 271 mDragHandles[INDEX_TOP].setVisibility(GONE); 272 mDragHandles[INDEX_BOTTOM].setVisibility(GONE); 273 } 274 mHorizontalResizeActive = (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0 275 && mMinHSpan < idp.numColumns && mMaxHSpan > 1 276 && mMinHSpan < mMaxHSpan; 277 if (!mHorizontalResizeActive) { 278 mDragHandles[INDEX_LEFT].setVisibility(GONE); 279 mDragHandles[INDEX_RIGHT].setVisibility(GONE); 280 } 281 282 mReconfigureButton = (ImageButton) findViewById(R.id.widget_reconfigure_button); 283 if (info.isReconfigurable()) { 284 mReconfigureButton.setVisibility(VISIBLE); 285 mReconfigureButton.setOnClickListener(view -> { 286 mLauncher.setWaitingForResult( 287 PendingRequestArgs.forWidgetInfo( 288 mWidgetView.getAppWidgetId(), 289 // Widget add handler is null since we're reconfiguring an existing 290 // widget. 291 /* widgetHandler= */ null, 292 (ItemInfo) mWidgetView.getTag())); 293 mLauncher 294 .getAppWidgetHolder() 295 .startConfigActivity( 296 mLauncher, 297 mWidgetView.getAppWidgetId(), 298 ActivityCodes.REQUEST_RECONFIGURE_APPWIDGET); 299 }); 300 if (!hasSeenReconfigurableWidgetEducationTip()) { 301 post(() -> { 302 if (showReconfigurableWidgetEducationTip() != null) { 303 LauncherPrefs.get(getContext()).put( 304 RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN, true); 305 } 306 }); 307 } 308 } 309 310 if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()) { 311 mWidgetView.setCellChildViewPreLayoutListener(mCellChildViewPreLayoutListener); 312 mWidgetViewOldRect.set(mWidgetView.getLeft(), mWidgetView.getTop(), 313 mWidgetView.getRight(), 314 mWidgetView.getBottom()); 315 mWidgetViewNewRect.set(mWidgetViewOldRect); 316 } 317 318 CellLayoutLayoutParams lp = (CellLayoutLayoutParams) mWidgetView.getLayoutParams(); 319 ItemInfo widgetInfo = (ItemInfo) mWidgetView.getTag(); 320 CellPos presenterPos = mLauncher.getCellPosMapper().mapModelToPresenter(widgetInfo); 321 lp.setCellX(presenterPos.cellX); 322 lp.setTmpCellX(presenterPos.cellX); 323 lp.setCellY(presenterPos.cellY); 324 lp.setTmpCellY(presenterPos.cellY); 325 lp.cellHSpan = widgetInfo.spanX; 326 lp.cellVSpan = widgetInfo.spanY; 327 lp.isLockedToGrid = true; 328 329 // When we create the resize frame, we first mark all cells as unoccupied. The appropriate 330 // cells (same if not resized, or different) will be marked as occupied when the resize 331 // frame is dismissed. 332 mCellLayout.markCellsAsUnoccupiedForView(mWidgetView); 333 334 mLauncher.getStatsLogManager() 335 .logger() 336 .withInstanceId(logInstanceId) 337 .withItemInfo(widgetInfo) 338 .log(LAUNCHER_WIDGET_RESIZE_STARTED); 339 340 setOnKeyListener(this); 341 342 setCornerRadiusFromWidget(); 343 mWidgetView.addOnLayoutChangeListener(mWidgetViewLayoutListener); 344 } 345 346 public boolean beginResizeIfPointInRegion(int x, int y) { 347 mLeftBorderActive = (x < mTouchTargetWidth) && mHorizontalResizeActive; 348 mRightBorderActive = (x > getWidth() - mTouchTargetWidth) && mHorizontalResizeActive; 349 mTopBorderActive = (y < mTouchTargetWidth + mTopTouchRegionAdjustment) 350 && mVerticalResizeActive; 351 mBottomBorderActive = (y > getHeight() - mTouchTargetWidth + mBottomTouchRegionAdjustment) 352 && mVerticalResizeActive; 353 354 boolean anyBordersActive = mLeftBorderActive || mRightBorderActive 355 || mTopBorderActive || mBottomBorderActive; 356 357 if (anyBordersActive) { 358 mDragHandles[INDEX_LEFT].setAlpha(mLeftBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); 359 mDragHandles[INDEX_RIGHT].setAlpha(mRightBorderActive ? 1.0f :DIMMED_HANDLE_ALPHA); 360 mDragHandles[INDEX_TOP].setAlpha(mTopBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); 361 mDragHandles[INDEX_BOTTOM].setAlpha(mBottomBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); 362 } 363 364 if (mLeftBorderActive) { 365 mDeltaXRange.set(-getLeft(), getWidth() - 2 * mTouchTargetWidth); 366 } else if (mRightBorderActive) { 367 mDeltaXRange.set(2 * mTouchTargetWidth - getWidth(), mDragLayer.getWidth() - getRight()); 368 } else { 369 mDeltaXRange.set(0, 0); 370 } 371 mBaselineX.set(getLeft(), getRight()); 372 373 if (mTopBorderActive) { 374 mDeltaYRange.set(-getTop(), getHeight() - 2 * mTouchTargetWidth); 375 } else if (mBottomBorderActive) { 376 mDeltaYRange.set(2 * mTouchTargetWidth - getHeight(), mDragLayer.getHeight() - getBottom()); 377 } else { 378 mDeltaYRange.set(0, 0); 379 } 380 mBaselineY.set(getTop(), getBottom()); 381 382 return anyBordersActive; 383 } 384 385 /** 386 * Based on the deltas, we resize the frame. 387 */ 388 public void visualizeResizeForDelta(int deltaX, int deltaY) { 389 mDeltaX = mDeltaXRange.clamp(deltaX); 390 mDeltaY = mDeltaYRange.clamp(deltaY); 391 392 DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); 393 mDeltaX = mDeltaXRange.clamp(deltaX); 394 mBaselineX.applyDelta(mLeftBorderActive, mRightBorderActive, mDeltaX, mTempRange1); 395 lp.x = mTempRange1.start; 396 lp.width = mTempRange1.size(); 397 398 mDeltaY = mDeltaYRange.clamp(deltaY); 399 mBaselineY.applyDelta(mTopBorderActive, mBottomBorderActive, mDeltaY, mTempRange1); 400 lp.y = mTempRange1.start; 401 lp.height = mTempRange1.size(); 402 403 resizeWidgetIfNeeded(false); 404 405 // Handle invalid resize across CellLayouts in the two panel UI. 406 if (mCellLayout.getParent() instanceof Workspace) { 407 Workspace<?> workspace = (Workspace<?>) mCellLayout.getParent(); 408 CellLayout pairedCellLayout = workspace.getScreenPair(mCellLayout); 409 if (pairedCellLayout != null) { 410 Rect focusedCellLayoutBound = sTmpRect; 411 mDragLayerRelativeCoordinateHelper.viewToRect(mCellLayout, focusedCellLayoutBound); 412 Rect resizeFrameBound = sTmpRect2; 413 findViewById(R.id.widget_resize_frame).getGlobalVisibleRect(resizeFrameBound); 414 float progress = 1f; 415 if (workspace.indexOfChild(pairedCellLayout) < workspace.indexOfChild(mCellLayout) 416 && mDeltaX < 0 417 && resizeFrameBound.left < focusedCellLayoutBound.left) { 418 // Resize from right to left. 419 progress = (mDragAcrossTwoPanelOpacityMargin + mDeltaX) 420 / mDragAcrossTwoPanelOpacityMargin; 421 } else if (workspace.indexOfChild(pairedCellLayout) 422 > workspace.indexOfChild(mCellLayout) 423 && mDeltaX > 0 424 && resizeFrameBound.right > focusedCellLayoutBound.right) { 425 // Resize from left to right. 426 progress = (mDragAcrossTwoPanelOpacityMargin - mDeltaX) 427 / mDragAcrossTwoPanelOpacityMargin; 428 } 429 float alpha = Math.max(MIN_OPACITY_FOR_CELL_LAYOUT_DURING_INVALID_RESIZE, progress); 430 float springLoadedProgress = Math.min(1f, 1f - progress); 431 updateInvalidResizeEffect(mCellLayout, pairedCellLayout, alpha, 432 springLoadedProgress); 433 } 434 } 435 436 requestLayout(); 437 } 438 439 private static int getSpanIncrement(float deltaFrac) { 440 return Math.abs(deltaFrac) > RESIZE_THRESHOLD ? Math.round(deltaFrac) : 0; 441 } 442 443 /** 444 * Based on the current deltas, we determine if and how to resize the widget. 445 */ 446 private void resizeWidgetIfNeeded(boolean onDismiss) { 447 ViewGroup.LayoutParams wlp = mWidgetView.getLayoutParams(); 448 if (!(wlp instanceof CellLayoutLayoutParams)) { 449 return; 450 } 451 DeviceProfile dp = mLauncher.getDeviceProfile(); 452 float xThreshold = mCellLayout.getCellWidth() + dp.cellLayoutBorderSpacePx.x; 453 float yThreshold = mCellLayout.getCellHeight() + dp.cellLayoutBorderSpacePx.y; 454 455 int hSpanInc = getSpanIncrement((mDeltaX + mDeltaXAddOn) / xThreshold - mRunningHInc); 456 int vSpanInc = getSpanIncrement((mDeltaY + mDeltaYAddOn) / yThreshold - mRunningVInc); 457 458 if (!onDismiss && (hSpanInc == 0 && vSpanInc == 0)) return; 459 460 mDirectionVector[0] = 0; 461 mDirectionVector[1] = 0; 462 463 CellLayoutLayoutParams lp = (CellLayoutLayoutParams) wlp; 464 465 int spanX = lp.cellHSpan; 466 int spanY = lp.cellVSpan; 467 int cellX = lp.useTmpCoords ? lp.getTmpCellX() : lp.getCellX(); 468 int cellY = lp.useTmpCoords ? lp.getTmpCellY() : lp.getCellY(); 469 470 // For each border, we bound the resizing based on the minimum width, and the maximum 471 // expandability. 472 mTempRange1.set(cellX, spanX + cellX); 473 int hSpanDelta = mTempRange1.applyDeltaAndBound(mLeftBorderActive, mRightBorderActive, 474 hSpanInc, mMinHSpan, mMaxHSpan, mCellLayout.getCountX(), mTempRange2); 475 cellX = mTempRange2.start; 476 spanX = mTempRange2.size(); 477 if (hSpanDelta != 0) { 478 mDirectionVector[0] = mLeftBorderActive ? -1 : 1; 479 } 480 481 mTempRange1.set(cellY, spanY + cellY); 482 int vSpanDelta = mTempRange1.applyDeltaAndBound(mTopBorderActive, mBottomBorderActive, 483 vSpanInc, mMinVSpan, mMaxVSpan, mCellLayout.getCountY(), mTempRange2); 484 cellY = mTempRange2.start; 485 spanY = mTempRange2.size(); 486 if (vSpanDelta != 0) { 487 mDirectionVector[1] = mTopBorderActive ? -1 : 1; 488 } 489 490 if (!onDismiss && vSpanDelta == 0 && hSpanDelta == 0) return; 491 492 // We always want the final commit to match the feedback, so we make sure to use the 493 // last used direction vector when committing the resize / reorder. 494 if (onDismiss) { 495 mDirectionVector[0] = mLastDirectionVector[0]; 496 mDirectionVector[1] = mLastDirectionVector[1]; 497 } else { 498 mLastDirectionVector[0] = mDirectionVector[0]; 499 mLastDirectionVector[1] = mDirectionVector[1]; 500 } 501 502 // We don't want to evaluate resize if a widget was pending config activity and was already 503 // occupying a space on the screen. This otherwise will cause reorder algorithm evaluate a 504 // different location for the widget and cause a jump. 505 if (!(mWidgetView instanceof PendingAppWidgetHostView) && mCellLayout.createAreaForResize( 506 cellX, cellY, spanX, spanY, mWidgetView, mDirectionVector, onDismiss)) { 507 if (mStateAnnouncer != null && (lp.cellHSpan != spanX || lp.cellVSpan != spanY) ) { 508 mStateAnnouncer.announce( 509 mLauncher.getString(R.string.widget_resized, spanX, spanY)); 510 } 511 512 lp.setTmpCellX(cellX); 513 lp.setTmpCellY(cellY); 514 lp.cellHSpan = spanX; 515 lp.cellVSpan = spanY; 516 mRunningVInc += vSpanDelta; 517 mRunningHInc += hSpanDelta; 518 519 if (!onDismiss) { 520 WidgetSizes.updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY); 521 } 522 } 523 mWidgetView.requestLayout(); 524 } 525 526 @Override 527 protected void onDetachedFromWindow() { 528 super.onDetachedFromWindow(); 529 530 // We are done with resizing the widget. Save the widget size & position to LauncherModel 531 resizeWidgetIfNeeded(true); 532 mLauncher.getStatsLogManager() 533 .logger() 534 .withInstanceId(logInstanceId) 535 .withItemInfo((ItemInfo) mWidgetView.getTag()) 536 .log(LAUNCHER_WIDGET_RESIZE_COMPLETED); 537 } 538 539 private void onTouchUp() { 540 DeviceProfile dp = mLauncher.getDeviceProfile(); 541 int xThreshold = mCellLayout.getCellWidth() + dp.cellLayoutBorderSpacePx.x; 542 int yThreshold = mCellLayout.getCellHeight() + dp.cellLayoutBorderSpacePx.y; 543 544 mDeltaXAddOn = mRunningHInc * xThreshold; 545 mDeltaYAddOn = mRunningVInc * yThreshold; 546 mDeltaX = 0; 547 mDeltaY = 0; 548 549 post(() -> snapToWidget(true)); 550 } 551 552 /** 553 * Returns the rect of this view when the frame is snapped around the widget, with the bounds 554 * relative to the {@link DragLayer}. 555 */ 556 private void getSnappedRectRelativeToDragLayer(@NonNull Rect out) { 557 float scale = mWidgetView.getScaleToFit(); 558 if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()) { 559 getViewRectRelativeToDragLayer(out); 560 } else { 561 mDragLayer.getViewRectRelativeToSelf(mWidgetView, out); 562 } 563 564 int width = 2 * mBackgroundPadding + Math.round(scale * out.width()); 565 int height = 2 * mBackgroundPadding + Math.round(scale * out.height()); 566 int x = out.left - mBackgroundPadding; 567 int y = out.top - mBackgroundPadding; 568 569 out.left = x; 570 out.top = y; 571 out.right = out.left + width; 572 out.bottom = out.top + height; 573 } 574 575 private void getViewRectRelativeToDragLayer(@NonNull Rect out) { 576 int[] afterPos = getViewPosRelativeToDragLayer(); 577 out.set(afterPos[0], afterPos[1], afterPos[0] + mWidgetViewNewRect.width(), 578 afterPos[1] + mWidgetViewNewRect.height()); 579 } 580 581 /** Returns the relative x and y values of the widget view after the layout transition */ 582 private int[] getViewPosRelativeToDragLayer() { 583 mDragLayer.getLocationInWindow(sDragLayerLoc); 584 int x = sDragLayerLoc[0]; 585 int y = sDragLayerLoc[1]; 586 587 if (mWidgetViewWindowPos == null) { 588 mWidgetViewWindowPos = new int[2]; 589 mWidgetView.getLocationInWindow(mWidgetViewWindowPos); 590 } 591 592 int leftOffset = mWidgetViewNewRect.left - mWidgetViewOldRect.left; 593 int topOffset = mWidgetViewNewRect.top - mWidgetViewOldRect.top; 594 595 return new int[] {mWidgetViewWindowPos[0] - x + leftOffset, 596 mWidgetViewWindowPos[1] - y + topOffset}; 597 } 598 599 private void snapToWidget(boolean animate) { 600 // The widget is guaranteed to be attached to the cell layout at this point, thus setting 601 // the transition here 602 if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get() 603 && mWidgetView.getLayoutTransition() == null) { 604 final LayoutTransition transition = new LayoutTransition(); 605 transition.setDuration(RESIZE_TRANSITION_DURATION_MS); 606 transition.enableTransitionType(LayoutTransition.CHANGING); 607 mWidgetView.setLayoutTransition(transition); 608 } 609 610 getSnappedRectRelativeToDragLayer(sTmpRect); 611 int newWidth = sTmpRect.width(); 612 int newHeight = sTmpRect.height(); 613 int newX = sTmpRect.left; 614 int newY = sTmpRect.top; 615 616 // We need to make sure the frame's touchable regions lie fully within the bounds of the 617 // DragLayer. We allow the actual handles to be clipped, but we shift the touch regions 618 // down accordingly to provide a proper touch target. 619 if (newY < 0) { 620 // In this case we shift the touch region down to start at the top of the DragLayer 621 mTopTouchRegionAdjustment = -newY; 622 } else { 623 mTopTouchRegionAdjustment = 0; 624 } 625 if (newY + newHeight > mDragLayer.getHeight()) { 626 // In this case we shift the touch region up to end at the bottom of the DragLayer 627 mBottomTouchRegionAdjustment = -(newY + newHeight - mDragLayer.getHeight()); 628 } else { 629 mBottomTouchRegionAdjustment = 0; 630 } 631 632 final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); 633 final CellLayout pairedCellLayout; 634 if (mCellLayout.getParent() instanceof Workspace) { 635 Workspace<?> workspace = (Workspace<?>) mCellLayout.getParent(); 636 pairedCellLayout = workspace.getScreenPair(mCellLayout); 637 } else { 638 pairedCellLayout = null; 639 } 640 if (!animate) { 641 lp.width = newWidth; 642 lp.height = newHeight; 643 lp.x = newX; 644 lp.y = newY; 645 for (int i = 0; i < HANDLE_COUNT; i++) { 646 mDragHandles[i].setAlpha(1f); 647 } 648 if (pairedCellLayout != null) { 649 updateInvalidResizeEffect(mCellLayout, pairedCellLayout, /* alpha= */ 1f, 650 /* springLoadedProgress= */ 0f); 651 } 652 requestLayout(); 653 } else { 654 ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(lp, 655 PropertyValuesHolder.ofInt(LAYOUT_WIDTH, lp.width, newWidth), 656 PropertyValuesHolder.ofInt(LAYOUT_HEIGHT, lp.height, newHeight), 657 PropertyValuesHolder.ofInt(LAYOUT_X, lp.x, newX), 658 PropertyValuesHolder.ofInt(LAYOUT_Y, lp.y, newY)); 659 mFirstFrameAnimatorHelper.addTo(oa).addUpdateListener(a -> requestLayout()); 660 661 AnimatorSet set = new AnimatorSet(); 662 set.play(oa); 663 for (int i = 0; i < HANDLE_COUNT; i++) { 664 set.play(mFirstFrameAnimatorHelper.addTo( 665 ObjectAnimator.ofFloat(mDragHandles[i], ALPHA, 1f))); 666 } 667 if (pairedCellLayout != null) { 668 updateInvalidResizeEffect(mCellLayout, pairedCellLayout, /* alpha= */ 1f, 669 /* springLoadedProgress= */ 0f, /* animatorSet= */ set); 670 } 671 set.setDuration(SNAP_DURATION_MS); 672 set.start(); 673 } 674 675 setFocusableInTouchMode(true); 676 requestFocus(); 677 } 678 679 @Override 680 public boolean onKey(View v, int keyCode, KeyEvent event) { 681 // Clear the frame and give focus to the widget host view when a directional key is pressed. 682 if (shouldConsume(keyCode)) { 683 close(false); 684 mWidgetView.requestFocus(); 685 return true; 686 } 687 return false; 688 } 689 690 private boolean handleTouchDown(MotionEvent ev) { 691 Rect hitRect = new Rect(); 692 int x = (int) ev.getX(); 693 int y = (int) ev.getY(); 694 695 getHitRect(hitRect); 696 if (hitRect.contains(x, y)) { 697 if (beginResizeIfPointInRegion(x - getLeft(), y - getTop())) { 698 mXDown = x; 699 mYDown = y; 700 return true; 701 } 702 } 703 return false; 704 } 705 706 private boolean isTouchOnReconfigureButton(MotionEvent ev) { 707 int xFrame = (int) ev.getX() - getLeft(); 708 int yFrame = (int) ev.getY() - getTop(); 709 mReconfigureButton.getHitRect(sTmpRect); 710 return sTmpRect.contains(xFrame, yFrame); 711 } 712 713 @Override 714 public boolean onControllerTouchEvent(MotionEvent ev) { 715 int action = ev.getAction(); 716 int x = (int) ev.getX(); 717 int y = (int) ev.getY(); 718 719 switch (action) { 720 case MotionEvent.ACTION_DOWN: 721 return handleTouchDown(ev); 722 case MotionEvent.ACTION_MOVE: 723 visualizeResizeForDelta(x - mXDown, y - mYDown); 724 break; 725 case MotionEvent.ACTION_CANCEL: 726 case MotionEvent.ACTION_UP: 727 visualizeResizeForDelta(x - mXDown, y - mYDown); 728 onTouchUp(); 729 mXDown = mYDown = 0; 730 break; 731 } 732 return true; 733 } 734 735 @Override 736 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 737 if (ev.getAction() == MotionEvent.ACTION_DOWN && handleTouchDown(ev)) { 738 return true; 739 } 740 // Keep the resize frame open but let a click on the reconfigure button fall through to the 741 // button's OnClickListener. 742 if (isTouchOnReconfigureButton(ev)) { 743 return false; 744 } 745 close(false); 746 return false; 747 } 748 749 @Override 750 protected void handleClose(boolean animate) { 751 if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()) { 752 mWidgetView.clearCellChildViewPreLayoutListener(); 753 mWidgetView.setLayoutTransition(null); 754 } 755 mDragLayer.removeView(this); 756 mWidgetView.removeOnLayoutChangeListener(mWidgetViewLayoutListener); 757 } 758 759 private void updateInvalidResizeEffect(CellLayout cellLayout, CellLayout pairedCellLayout, 760 float alpha, float springLoadedProgress) { 761 updateInvalidResizeEffect(cellLayout, pairedCellLayout, alpha, 762 springLoadedProgress, /* animatorSet= */ null); 763 } 764 765 private void updateInvalidResizeEffect(CellLayout cellLayout, CellLayout pairedCellLayout, 766 float alpha, float springLoadedProgress, @Nullable AnimatorSet animatorSet) { 767 int childCount = pairedCellLayout.getChildCount(); 768 for (int i = 0; i < childCount; i++) { 769 View child = pairedCellLayout.getChildAt(i); 770 if (animatorSet != null) { 771 animatorSet.play( 772 mFirstFrameAnimatorHelper.addTo( 773 ObjectAnimator.ofFloat(child, ALPHA, alpha))); 774 } else { 775 child.setAlpha(alpha); 776 } 777 } 778 if (animatorSet != null) { 779 animatorSet.play(mFirstFrameAnimatorHelper.addTo( 780 ObjectAnimator.ofFloat(cellLayout, SPRING_LOADED_PROGRESS, 781 springLoadedProgress))); 782 animatorSet.play(mFirstFrameAnimatorHelper.addTo( 783 ObjectAnimator.ofFloat(pairedCellLayout, SPRING_LOADED_PROGRESS, 784 springLoadedProgress))); 785 } else { 786 cellLayout.setSpringLoadedProgress(springLoadedProgress); 787 pairedCellLayout.setSpringLoadedProgress(springLoadedProgress); 788 } 789 790 boolean shouldShowCellLayoutBorder = springLoadedProgress > 0f; 791 if (animatorSet != null) { 792 animatorSet.addListener(new AnimatorListenerAdapter() { 793 @Override 794 public void onAnimationEnd(Animator animator) { 795 cellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder); 796 pairedCellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder); 797 } 798 }); 799 } else { 800 cellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder); 801 pairedCellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder); 802 } 803 } 804 805 @Override 806 protected boolean isOfType(int type) { 807 return (type & TYPE_WIDGET_RESIZE_FRAME) != 0; 808 } 809 810 /** 811 * A mutable class for describing the range of two int values. 812 */ 813 private static class IntRange { 814 815 public int start, end; 816 817 public int clamp(int value) { 818 return Utilities.boundToRange(value, start, end); 819 } 820 821 public void set(int s, int e) { 822 start = s; 823 end = e; 824 } 825 826 public int size() { 827 return end - start; 828 } 829 830 /** 831 * Moves either the start or end edge (but never both) by {@param delta} and sets the 832 * result in {@param out} 833 */ 834 public void applyDelta(boolean moveStart, boolean moveEnd, int delta, IntRange out) { 835 out.start = moveStart ? start + delta : start; 836 out.end = moveEnd ? end + delta : end; 837 } 838 839 /** 840 * Applies delta similar to {@link #applyDelta(boolean, boolean, int, IntRange)}, 841 * with extra conditions. 842 * @param minSize minimum size after with the moving edge should not be shifted any further. 843 * For eg, if delta = -3 when moving the endEdge brings the size to less than 844 * minSize, only delta = -2 will applied 845 * @param maxSize maximum size after with the moving edge should not be shifted any further. 846 * For eg, if delta = -3 when moving the endEdge brings the size to greater 847 * than maxSize, only delta = -2 will applied 848 * @param maxEnd The maximum value to the end edge (start edge is always restricted to 0) 849 * @return the amount of increase when endEdge was moves and the amount of decrease when 850 * the start edge was moved. 851 */ 852 public int applyDeltaAndBound(boolean moveStart, boolean moveEnd, int delta, 853 int minSize, int maxSize, int maxEnd, IntRange out) { 854 applyDelta(moveStart, moveEnd, delta, out); 855 if (out.start < 0) { 856 out.start = 0; 857 } 858 if (out.end > maxEnd) { 859 out.end = maxEnd; 860 } 861 if (out.size() < minSize) { 862 if (moveStart) { 863 out.start = out.end - minSize; 864 } else if (moveEnd) { 865 out.end = out.start + minSize; 866 } 867 } 868 if (out.size() > maxSize) { 869 if (moveStart) { 870 out.start = out.end - maxSize; 871 } else if (moveEnd) { 872 out.end = out.start + maxSize; 873 } 874 } 875 return moveEnd ? out.size() - size() : size() - out.size(); 876 } 877 } 878 879 /** 880 * Returns true only if this utility class handles the key code. 881 */ 882 public static boolean shouldConsume(int keyCode) { 883 return (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT 884 || keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN 885 || keyCode == KeyEvent.KEYCODE_MOVE_HOME || keyCode == KeyEvent.KEYCODE_MOVE_END 886 || keyCode == KeyEvent.KEYCODE_PAGE_UP || keyCode == KeyEvent.KEYCODE_PAGE_DOWN); 887 } 888 889 @Nullable private ArrowTipView showReconfigurableWidgetEducationTip() { 890 Rect rect = new Rect(); 891 if (!mReconfigureButton.getGlobalVisibleRect(rect)) { 892 return null; 893 } 894 @Px int tipMargin = mLauncher.getResources() 895 .getDimensionPixelSize(R.dimen.widget_reconfigure_tip_top_margin); 896 return new ArrowTipView(mLauncher, /* isPointingUp= */ true) 897 .showAroundRect( 898 getContext().getString(R.string.reconfigurable_widget_education_tip), 899 /* arrowXCoord= */ rect.left + mReconfigureButton.getWidth() / 2, 900 /* rect= */ rect, 901 /* margin= */ tipMargin); 902 } 903 904 private boolean hasSeenReconfigurableWidgetEducationTip() { 905 return LauncherPrefs.get(getContext()).get(RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN) 906 || Utilities.isRunningInTestHarness(); 907 } 908 } 909