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