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