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