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