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