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