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