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