1 package com.android.launcher3; 2 3 import android.animation.AnimatorSet; 4 import android.animation.ObjectAnimator; 5 import android.animation.PropertyValuesHolder; 6 import android.animation.ValueAnimator; 7 import android.animation.ValueAnimator.AnimatorUpdateListener; 8 import android.appwidget.AppWidgetHostView; 9 import android.appwidget.AppWidgetProviderInfo; 10 import android.content.Context; 11 import android.content.res.Resources; 12 import android.graphics.Point; 13 import android.graphics.Rect; 14 import android.util.AttributeSet; 15 import android.view.KeyEvent; 16 import android.view.MotionEvent; 17 import android.view.View; 18 import android.widget.FrameLayout; 19 20 import com.android.launcher3.accessibility.DragViewStateAnnouncer; 21 import com.android.launcher3.dragndrop.DragLayer; 22 import com.android.launcher3.util.FocusLogic; 23 import com.android.launcher3.util.TouchController; 24 25 public class AppWidgetResizeFrame extends FrameLayout 26 implements View.OnKeyListener, TouchController { 27 private static final int SNAP_DURATION = 150; 28 private static final float DIMMED_HANDLE_ALPHA = 0f; 29 private static final float RESIZE_THRESHOLD = 0.66f; 30 31 private static final Rect sTmpRect = new Rect(); 32 33 // Represents the cell size on the grid in the two orientations. 34 private static Point[] sCellSize; 35 36 private static final int HANDLE_COUNT = 4; 37 private static final int INDEX_LEFT = 0; 38 private static final int INDEX_TOP = 1; 39 private static final int INDEX_RIGHT = 2; 40 private static final int INDEX_BOTTOM = 3; 41 42 private final Launcher mLauncher; 43 private final DragViewStateAnnouncer mStateAnnouncer; 44 45 private final View[] mDragHandles = new View[HANDLE_COUNT]; 46 47 private LauncherAppWidgetHostView mWidgetView; 48 private CellLayout mCellLayout; 49 private DragLayer mDragLayer; 50 51 private Rect mWidgetPadding; 52 53 private final int mBackgroundPadding; 54 private final int mTouchTargetWidth; 55 56 private final int[] mDirectionVector = new int[2]; 57 private final int[] mLastDirectionVector = new int[2]; 58 59 private final IntRange mTempRange1 = new IntRange(); 60 private final IntRange mTempRange2 = new IntRange(); 61 62 private final IntRange mDeltaXRange = new IntRange(); 63 private final IntRange mBaselineX = new IntRange(); 64 65 private final IntRange mDeltaYRange = new IntRange(); 66 private final IntRange mBaselineY = new IntRange(); 67 68 private boolean mLeftBorderActive; 69 private boolean mRightBorderActive; 70 private boolean mTopBorderActive; 71 private boolean mBottomBorderActive; 72 73 private int mResizeMode; 74 75 private int mRunningHInc; 76 private int mRunningVInc; 77 private int mMinHSpan; 78 private int mMinVSpan; 79 private int mDeltaX; 80 private int mDeltaY; 81 private int mDeltaXAddOn; 82 private int mDeltaYAddOn; 83 84 private int mTopTouchRegionAdjustment = 0; 85 private int mBottomTouchRegionAdjustment = 0; 86 87 private int mXDown, mYDown; 88 AppWidgetResizeFrame(Context context)89 public AppWidgetResizeFrame(Context context) { 90 this(context, null); 91 } 92 AppWidgetResizeFrame(Context context, AttributeSet attrs)93 public AppWidgetResizeFrame(Context context, AttributeSet attrs) { 94 this(context, attrs, 0); 95 } 96 AppWidgetResizeFrame(Context context, AttributeSet attrs, int defStyleAttr)97 public AppWidgetResizeFrame(Context context, AttributeSet attrs, int defStyleAttr) { 98 super(context, attrs, defStyleAttr); 99 100 mLauncher = Launcher.getLauncher(context); 101 mStateAnnouncer = DragViewStateAnnouncer.createFor(this); 102 103 mBackgroundPadding = getResources() 104 .getDimensionPixelSize(R.dimen.resize_frame_background_padding); 105 mTouchTargetWidth = 2 * mBackgroundPadding; 106 } 107 108 @Override onFinishInflate()109 protected void onFinishInflate() { 110 super.onFinishInflate(); 111 112 for (int i = 0; i < HANDLE_COUNT; i ++) { 113 mDragHandles[i] = getChildAt(i); 114 } 115 } 116 setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout, DragLayer dragLayer)117 public void setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout, 118 DragLayer dragLayer) { 119 mCellLayout = cellLayout; 120 mWidgetView = widgetView; 121 LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) 122 widgetView.getAppWidgetInfo(); 123 mResizeMode = info.resizeMode; 124 mDragLayer = dragLayer; 125 126 mMinHSpan = info.minSpanX; 127 mMinVSpan = info.minSpanY; 128 129 if (!info.isCustomWidget) { 130 mWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(getContext(), 131 widgetView.getAppWidgetInfo().provider, null); 132 } else { 133 Resources r = getContext().getResources(); 134 int padding = r.getDimensionPixelSize(R.dimen.default_widget_padding); 135 mWidgetPadding = new Rect(padding, padding, padding, padding); 136 } 137 138 if (mResizeMode == AppWidgetProviderInfo.RESIZE_HORIZONTAL) { 139 mDragHandles[INDEX_TOP].setVisibility(GONE); 140 mDragHandles[INDEX_BOTTOM].setVisibility(GONE); 141 } else if (mResizeMode == AppWidgetProviderInfo.RESIZE_VERTICAL) { 142 mDragHandles[INDEX_LEFT].setVisibility(GONE); 143 mDragHandles[INDEX_RIGHT].setVisibility(GONE); 144 } 145 146 // When we create the resize frame, we first mark all cells as unoccupied. The appropriate 147 // cells (same if not resized, or different) will be marked as occupied when the resize 148 // frame is dismissed. 149 mCellLayout.markCellsAsUnoccupiedForView(mWidgetView); 150 151 setOnKeyListener(this); 152 } 153 beginResizeIfPointInRegion(int x, int y)154 public boolean beginResizeIfPointInRegion(int x, int y) { 155 boolean horizontalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0; 156 boolean verticalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0; 157 158 mLeftBorderActive = (x < mTouchTargetWidth) && horizontalActive; 159 mRightBorderActive = (x > getWidth() - mTouchTargetWidth) && horizontalActive; 160 mTopBorderActive = (y < mTouchTargetWidth + mTopTouchRegionAdjustment) && verticalActive; 161 mBottomBorderActive = (y > getHeight() - mTouchTargetWidth + mBottomTouchRegionAdjustment) 162 && verticalActive; 163 164 boolean anyBordersActive = mLeftBorderActive || mRightBorderActive 165 || mTopBorderActive || mBottomBorderActive; 166 167 if (anyBordersActive) { 168 mDragHandles[INDEX_LEFT].setAlpha(mLeftBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); 169 mDragHandles[INDEX_RIGHT].setAlpha(mRightBorderActive ? 1.0f :DIMMED_HANDLE_ALPHA); 170 mDragHandles[INDEX_TOP].setAlpha(mTopBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); 171 mDragHandles[INDEX_BOTTOM].setAlpha(mBottomBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); 172 } 173 174 if (mLeftBorderActive) { 175 mDeltaXRange.set(-getLeft(), getWidth() - 2 * mTouchTargetWidth); 176 } else if (mRightBorderActive) { 177 mDeltaXRange.set(2 * mTouchTargetWidth - getWidth(), mDragLayer.getWidth() - getRight()); 178 } else { 179 mDeltaXRange.set(0, 0); 180 } 181 mBaselineX.set(getLeft(), getRight()); 182 183 if (mTopBorderActive) { 184 mDeltaYRange.set(-getTop(), getHeight() - 2 * mTouchTargetWidth); 185 } else if (mBottomBorderActive) { 186 mDeltaYRange.set(2 * mTouchTargetWidth - getHeight(), mDragLayer.getHeight() - getBottom()); 187 } else { 188 mDeltaYRange.set(0, 0); 189 } 190 mBaselineY.set(getTop(), getBottom()); 191 192 return anyBordersActive; 193 } 194 195 /** 196 * Based on the deltas, we resize the frame. 197 */ visualizeResizeForDelta(int deltaX, int deltaY)198 public void visualizeResizeForDelta(int deltaX, int deltaY) { 199 mDeltaX = mDeltaXRange.clamp(deltaX); 200 mDeltaY = mDeltaYRange.clamp(deltaY); 201 202 DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); 203 mDeltaX = mDeltaXRange.clamp(deltaX); 204 mBaselineX.applyDelta(mLeftBorderActive, mRightBorderActive, mDeltaX, mTempRange1); 205 lp.x = mTempRange1.start; 206 lp.width = mTempRange1.size(); 207 208 mDeltaY = mDeltaYRange.clamp(deltaY); 209 mBaselineY.applyDelta(mTopBorderActive, mBottomBorderActive, mDeltaY, mTempRange1); 210 lp.y = mTempRange1.start; 211 lp.height = mTempRange1.size(); 212 213 resizeWidgetIfNeeded(false); 214 215 // When the widget resizes in multi-window mode, the translation value changes to maintain 216 // a center fit. These overrides ensure the resize frame always aligns with the widget view. 217 getSnappedRectRelativeToDragLayer(sTmpRect); 218 if (mLeftBorderActive) { 219 lp.width = sTmpRect.width() + sTmpRect.left - lp.x; 220 } 221 if (mTopBorderActive) { 222 lp.height = sTmpRect.height() + sTmpRect.top - lp.y; 223 } 224 if (mRightBorderActive) { 225 lp.x = sTmpRect.left; 226 } 227 if (mBottomBorderActive) { 228 lp.y = sTmpRect.top; 229 } 230 231 requestLayout(); 232 } 233 getSpanIncrement(float deltaFrac)234 private static int getSpanIncrement(float deltaFrac) { 235 return Math.abs(deltaFrac) > RESIZE_THRESHOLD ? Math.round(deltaFrac) : 0; 236 } 237 238 /** 239 * Based on the current deltas, we determine if and how to resize the widget. 240 */ resizeWidgetIfNeeded(boolean onDismiss)241 private void resizeWidgetIfNeeded(boolean onDismiss) { 242 float xThreshold = mCellLayout.getCellWidth(); 243 float yThreshold = mCellLayout.getCellHeight(); 244 245 int hSpanInc = getSpanIncrement((mDeltaX + mDeltaXAddOn) / xThreshold - mRunningHInc); 246 int vSpanInc = getSpanIncrement((mDeltaY + mDeltaYAddOn) / yThreshold - mRunningVInc); 247 248 if (!onDismiss && (hSpanInc == 0 && vSpanInc == 0)) return; 249 250 mDirectionVector[0] = 0; 251 mDirectionVector[1] = 0; 252 253 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mWidgetView.getLayoutParams(); 254 255 int spanX = lp.cellHSpan; 256 int spanY = lp.cellVSpan; 257 int cellX = lp.useTmpCoords ? lp.tmpCellX : lp.cellX; 258 int cellY = lp.useTmpCoords ? lp.tmpCellY : lp.cellY; 259 260 // For each border, we bound the resizing based on the minimum width, and the maximum 261 // expandability. 262 mTempRange1.set(cellX, spanX + cellX); 263 int hSpanDelta = mTempRange1.applyDeltaAndBound(mLeftBorderActive, mRightBorderActive, 264 hSpanInc, mMinHSpan, mCellLayout.getCountX(), mTempRange2); 265 cellX = mTempRange2.start; 266 spanX = mTempRange2.size(); 267 if (hSpanDelta != 0) { 268 mDirectionVector[0] = mLeftBorderActive ? -1 : 1; 269 } 270 271 mTempRange1.set(cellY, spanY + cellY); 272 int vSpanDelta = mTempRange1.applyDeltaAndBound(mTopBorderActive, mBottomBorderActive, 273 vSpanInc, mMinVSpan, mCellLayout.getCountY(), mTempRange2); 274 cellY = mTempRange2.start; 275 spanY = mTempRange2.size(); 276 if (vSpanDelta != 0) { 277 mDirectionVector[1] = mTopBorderActive ? -1 : 1; 278 } 279 280 if (!onDismiss && vSpanDelta == 0 && hSpanDelta == 0) return; 281 282 // We always want the final commit to match the feedback, so we make sure to use the 283 // last used direction vector when committing the resize / reorder. 284 if (onDismiss) { 285 mDirectionVector[0] = mLastDirectionVector[0]; 286 mDirectionVector[1] = mLastDirectionVector[1]; 287 } else { 288 mLastDirectionVector[0] = mDirectionVector[0]; 289 mLastDirectionVector[1] = mDirectionVector[1]; 290 } 291 292 if (mCellLayout.createAreaForResize(cellX, cellY, spanX, spanY, mWidgetView, 293 mDirectionVector, onDismiss)) { 294 if (mStateAnnouncer != null && (lp.cellHSpan != spanX || lp.cellVSpan != spanY) ) { 295 mStateAnnouncer.announce( 296 mLauncher.getString(R.string.widget_resized, spanX, spanY)); 297 } 298 299 lp.tmpCellX = cellX; 300 lp.tmpCellY = cellY; 301 lp.cellHSpan = spanX; 302 lp.cellVSpan = spanY; 303 mRunningVInc += vSpanDelta; 304 mRunningHInc += hSpanDelta; 305 306 if (!onDismiss) { 307 updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY); 308 } 309 } 310 mWidgetView.requestLayout(); 311 } 312 updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher, int spanX, int spanY)313 static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher, 314 int spanX, int spanY) { 315 getWidgetSizeRanges(launcher, spanX, spanY, sTmpRect); 316 widgetView.updateAppWidgetSize(null, sTmpRect.left, sTmpRect.top, 317 sTmpRect.right, sTmpRect.bottom); 318 } 319 getWidgetSizeRanges(Context context, int spanX, int spanY, Rect rect)320 public static Rect getWidgetSizeRanges(Context context, int spanX, int spanY, Rect rect) { 321 if (sCellSize == null) { 322 InvariantDeviceProfile inv = LauncherAppState.getIDP(context); 323 324 // Initiate cell sizes. 325 sCellSize = new Point[2]; 326 sCellSize[0] = inv.landscapeProfile.getCellSize(); 327 sCellSize[1] = inv.portraitProfile.getCellSize(); 328 } 329 330 if (rect == null) { 331 rect = new Rect(); 332 } 333 final float density = context.getResources().getDisplayMetrics().density; 334 335 // Compute landscape size 336 int landWidth = (int) ((spanX * sCellSize[0].x) / density); 337 int landHeight = (int) ((spanY * sCellSize[0].y) / density); 338 339 // Compute portrait size 340 int portWidth = (int) ((spanX * sCellSize[1].x) / density); 341 int portHeight = (int) ((spanY * sCellSize[1].y) / density); 342 rect.set(portWidth, landHeight, landWidth, portHeight); 343 return rect; 344 } 345 346 @Override onDetachedFromWindow()347 protected void onDetachedFromWindow() { 348 super.onDetachedFromWindow(); 349 350 // We are done with resizing the widget. Save the widget size & position to LauncherModel 351 resizeWidgetIfNeeded(true); 352 } 353 onTouchUp()354 private void onTouchUp() { 355 int xThreshold = mCellLayout.getCellWidth(); 356 int yThreshold = mCellLayout.getCellHeight(); 357 358 mDeltaXAddOn = mRunningHInc * xThreshold; 359 mDeltaYAddOn = mRunningVInc * yThreshold; 360 mDeltaX = 0; 361 mDeltaY = 0; 362 363 post(new Runnable() { 364 @Override 365 public void run() { 366 snapToWidget(true); 367 } 368 }); 369 } 370 371 /** 372 * Returns the rect of this view when the frame is snapped around the widget, with the bounds 373 * relative to the {@link DragLayer}. 374 */ getSnappedRectRelativeToDragLayer(Rect out)375 private void getSnappedRectRelativeToDragLayer(Rect out) { 376 float scale = mWidgetView.getScaleToFit(); 377 378 mDragLayer.getViewRectRelativeToSelf(mWidgetView, out); 379 380 int width = 2 * mBackgroundPadding 381 + (int) (scale * (out.width() - mWidgetPadding.left - mWidgetPadding.right)); 382 int height = 2 * mBackgroundPadding 383 + (int) (scale * (out.height() - mWidgetPadding.top - mWidgetPadding.bottom)); 384 385 int x = (int) (out.left - mBackgroundPadding + scale * mWidgetPadding.left); 386 int y = (int) (out.top - mBackgroundPadding + scale * mWidgetPadding.top); 387 388 out.left = x; 389 out.top = y; 390 out.right = out.left + width; 391 out.bottom = out.top + height; 392 } 393 snapToWidget(boolean animate)394 public void snapToWidget(boolean animate) { 395 getSnappedRectRelativeToDragLayer(sTmpRect); 396 int newWidth = sTmpRect.width(); 397 int newHeight = sTmpRect.height(); 398 int newX = sTmpRect.left; 399 int newY = sTmpRect.top; 400 401 // We need to make sure the frame's touchable regions lie fully within the bounds of the 402 // DragLayer. We allow the actual handles to be clipped, but we shift the touch regions 403 // down accordingly to provide a proper touch target. 404 if (newY < 0) { 405 // In this case we shift the touch region down to start at the top of the DragLayer 406 mTopTouchRegionAdjustment = -newY; 407 } else { 408 mTopTouchRegionAdjustment = 0; 409 } 410 if (newY + newHeight > mDragLayer.getHeight()) { 411 // In this case we shift the touch region up to end at the bottom of the DragLayer 412 mBottomTouchRegionAdjustment = -(newY + newHeight - mDragLayer.getHeight()); 413 } else { 414 mBottomTouchRegionAdjustment = 0; 415 } 416 417 final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); 418 if (!animate) { 419 lp.width = newWidth; 420 lp.height = newHeight; 421 lp.x = newX; 422 lp.y = newY; 423 for (int i = 0; i < HANDLE_COUNT; i++) { 424 mDragHandles[i].setAlpha(1.0f); 425 } 426 requestLayout(); 427 } else { 428 PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", lp.width, newWidth); 429 PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", lp.height, 430 newHeight); 431 PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", lp.x, newX); 432 PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", lp.y, newY); 433 ObjectAnimator oa = 434 LauncherAnimUtils.ofPropertyValuesHolder(lp, this, width, height, x, y); 435 oa.addUpdateListener(new AnimatorUpdateListener() { 436 public void onAnimationUpdate(ValueAnimator animation) { 437 requestLayout(); 438 } 439 }); 440 AnimatorSet set = LauncherAnimUtils.createAnimatorSet(); 441 set.play(oa); 442 for (int i = 0; i < HANDLE_COUNT; i++) { 443 set.play(LauncherAnimUtils.ofFloat(mDragHandles[i], ALPHA, 1.0f)); 444 } 445 446 set.setDuration(SNAP_DURATION); 447 set.start(); 448 } 449 450 setFocusableInTouchMode(true); 451 requestFocus(); 452 } 453 454 @Override onKey(View v, int keyCode, KeyEvent event)455 public boolean onKey(View v, int keyCode, KeyEvent event) { 456 // Clear the frame and give focus to the widget host view when a directional key is pressed. 457 if (FocusLogic.shouldConsume(keyCode)) { 458 mDragLayer.clearResizeFrame(); 459 mWidgetView.requestFocus(); 460 return true; 461 } 462 return false; 463 } 464 handleTouchDown(MotionEvent ev)465 private boolean handleTouchDown(MotionEvent ev) { 466 Rect hitRect = new Rect(); 467 int x = (int) ev.getX(); 468 int y = (int) ev.getY(); 469 470 getHitRect(hitRect); 471 if (hitRect.contains(x, y)) { 472 if (beginResizeIfPointInRegion(x - getLeft(), y - getTop())) { 473 mXDown = x; 474 mYDown = y; 475 return true; 476 } 477 } 478 return false; 479 } 480 481 @Override onControllerTouchEvent(MotionEvent ev)482 public boolean onControllerTouchEvent(MotionEvent ev) { 483 int action = ev.getAction(); 484 int x = (int) ev.getX(); 485 int y = (int) ev.getY(); 486 487 switch (action) { 488 case MotionEvent.ACTION_DOWN: 489 return handleTouchDown(ev); 490 case MotionEvent.ACTION_MOVE: 491 visualizeResizeForDelta(x - mXDown, y - mYDown); 492 break; 493 case MotionEvent.ACTION_CANCEL: 494 case MotionEvent.ACTION_UP: 495 visualizeResizeForDelta(x - mXDown, y - mYDown); 496 onTouchUp(); 497 mXDown = mYDown = 0; 498 break; 499 } 500 return true; 501 } 502 503 @Override onControllerInterceptTouchEvent(MotionEvent ev)504 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 505 if (ev.getAction() == MotionEvent.ACTION_DOWN && handleTouchDown(ev)) { 506 return true; 507 } 508 return false; 509 } 510 511 /** 512 * A mutable class for describing the range of two int values. 513 */ 514 private static class IntRange { 515 516 public int start, end; 517 clamp(int value)518 public int clamp(int value) { 519 return Utilities.boundToRange(value, start, end); 520 } 521 set(int s, int e)522 public void set(int s, int e) { 523 start = s; 524 end = e; 525 } 526 size()527 public int size() { 528 return end - start; 529 } 530 531 /** 532 * Moves either the start or end edge (but never both) by {@param delta} and sets the 533 * result in {@param out} 534 */ applyDelta(boolean moveStart, boolean moveEnd, int delta, IntRange out)535 public void applyDelta(boolean moveStart, boolean moveEnd, int delta, IntRange out) { 536 out.start = moveStart ? start + delta : start; 537 out.end = moveEnd ? end + delta : end; 538 } 539 540 /** 541 * Applies delta similar to {@link #applyDelta(boolean, boolean, int, IntRange)}, 542 * with extra conditions. 543 * @param minSize minimum size after with the moving edge should not be shifted any further. 544 * For eg, if delta = -3 when moving the endEdge brings the size to less than 545 * minSize, only delta = -2 will applied 546 * @param maxEnd The maximum value to the end edge (start edge is always restricted to 0) 547 * @return the amount of increase when endEdge was moves and the amount of decrease when 548 * the start edge was moved. 549 */ applyDeltaAndBound(boolean moveStart, boolean moveEnd, int delta, int minSize, int maxEnd, IntRange out)550 public int applyDeltaAndBound(boolean moveStart, boolean moveEnd, int delta, 551 int minSize, int maxEnd, IntRange out) { 552 applyDelta(moveStart, moveEnd, delta, out); 553 if (out.start < 0) { 554 out.start = 0; 555 } 556 if (out.end > maxEnd) { 557 out.end = maxEnd; 558 } 559 if (out.size() < minSize) { 560 if (moveStart) { 561 out.start = out.end - minSize; 562 } else if (moveEnd) { 563 out.end = out.start + minSize; 564 } 565 } 566 return moveEnd ? out.size() - size() : size() - out.size(); 567 } 568 } 569 } 570