| package com.android.launcher3; |
| |
| import android.animation.AnimatorSet; |
| import android.animation.ObjectAnimator; |
| import android.animation.PropertyValuesHolder; |
| import android.animation.ValueAnimator; |
| import android.animation.ValueAnimator.AnimatorUpdateListener; |
| import android.appwidget.AppWidgetHostView; |
| import android.appwidget.AppWidgetProviderInfo; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Rect; |
| import android.view.Gravity; |
| import android.widget.FrameLayout; |
| import android.widget.ImageView; |
| |
| public class AppWidgetResizeFrame extends FrameLayout { |
| private static final int SNAP_DURATION = 150; |
| private static final float DIMMED_HANDLE_ALPHA = 0f; |
| private static final float RESIZE_THRESHOLD = 0.66f; |
| |
| private static Rect sTmpRect = new Rect(); |
| |
| private final Launcher mLauncher; |
| private final LauncherAppWidgetHostView mWidgetView; |
| private final CellLayout mCellLayout; |
| private final DragLayer mDragLayer; |
| |
| private final ImageView mLeftHandle; |
| private final ImageView mRightHandle; |
| private final ImageView mTopHandle; |
| private final ImageView mBottomHandle; |
| |
| private final Rect mWidgetPadding; |
| |
| private final int mBackgroundPadding; |
| private final int mTouchTargetWidth; |
| |
| private final int[] mDirectionVector = new int[2]; |
| private final int[] mLastDirectionVector = new int[2]; |
| private final int[] mTmpPt = new int[2]; |
| |
| private boolean mLeftBorderActive; |
| private boolean mRightBorderActive; |
| private boolean mTopBorderActive; |
| private boolean mBottomBorderActive; |
| |
| private int mBaselineWidth; |
| private int mBaselineHeight; |
| private int mBaselineX; |
| private int mBaselineY; |
| private int mResizeMode; |
| |
| private int mRunningHInc; |
| private int mRunningVInc; |
| private int mMinHSpan; |
| private int mMinVSpan; |
| private int mDeltaX; |
| private int mDeltaY; |
| private int mDeltaXAddOn; |
| private int mDeltaYAddOn; |
| |
| private int mTopTouchRegionAdjustment = 0; |
| private int mBottomTouchRegionAdjustment = 0; |
| |
| public AppWidgetResizeFrame(Context context, |
| LauncherAppWidgetHostView widgetView, CellLayout cellLayout, DragLayer dragLayer) { |
| |
| super(context); |
| mLauncher = (Launcher) context; |
| mCellLayout = cellLayout; |
| mWidgetView = widgetView; |
| LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) |
| widgetView.getAppWidgetInfo(); |
| mResizeMode = info.resizeMode; |
| mDragLayer = dragLayer; |
| |
| mMinHSpan = info.minSpanX; |
| mMinVSpan = info.minSpanY; |
| |
| setBackgroundResource(R.drawable.widget_resize_shadow); |
| setForeground(getResources().getDrawable(R.drawable.widget_resize_frame)); |
| setPadding(0, 0, 0, 0); |
| |
| final int handleMargin = getResources().getDimensionPixelSize(R.dimen.widget_handle_margin); |
| LayoutParams lp; |
| mLeftHandle = new ImageView(context); |
| mLeftHandle.setImageResource(R.drawable.ic_widget_resize_handle); |
| lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, |
| Gravity.LEFT | Gravity.CENTER_VERTICAL); |
| lp.leftMargin = handleMargin; |
| addView(mLeftHandle, lp); |
| |
| mRightHandle = new ImageView(context); |
| mRightHandle.setImageResource(R.drawable.ic_widget_resize_handle); |
| lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, |
| Gravity.RIGHT | Gravity.CENTER_VERTICAL); |
| lp.rightMargin = handleMargin; |
| addView(mRightHandle, lp); |
| |
| mTopHandle = new ImageView(context); |
| mTopHandle.setImageResource(R.drawable.ic_widget_resize_handle); |
| lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, |
| Gravity.CENTER_HORIZONTAL | Gravity.TOP); |
| lp.topMargin = handleMargin; |
| addView(mTopHandle, lp); |
| |
| mBottomHandle = new ImageView(context); |
| mBottomHandle.setImageResource(R.drawable.ic_widget_resize_handle); |
| lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, |
| Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); |
| lp.bottomMargin = handleMargin; |
| addView(mBottomHandle, lp); |
| |
| if (!info.isCustomWidget) { |
| mWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, |
| widgetView.getAppWidgetInfo().provider, null); |
| } else { |
| Resources r = context.getResources(); |
| int padding = r.getDimensionPixelSize(R.dimen.default_widget_padding); |
| mWidgetPadding = new Rect(padding, padding, padding, padding); |
| } |
| |
| if (mResizeMode == AppWidgetProviderInfo.RESIZE_HORIZONTAL) { |
| mTopHandle.setVisibility(GONE); |
| mBottomHandle.setVisibility(GONE); |
| } else if (mResizeMode == AppWidgetProviderInfo.RESIZE_VERTICAL) { |
| mLeftHandle.setVisibility(GONE); |
| mRightHandle.setVisibility(GONE); |
| } |
| |
| mBackgroundPadding = getResources() |
| .getDimensionPixelSize(R.dimen.resize_frame_background_padding); |
| mTouchTargetWidth = 2 * mBackgroundPadding; |
| |
| // When we create the resize frame, we first mark all cells as unoccupied. The appropriate |
| // cells (same if not resized, or different) will be marked as occupied when the resize |
| // frame is dismissed. |
| mCellLayout.markCellsAsUnoccupiedForView(mWidgetView); |
| } |
| |
| public boolean beginResizeIfPointInRegion(int x, int y) { |
| boolean horizontalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0; |
| boolean verticalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0; |
| |
| mLeftBorderActive = (x < mTouchTargetWidth) && horizontalActive; |
| mRightBorderActive = (x > getWidth() - mTouchTargetWidth) && horizontalActive; |
| mTopBorderActive = (y < mTouchTargetWidth + mTopTouchRegionAdjustment) && verticalActive; |
| mBottomBorderActive = (y > getHeight() - mTouchTargetWidth + mBottomTouchRegionAdjustment) |
| && verticalActive; |
| |
| boolean anyBordersActive = mLeftBorderActive || mRightBorderActive |
| || mTopBorderActive || mBottomBorderActive; |
| |
| mBaselineWidth = getMeasuredWidth(); |
| mBaselineHeight = getMeasuredHeight(); |
| mBaselineX = getLeft(); |
| mBaselineY = getTop(); |
| |
| if (anyBordersActive) { |
| mLeftHandle.setAlpha(mLeftBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); |
| mRightHandle.setAlpha(mRightBorderActive ? 1.0f :DIMMED_HANDLE_ALPHA); |
| mTopHandle.setAlpha(mTopBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); |
| mBottomHandle.setAlpha(mBottomBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); |
| } |
| return anyBordersActive; |
| } |
| |
| /** |
| * Here we bound the deltas such that the frame cannot be stretched beyond the extents |
| * of the CellLayout, and such that the frame's borders can't cross. |
| */ |
| public void updateDeltas(int deltaX, int deltaY) { |
| if (mLeftBorderActive) { |
| mDeltaX = Math.max(-mBaselineX, deltaX); |
| mDeltaX = Math.min(mBaselineWidth - 2 * mTouchTargetWidth, mDeltaX); |
| } else if (mRightBorderActive) { |
| mDeltaX = Math.min(mDragLayer.getWidth() - (mBaselineX + mBaselineWidth), deltaX); |
| mDeltaX = Math.max(-mBaselineWidth + 2 * mTouchTargetWidth, mDeltaX); |
| } |
| |
| if (mTopBorderActive) { |
| mDeltaY = Math.max(-mBaselineY, deltaY); |
| mDeltaY = Math.min(mBaselineHeight - 2 * mTouchTargetWidth, mDeltaY); |
| } else if (mBottomBorderActive) { |
| mDeltaY = Math.min(mDragLayer.getHeight() - (mBaselineY + mBaselineHeight), deltaY); |
| mDeltaY = Math.max(-mBaselineHeight + 2 * mTouchTargetWidth, mDeltaY); |
| } |
| } |
| |
| public void visualizeResizeForDelta(int deltaX, int deltaY) { |
| visualizeResizeForDelta(deltaX, deltaY, false); |
| } |
| |
| /** |
| * Based on the deltas, we resize the frame, and, if needed, we resize the widget. |
| */ |
| private void visualizeResizeForDelta(int deltaX, int deltaY, boolean onDismiss) { |
| updateDeltas(deltaX, deltaY); |
| DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); |
| |
| if (mLeftBorderActive) { |
| lp.x = mBaselineX + mDeltaX; |
| lp.width = mBaselineWidth - mDeltaX; |
| } else if (mRightBorderActive) { |
| lp.width = mBaselineWidth + mDeltaX; |
| } |
| |
| if (mTopBorderActive) { |
| lp.y = mBaselineY + mDeltaY; |
| lp.height = mBaselineHeight - mDeltaY; |
| } else if (mBottomBorderActive) { |
| lp.height = mBaselineHeight + mDeltaY; |
| } |
| |
| resizeWidgetIfNeeded(onDismiss); |
| requestLayout(); |
| } |
| |
| /** |
| * Based on the current deltas, we determine if and how to resize the widget. |
| */ |
| private void resizeWidgetIfNeeded(boolean onDismiss) { |
| int xThreshold = mCellLayout.getCellWidth() + mCellLayout.getWidthGap(); |
| int yThreshold = mCellLayout.getCellHeight() + mCellLayout.getHeightGap(); |
| |
| int deltaX = mDeltaX + mDeltaXAddOn; |
| int deltaY = mDeltaY + mDeltaYAddOn; |
| |
| float hSpanIncF = 1.0f * deltaX / xThreshold - mRunningHInc; |
| float vSpanIncF = 1.0f * deltaY / yThreshold - mRunningVInc; |
| |
| int hSpanInc = 0; |
| int vSpanInc = 0; |
| int cellXInc = 0; |
| int cellYInc = 0; |
| |
| int countX = mCellLayout.getCountX(); |
| int countY = mCellLayout.getCountY(); |
| |
| if (Math.abs(hSpanIncF) > RESIZE_THRESHOLD) { |
| hSpanInc = Math.round(hSpanIncF); |
| } |
| if (Math.abs(vSpanIncF) > RESIZE_THRESHOLD) { |
| vSpanInc = Math.round(vSpanIncF); |
| } |
| |
| if (!onDismiss && (hSpanInc == 0 && vSpanInc == 0)) return; |
| |
| |
| CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mWidgetView.getLayoutParams(); |
| |
| int spanX = lp.cellHSpan; |
| int spanY = lp.cellVSpan; |
| int cellX = lp.useTmpCoords ? lp.tmpCellX : lp.cellX; |
| int cellY = lp.useTmpCoords ? lp.tmpCellY : lp.cellY; |
| |
| int hSpanDelta = 0; |
| int vSpanDelta = 0; |
| |
| // For each border, we bound the resizing based on the minimum width, and the maximum |
| // expandability. |
| if (mLeftBorderActive) { |
| cellXInc = Math.max(-cellX, hSpanInc); |
| cellXInc = Math.min(lp.cellHSpan - mMinHSpan, cellXInc); |
| hSpanInc *= -1; |
| hSpanInc = Math.min(cellX, hSpanInc); |
| hSpanInc = Math.max(-(lp.cellHSpan - mMinHSpan), hSpanInc); |
| hSpanDelta = -hSpanInc; |
| |
| } else if (mRightBorderActive) { |
| hSpanInc = Math.min(countX - (cellX + spanX), hSpanInc); |
| hSpanInc = Math.max(-(lp.cellHSpan - mMinHSpan), hSpanInc); |
| hSpanDelta = hSpanInc; |
| } |
| |
| if (mTopBorderActive) { |
| cellYInc = Math.max(-cellY, vSpanInc); |
| cellYInc = Math.min(lp.cellVSpan - mMinVSpan, cellYInc); |
| vSpanInc *= -1; |
| vSpanInc = Math.min(cellY, vSpanInc); |
| vSpanInc = Math.max(-(lp.cellVSpan - mMinVSpan), vSpanInc); |
| vSpanDelta = -vSpanInc; |
| } else if (mBottomBorderActive) { |
| vSpanInc = Math.min(countY - (cellY + spanY), vSpanInc); |
| vSpanInc = Math.max(-(lp.cellVSpan - mMinVSpan), vSpanInc); |
| vSpanDelta = vSpanInc; |
| } |
| |
| mDirectionVector[0] = 0; |
| mDirectionVector[1] = 0; |
| // Update the widget's dimensions and position according to the deltas computed above |
| if (mLeftBorderActive || mRightBorderActive) { |
| spanX += hSpanInc; |
| cellX += cellXInc; |
| if (hSpanDelta != 0) { |
| mDirectionVector[0] = mLeftBorderActive ? -1 : 1; |
| } |
| } |
| |
| if (mTopBorderActive || mBottomBorderActive) { |
| spanY += vSpanInc; |
| cellY += cellYInc; |
| if (vSpanDelta != 0) { |
| mDirectionVector[1] = mTopBorderActive ? -1 : 1; |
| } |
| } |
| |
| if (!onDismiss && vSpanDelta == 0 && hSpanDelta == 0) return; |
| |
| // We always want the final commit to match the feedback, so we make sure to use the |
| // last used direction vector when committing the resize / reorder. |
| if (onDismiss) { |
| mDirectionVector[0] = mLastDirectionVector[0]; |
| mDirectionVector[1] = mLastDirectionVector[1]; |
| } else { |
| mLastDirectionVector[0] = mDirectionVector[0]; |
| mLastDirectionVector[1] = mDirectionVector[1]; |
| } |
| |
| if (mCellLayout.createAreaForResize(cellX, cellY, spanX, spanY, mWidgetView, |
| mDirectionVector, onDismiss)) { |
| lp.tmpCellX = cellX; |
| lp.tmpCellY = cellY; |
| lp.cellHSpan = spanX; |
| lp.cellVSpan = spanY; |
| mRunningVInc += vSpanDelta; |
| mRunningHInc += hSpanDelta; |
| if (!onDismiss) { |
| updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY); |
| } |
| } |
| mWidgetView.requestLayout(); |
| } |
| |
| static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher, |
| int spanX, int spanY) { |
| getWidgetSizeRanges(launcher, spanX, spanY, sTmpRect); |
| widgetView.updateAppWidgetSize(null, sTmpRect.left, sTmpRect.top, |
| sTmpRect.right, sTmpRect.bottom); |
| } |
| |
| public static Rect getWidgetSizeRanges(Launcher launcher, int spanX, int spanY, Rect rect) { |
| if (rect == null) { |
| rect = new Rect(); |
| } |
| Rect landMetrics = Workspace.getCellLayoutMetrics(launcher, CellLayout.LANDSCAPE); |
| Rect portMetrics = Workspace.getCellLayoutMetrics(launcher, CellLayout.PORTRAIT); |
| final float density = launcher.getResources().getDisplayMetrics().density; |
| |
| // Compute landscape size |
| int cellWidth = landMetrics.left; |
| int cellHeight = landMetrics.top; |
| int widthGap = landMetrics.right; |
| int heightGap = landMetrics.bottom; |
| int landWidth = (int) ((spanX * cellWidth + (spanX - 1) * widthGap) / density); |
| int landHeight = (int) ((spanY * cellHeight + (spanY - 1) * heightGap) / density); |
| |
| // Compute portrait size |
| cellWidth = portMetrics.left; |
| cellHeight = portMetrics.top; |
| widthGap = portMetrics.right; |
| heightGap = portMetrics.bottom; |
| int portWidth = (int) ((spanX * cellWidth + (spanX - 1) * widthGap) / density); |
| int portHeight = (int) ((spanY * cellHeight + (spanY - 1) * heightGap) / density); |
| rect.set(portWidth, landHeight, landWidth, portHeight); |
| return rect; |
| } |
| |
| /** |
| * This is the final step of the resize. Here we save the new widget size and position |
| * to LauncherModel and animate the resize frame. |
| */ |
| public void commitResize() { |
| resizeWidgetIfNeeded(true); |
| requestLayout(); |
| } |
| |
| public void onTouchUp() { |
| int xThreshold = mCellLayout.getCellWidth() + mCellLayout.getWidthGap(); |
| int yThreshold = mCellLayout.getCellHeight() + mCellLayout.getHeightGap(); |
| |
| mDeltaXAddOn = mRunningHInc * xThreshold; |
| mDeltaYAddOn = mRunningVInc * yThreshold; |
| mDeltaX = 0; |
| mDeltaY = 0; |
| |
| post(new Runnable() { |
| @Override |
| public void run() { |
| snapToWidget(true); |
| } |
| }); |
| } |
| |
| public void snapToWidget(boolean animate) { |
| final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); |
| int newWidth = mWidgetView.getWidth() + 2 * mBackgroundPadding |
| - mWidgetPadding.left - mWidgetPadding.right; |
| int newHeight = mWidgetView.getHeight() + 2 * mBackgroundPadding |
| - mWidgetPadding.top - mWidgetPadding.bottom; |
| |
| mTmpPt[0] = mWidgetView.getLeft(); |
| mTmpPt[1] = mWidgetView.getTop(); |
| mDragLayer.getDescendantCoordRelativeToSelf(mCellLayout.getShortcutsAndWidgets(), mTmpPt); |
| |
| int newX = mTmpPt[0] - mBackgroundPadding + mWidgetPadding.left; |
| int newY = mTmpPt[1] - mBackgroundPadding + mWidgetPadding.top; |
| |
| // We need to make sure the frame's touchable regions lie fully within the bounds of the |
| // DragLayer. We allow the actual handles to be clipped, but we shift the touch regions |
| // down accordingly to provide a proper touch target. |
| if (newY < 0) { |
| // In this case we shift the touch region down to start at the top of the DragLayer |
| mTopTouchRegionAdjustment = -newY; |
| } else { |
| mTopTouchRegionAdjustment = 0; |
| } |
| if (newY + newHeight > mDragLayer.getHeight()) { |
| // In this case we shift the touch region up to end at the bottom of the DragLayer |
| mBottomTouchRegionAdjustment = -(newY + newHeight - mDragLayer.getHeight()); |
| } else { |
| mBottomTouchRegionAdjustment = 0; |
| } |
| |
| if (!animate) { |
| lp.width = newWidth; |
| lp.height = newHeight; |
| lp.x = newX; |
| lp.y = newY; |
| mLeftHandle.setAlpha(1.0f); |
| mRightHandle.setAlpha(1.0f); |
| mTopHandle.setAlpha(1.0f); |
| mBottomHandle.setAlpha(1.0f); |
| requestLayout(); |
| } else { |
| PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", lp.width, newWidth); |
| PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", lp.height, |
| newHeight); |
| PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", lp.x, newX); |
| PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", lp.y, newY); |
| ObjectAnimator oa = |
| LauncherAnimUtils.ofPropertyValuesHolder(lp, this, width, height, x, y); |
| ObjectAnimator leftOa = LauncherAnimUtils.ofFloat(mLeftHandle, "alpha", 1.0f); |
| ObjectAnimator rightOa = LauncherAnimUtils.ofFloat(mRightHandle, "alpha", 1.0f); |
| ObjectAnimator topOa = LauncherAnimUtils.ofFloat(mTopHandle, "alpha", 1.0f); |
| ObjectAnimator bottomOa = LauncherAnimUtils.ofFloat(mBottomHandle, "alpha", 1.0f); |
| oa.addUpdateListener(new AnimatorUpdateListener() { |
| public void onAnimationUpdate(ValueAnimator animation) { |
| requestLayout(); |
| } |
| }); |
| AnimatorSet set = LauncherAnimUtils.createAnimatorSet(); |
| if (mResizeMode == AppWidgetProviderInfo.RESIZE_VERTICAL) { |
| set.playTogether(oa, topOa, bottomOa); |
| } else if (mResizeMode == AppWidgetProviderInfo.RESIZE_HORIZONTAL) { |
| set.playTogether(oa, leftOa, rightOa); |
| } else { |
| set.playTogether(oa, leftOa, rightOa, topOa, bottomOa); |
| } |
| |
| set.setDuration(SNAP_DURATION); |
| set.start(); |
| } |
| } |
| } |