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