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