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