1package com.android.launcher3; 2 3import com.android.launcher3.dragndrop.DragLayer; 4 5import android.animation.AnimatorSet; 6import android.animation.ObjectAnimator; 7import android.animation.PropertyValuesHolder; 8import android.animation.ValueAnimator; 9import android.animation.ValueAnimator.AnimatorUpdateListener; 10import android.appwidget.AppWidgetHostView; 11import android.appwidget.AppWidgetProviderInfo; 12import android.content.Context; 13import android.content.res.Resources; 14import android.graphics.Point; 15import android.graphics.Rect; 16import android.view.Gravity; 17import android.view.KeyEvent; 18import android.view.View; 19import android.widget.FrameLayout; 20import android.widget.ImageView; 21 22import com.android.launcher3.accessibility.DragViewStateAnnouncer; 23import com.android.launcher3.util.FocusLogic; 24 25public class AppWidgetResizeFrame extends FrameLayout implements View.OnKeyListener { 26 private static final int SNAP_DURATION = 150; 27 private static final float DIMMED_HANDLE_ALPHA = 0f; 28 private static final float RESIZE_THRESHOLD = 0.66f; 29 30 private static final Rect sTmpRect = new Rect(); 31 32 // Represents the cell size on the grid in the two orientations. 33 private static Point[] sCellSize; 34 35 private final Launcher mLauncher; 36 private final LauncherAppWidgetHostView mWidgetView; 37 private final CellLayout mCellLayout; 38 private final DragLayer mDragLayer; 39 40 private final ImageView mLeftHandle; 41 private final ImageView mRightHandle; 42 private final ImageView mTopHandle; 43 private final ImageView mBottomHandle; 44 45 private final Rect mWidgetPadding; 46 47 private final int mBackgroundPadding; 48 private final int mTouchTargetWidth; 49 50 private final int[] mDirectionVector = new int[2]; 51 private final int[] mLastDirectionVector = new int[2]; 52 private final int[] mTmpPt = new int[2]; 53 54 private final DragViewStateAnnouncer mStateAnnouncer; 55 56 private boolean mLeftBorderActive; 57 private boolean mRightBorderActive; 58 private boolean mTopBorderActive; 59 private boolean mBottomBorderActive; 60 61 private int mBaselineWidth; 62 private int mBaselineHeight; 63 private int mBaselineX; 64 private int mBaselineY; 65 private int mResizeMode; 66 67 private int mRunningHInc; 68 private int mRunningVInc; 69 private int mMinHSpan; 70 private int mMinVSpan; 71 private int mDeltaX; 72 private int mDeltaY; 73 private int mDeltaXAddOn; 74 private int mDeltaYAddOn; 75 76 private int mTopTouchRegionAdjustment = 0; 77 private int mBottomTouchRegionAdjustment = 0; 78 79 public AppWidgetResizeFrame(Context context, 80 LauncherAppWidgetHostView widgetView, CellLayout cellLayout, DragLayer dragLayer) { 81 82 super(context); 83 mLauncher = Launcher.getLauncher(context); 84 mCellLayout = cellLayout; 85 mWidgetView = widgetView; 86 LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) 87 widgetView.getAppWidgetInfo(); 88 mResizeMode = info.resizeMode; 89 mDragLayer = dragLayer; 90 91 mMinHSpan = info.minSpanX; 92 mMinVSpan = info.minSpanY; 93 94 mStateAnnouncer = DragViewStateAnnouncer.createFor(this); 95 96 setBackgroundResource(R.drawable.widget_resize_shadow); 97 setForeground(getResources().getDrawable(R.drawable.widget_resize_frame)); 98 setPadding(0, 0, 0, 0); 99 100 final int handleMargin = getResources().getDimensionPixelSize(R.dimen.widget_handle_margin); 101 LayoutParams lp; 102 mLeftHandle = new ImageView(context); 103 mLeftHandle.setImageResource(R.drawable.ic_widget_resize_handle); 104 lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 105 Gravity.LEFT | Gravity.CENTER_VERTICAL); 106 lp.leftMargin = handleMargin; 107 addView(mLeftHandle, lp); 108 109 mRightHandle = new ImageView(context); 110 mRightHandle.setImageResource(R.drawable.ic_widget_resize_handle); 111 lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 112 Gravity.RIGHT | Gravity.CENTER_VERTICAL); 113 lp.rightMargin = handleMargin; 114 addView(mRightHandle, lp); 115 116 mTopHandle = new ImageView(context); 117 mTopHandle.setImageResource(R.drawable.ic_widget_resize_handle); 118 lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 119 Gravity.CENTER_HORIZONTAL | Gravity.TOP); 120 lp.topMargin = handleMargin; 121 addView(mTopHandle, lp); 122 123 mBottomHandle = new ImageView(context); 124 mBottomHandle.setImageResource(R.drawable.ic_widget_resize_handle); 125 lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 126 Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); 127 lp.bottomMargin = handleMargin; 128 addView(mBottomHandle, lp); 129 130 if (!info.isCustomWidget) { 131 mWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, 132 widgetView.getAppWidgetInfo().provider, null); 133 } else { 134 Resources r = context.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 mTopHandle.setVisibility(GONE); 141 mBottomHandle.setVisibility(GONE); 142 } else if (mResizeMode == AppWidgetProviderInfo.RESIZE_VERTICAL) { 143 mLeftHandle.setVisibility(GONE); 144 mRightHandle.setVisibility(GONE); 145 } 146 147 mBackgroundPadding = getResources() 148 .getDimensionPixelSize(R.dimen.resize_frame_background_padding); 149 mTouchTargetWidth = 2 * mBackgroundPadding; 150 151 // When we create the resize frame, we first mark all cells as unoccupied. The appropriate 152 // cells (same if not resized, or different) will be marked as occupied when the resize 153 // frame is dismissed. 154 mCellLayout.markCellsAsUnoccupiedForView(mWidgetView); 155 156 setOnKeyListener(this); 157 } 158 159 public boolean beginResizeIfPointInRegion(int x, int y) { 160 boolean horizontalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0; 161 boolean verticalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0; 162 163 mLeftBorderActive = (x < mTouchTargetWidth) && horizontalActive; 164 mRightBorderActive = (x > getWidth() - mTouchTargetWidth) && horizontalActive; 165 mTopBorderActive = (y < mTouchTargetWidth + mTopTouchRegionAdjustment) && verticalActive; 166 mBottomBorderActive = (y > getHeight() - mTouchTargetWidth + mBottomTouchRegionAdjustment) 167 && verticalActive; 168 169 boolean anyBordersActive = mLeftBorderActive || mRightBorderActive 170 || mTopBorderActive || mBottomBorderActive; 171 172 mBaselineWidth = getMeasuredWidth(); 173 mBaselineHeight = getMeasuredHeight(); 174 mBaselineX = getLeft(); 175 mBaselineY = getTop(); 176 177 if (anyBordersActive) { 178 mLeftHandle.setAlpha(mLeftBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); 179 mRightHandle.setAlpha(mRightBorderActive ? 1.0f :DIMMED_HANDLE_ALPHA); 180 mTopHandle.setAlpha(mTopBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); 181 mBottomHandle.setAlpha(mBottomBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); 182 } 183 return anyBordersActive; 184 } 185 186 /** 187 * Here we bound the deltas such that the frame cannot be stretched beyond the extents 188 * of the CellLayout, and such that the frame's borders can't cross. 189 */ 190 public void updateDeltas(int deltaX, int deltaY) { 191 if (mLeftBorderActive) { 192 mDeltaX = Math.max(-mBaselineX, deltaX); 193 mDeltaX = Math.min(mBaselineWidth - 2 * mTouchTargetWidth, mDeltaX); 194 } else if (mRightBorderActive) { 195 mDeltaX = Math.min(mDragLayer.getWidth() - (mBaselineX + mBaselineWidth), deltaX); 196 mDeltaX = Math.max(-mBaselineWidth + 2 * mTouchTargetWidth, mDeltaX); 197 } 198 199 if (mTopBorderActive) { 200 mDeltaY = Math.max(-mBaselineY, deltaY); 201 mDeltaY = Math.min(mBaselineHeight - 2 * mTouchTargetWidth, mDeltaY); 202 } else if (mBottomBorderActive) { 203 mDeltaY = Math.min(mDragLayer.getHeight() - (mBaselineY + mBaselineHeight), deltaY); 204 mDeltaY = Math.max(-mBaselineHeight + 2 * mTouchTargetWidth, mDeltaY); 205 } 206 } 207 208 public void visualizeResizeForDelta(int deltaX, int deltaY) { 209 visualizeResizeForDelta(deltaX, deltaY, false); 210 } 211 212 /** 213 * Based on the deltas, we resize the frame, and, if needed, we resize the widget. 214 */ 215 private void visualizeResizeForDelta(int deltaX, int deltaY, boolean onDismiss) { 216 updateDeltas(deltaX, deltaY); 217 DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); 218 219 if (mLeftBorderActive) { 220 lp.x = mBaselineX + mDeltaX; 221 lp.width = mBaselineWidth - mDeltaX; 222 } else if (mRightBorderActive) { 223 lp.width = mBaselineWidth + mDeltaX; 224 } 225 226 if (mTopBorderActive) { 227 lp.y = mBaselineY + mDeltaY; 228 lp.height = mBaselineHeight - mDeltaY; 229 } else if (mBottomBorderActive) { 230 lp.height = mBaselineHeight + mDeltaY; 231 } 232 233 resizeWidgetIfNeeded(onDismiss); 234 requestLayout(); 235 } 236 237 /** 238 * Based on the current deltas, we determine if and how to resize the widget. 239 */ 240 private void resizeWidgetIfNeeded(boolean onDismiss) { 241 int xThreshold = mCellLayout.getCellWidth() + mCellLayout.getWidthGap(); 242 int yThreshold = mCellLayout.getCellHeight() + mCellLayout.getHeightGap(); 243 244 int deltaX = mDeltaX + mDeltaXAddOn; 245 int deltaY = mDeltaY + mDeltaYAddOn; 246 247 float hSpanIncF = 1.0f * deltaX / xThreshold - mRunningHInc; 248 float vSpanIncF = 1.0f * deltaY / yThreshold - mRunningVInc; 249 250 int hSpanInc = 0; 251 int vSpanInc = 0; 252 int cellXInc = 0; 253 int cellYInc = 0; 254 255 int countX = mCellLayout.getCountX(); 256 int countY = mCellLayout.getCountY(); 257 258 if (Math.abs(hSpanIncF) > RESIZE_THRESHOLD) { 259 hSpanInc = Math.round(hSpanIncF); 260 } 261 if (Math.abs(vSpanIncF) > RESIZE_THRESHOLD) { 262 vSpanInc = Math.round(vSpanIncF); 263 } 264 265 if (!onDismiss && (hSpanInc == 0 && vSpanInc == 0)) return; 266 267 268 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mWidgetView.getLayoutParams(); 269 270 int spanX = lp.cellHSpan; 271 int spanY = lp.cellVSpan; 272 int cellX = lp.useTmpCoords ? lp.tmpCellX : lp.cellX; 273 int cellY = lp.useTmpCoords ? lp.tmpCellY : lp.cellY; 274 275 int hSpanDelta = 0; 276 int vSpanDelta = 0; 277 278 // For each border, we bound the resizing based on the minimum width, and the maximum 279 // expandability. 280 if (mLeftBorderActive) { 281 cellXInc = Math.max(-cellX, hSpanInc); 282 cellXInc = Math.min(lp.cellHSpan - mMinHSpan, cellXInc); 283 hSpanInc *= -1; 284 hSpanInc = Math.min(cellX, hSpanInc); 285 hSpanInc = Math.max(-(lp.cellHSpan - mMinHSpan), hSpanInc); 286 hSpanDelta = -hSpanInc; 287 288 } else if (mRightBorderActive) { 289 hSpanInc = Math.min(countX - (cellX + spanX), hSpanInc); 290 hSpanInc = Math.max(-(lp.cellHSpan - mMinHSpan), hSpanInc); 291 hSpanDelta = hSpanInc; 292 } 293 294 if (mTopBorderActive) { 295 cellYInc = Math.max(-cellY, vSpanInc); 296 cellYInc = Math.min(lp.cellVSpan - mMinVSpan, cellYInc); 297 vSpanInc *= -1; 298 vSpanInc = Math.min(cellY, vSpanInc); 299 vSpanInc = Math.max(-(lp.cellVSpan - mMinVSpan), vSpanInc); 300 vSpanDelta = -vSpanInc; 301 } else if (mBottomBorderActive) { 302 vSpanInc = Math.min(countY - (cellY + spanY), vSpanInc); 303 vSpanInc = Math.max(-(lp.cellVSpan - mMinVSpan), vSpanInc); 304 vSpanDelta = vSpanInc; 305 } 306 307 mDirectionVector[0] = 0; 308 mDirectionVector[1] = 0; 309 // Update the widget's dimensions and position according to the deltas computed above 310 if (mLeftBorderActive || mRightBorderActive) { 311 spanX += hSpanInc; 312 cellX += cellXInc; 313 if (hSpanDelta != 0) { 314 mDirectionVector[0] = mLeftBorderActive ? -1 : 1; 315 } 316 } 317 318 if (mTopBorderActive || mBottomBorderActive) { 319 spanY += vSpanInc; 320 cellY += cellYInc; 321 if (vSpanDelta != 0) { 322 mDirectionVector[1] = mTopBorderActive ? -1 : 1; 323 } 324 } 325 326 if (!onDismiss && vSpanDelta == 0 && hSpanDelta == 0) return; 327 328 // We always want the final commit to match the feedback, so we make sure to use the 329 // last used direction vector when committing the resize / reorder. 330 if (onDismiss) { 331 mDirectionVector[0] = mLastDirectionVector[0]; 332 mDirectionVector[1] = mLastDirectionVector[1]; 333 } else { 334 mLastDirectionVector[0] = mDirectionVector[0]; 335 mLastDirectionVector[1] = mDirectionVector[1]; 336 } 337 338 if (mCellLayout.createAreaForResize(cellX, cellY, spanX, spanY, mWidgetView, 339 mDirectionVector, onDismiss)) { 340 if (mStateAnnouncer != null && (lp.cellHSpan != spanX || lp.cellVSpan != spanY) ) { 341 mStateAnnouncer.announce( 342 mLauncher.getString(R.string.widget_resized, spanX, spanY)); 343 } 344 345 lp.tmpCellX = cellX; 346 lp.tmpCellY = cellY; 347 lp.cellHSpan = spanX; 348 lp.cellVSpan = spanY; 349 mRunningVInc += vSpanDelta; 350 mRunningHInc += hSpanDelta; 351 352 if (!onDismiss) { 353 updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY); 354 } 355 } 356 mWidgetView.requestLayout(); 357 } 358 359 static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher, 360 int spanX, int spanY) { 361 getWidgetSizeRanges(launcher, spanX, spanY, sTmpRect); 362 widgetView.updateAppWidgetSize(null, sTmpRect.left, sTmpRect.top, 363 sTmpRect.right, sTmpRect.bottom); 364 } 365 366 public static Rect getWidgetSizeRanges(Context context, int spanX, int spanY, Rect rect) { 367 if (sCellSize == null) { 368 InvariantDeviceProfile inv = LauncherAppState.getInstance().getInvariantDeviceProfile(); 369 370 // Initiate cell sizes. 371 sCellSize = new Point[2]; 372 sCellSize[0] = inv.landscapeProfile.getCellSize(); 373 sCellSize[1] = inv.portraitProfile.getCellSize(); 374 } 375 376 if (rect == null) { 377 rect = new Rect(); 378 } 379 final float density = context.getResources().getDisplayMetrics().density; 380 381 // Compute landscape size 382 int landWidth = (int) ((spanX * sCellSize[0].x) / density); 383 int landHeight = (int) ((spanY * sCellSize[0].y) / density); 384 385 // Compute portrait size 386 int portWidth = (int) ((spanX * sCellSize[1].x) / density); 387 int portHeight = (int) ((spanY * sCellSize[1].y) / density); 388 rect.set(portWidth, landHeight, landWidth, portHeight); 389 return rect; 390 } 391 392 /** 393 * This is the final step of the resize. Here we save the new widget size and position 394 * to LauncherModel and animate the resize frame. 395 */ 396 public void commitResize() { 397 resizeWidgetIfNeeded(true); 398 requestLayout(); 399 } 400 401 public void onTouchUp() { 402 int xThreshold = mCellLayout.getCellWidth() + mCellLayout.getWidthGap(); 403 int yThreshold = mCellLayout.getCellHeight() + mCellLayout.getHeightGap(); 404 405 mDeltaXAddOn = mRunningHInc * xThreshold; 406 mDeltaYAddOn = mRunningVInc * yThreshold; 407 mDeltaX = 0; 408 mDeltaY = 0; 409 410 post(new Runnable() { 411 @Override 412 public void run() { 413 snapToWidget(true); 414 } 415 }); 416 } 417 418 public void snapToWidget(boolean animate) { 419 final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); 420 int newWidth = mWidgetView.getWidth() + 2 * mBackgroundPadding 421 - mWidgetPadding.left - mWidgetPadding.right; 422 int newHeight = mWidgetView.getHeight() + 2 * mBackgroundPadding 423 - mWidgetPadding.top - mWidgetPadding.bottom; 424 425 mTmpPt[0] = mWidgetView.getLeft(); 426 mTmpPt[1] = mWidgetView.getTop(); 427 mDragLayer.getDescendantCoordRelativeToSelf(mCellLayout.getShortcutsAndWidgets(), mTmpPt); 428 429 int newX = mTmpPt[0] - mBackgroundPadding + mWidgetPadding.left; 430 int newY = mTmpPt[1] - mBackgroundPadding + mWidgetPadding.top; 431 432 // We need to make sure the frame's touchable regions lie fully within the bounds of the 433 // DragLayer. We allow the actual handles to be clipped, but we shift the touch regions 434 // down accordingly to provide a proper touch target. 435 if (newY < 0) { 436 // In this case we shift the touch region down to start at the top of the DragLayer 437 mTopTouchRegionAdjustment = -newY; 438 } else { 439 mTopTouchRegionAdjustment = 0; 440 } 441 if (newY + newHeight > mDragLayer.getHeight()) { 442 // In this case we shift the touch region up to end at the bottom of the DragLayer 443 mBottomTouchRegionAdjustment = -(newY + newHeight - mDragLayer.getHeight()); 444 } else { 445 mBottomTouchRegionAdjustment = 0; 446 } 447 448 if (!animate) { 449 lp.width = newWidth; 450 lp.height = newHeight; 451 lp.x = newX; 452 lp.y = newY; 453 mLeftHandle.setAlpha(1.0f); 454 mRightHandle.setAlpha(1.0f); 455 mTopHandle.setAlpha(1.0f); 456 mBottomHandle.setAlpha(1.0f); 457 requestLayout(); 458 } else { 459 PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", lp.width, newWidth); 460 PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", lp.height, 461 newHeight); 462 PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", lp.x, newX); 463 PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", lp.y, newY); 464 ObjectAnimator oa = 465 LauncherAnimUtils.ofPropertyValuesHolder(lp, this, width, height, x, y); 466 ObjectAnimator leftOa = LauncherAnimUtils.ofFloat(mLeftHandle, ALPHA, 1.0f); 467 ObjectAnimator rightOa = LauncherAnimUtils.ofFloat(mRightHandle, ALPHA, 1.0f); 468 ObjectAnimator topOa = LauncherAnimUtils.ofFloat(mTopHandle, ALPHA, 1.0f); 469 ObjectAnimator bottomOa = LauncherAnimUtils.ofFloat(mBottomHandle, ALPHA, 1.0f); 470 oa.addUpdateListener(new AnimatorUpdateListener() { 471 public void onAnimationUpdate(ValueAnimator animation) { 472 requestLayout(); 473 } 474 }); 475 AnimatorSet set = LauncherAnimUtils.createAnimatorSet(); 476 if (mResizeMode == AppWidgetProviderInfo.RESIZE_VERTICAL) { 477 set.playTogether(oa, topOa, bottomOa); 478 } else if (mResizeMode == AppWidgetProviderInfo.RESIZE_HORIZONTAL) { 479 set.playTogether(oa, leftOa, rightOa); 480 } else { 481 set.playTogether(oa, leftOa, rightOa, topOa, bottomOa); 482 } 483 484 set.setDuration(SNAP_DURATION); 485 set.start(); 486 } 487 488 setFocusableInTouchMode(true); 489 requestFocus(); 490 } 491 492 @Override 493 public boolean onKey(View v, int keyCode, KeyEvent event) { 494 // Clear the frame and give focus to the widget host view when a directional key is pressed. 495 if (FocusLogic.shouldConsume(keyCode)) { 496 mDragLayer.clearAllResizeFrames(); 497 mWidgetView.requestFocus(); 498 return true; 499 } 500 return false; 501 } 502} 503