summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher3/CellLayout.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/launcher3/CellLayout.java')
-rw-r--r--src/com/android/launcher3/CellLayout.java3338
1 files changed, 3338 insertions, 0 deletions
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
new file mode 100644
index 000000000..842037c31
--- /dev/null
+++ b/src/com/android/launcher3/CellLayout.java
@@ -0,0 +1,3338 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.NinePatchDrawable;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.LayoutAnimationController;
+
+import com.android.launcher3.R;
+import com.android.launcher3.FolderIcon.FolderRingAnimator;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Stack;
+
+public class CellLayout extends ViewGroup {
+ static final String TAG = "CellLayout";
+
+ private Launcher mLauncher;
+ private int mCellWidth;
+ private int mCellHeight;
+
+ private int mCountX;
+ private int mCountY;
+
+ private int mOriginalWidthGap;
+ private int mOriginalHeightGap;
+ private int mWidthGap;
+ private int mHeightGap;
+ private int mMaxGap;
+ private boolean mScrollingTransformsDirty = false;
+
+ private final Rect mRect = new Rect();
+ private final CellInfo mCellInfo = new CellInfo();
+
+ // These are temporary variables to prevent having to allocate a new object just to
+ // return an (x, y) value from helper functions. Do NOT use them to maintain other state.
+ private final int[] mTmpXY = new int[2];
+ private final int[] mTmpPoint = new int[2];
+ int[] mTempLocation = new int[2];
+
+ boolean[][] mOccupied;
+ boolean[][] mTmpOccupied;
+ private boolean mLastDownOnOccupiedCell = false;
+
+ private OnTouchListener mInterceptTouchListener;
+
+ private ArrayList<FolderRingAnimator> mFolderOuterRings = new ArrayList<FolderRingAnimator>();
+ private int[] mFolderLeaveBehindCell = {-1, -1};
+
+ private int mForegroundAlpha = 0;
+ private float mBackgroundAlpha;
+ private float mBackgroundAlphaMultiplier = 1.0f;
+
+ private Drawable mNormalBackground;
+ private Drawable mActiveGlowBackground;
+ private Drawable mOverScrollForegroundDrawable;
+ private Drawable mOverScrollLeft;
+ private Drawable mOverScrollRight;
+ private Rect mBackgroundRect;
+ private Rect mForegroundRect;
+ private int mForegroundPadding;
+
+ // If we're actively dragging something over this screen, mIsDragOverlapping is true
+ private boolean mIsDragOverlapping = false;
+ private final Point mDragCenter = new Point();
+
+ // These arrays are used to implement the drag visualization on x-large screens.
+ // They are used as circular arrays, indexed by mDragOutlineCurrent.
+ private Rect[] mDragOutlines = new Rect[4];
+ private float[] mDragOutlineAlphas = new float[mDragOutlines.length];
+ private InterruptibleInOutAnimator[] mDragOutlineAnims =
+ new InterruptibleInOutAnimator[mDragOutlines.length];
+
+ // Used as an index into the above 3 arrays; indicates which is the most current value.
+ private int mDragOutlineCurrent = 0;
+ private final Paint mDragOutlinePaint = new Paint();
+
+ private BubbleTextView mPressedOrFocusedIcon;
+
+ private HashMap<CellLayout.LayoutParams, Animator> mReorderAnimators = new
+ HashMap<CellLayout.LayoutParams, Animator>();
+ private HashMap<View, ReorderHintAnimation>
+ mShakeAnimators = new HashMap<View, ReorderHintAnimation>();
+
+ private boolean mItemPlacementDirty = false;
+
+ // When a drag operation is in progress, holds the nearest cell to the touch point
+ private final int[] mDragCell = new int[2];
+
+ private boolean mDragging = false;
+
+ private TimeInterpolator mEaseOutInterpolator;
+ private ShortcutAndWidgetContainer mShortcutsAndWidgets;
+
+ private boolean mIsHotseat = false;
+ private float mHotseatScale = 1f;
+
+ public static final int MODE_DRAG_OVER = 0;
+ public static final int MODE_ON_DROP = 1;
+ public static final int MODE_ON_DROP_EXTERNAL = 2;
+ public static final int MODE_ACCEPT_DROP = 3;
+ private static final boolean DESTRUCTIVE_REORDER = false;
+ private static final boolean DEBUG_VISUALIZE_OCCUPIED = false;
+
+ static final int LANDSCAPE = 0;
+ static final int PORTRAIT = 1;
+
+ private static final float REORDER_HINT_MAGNITUDE = 0.12f;
+ private static final int REORDER_ANIMATION_DURATION = 150;
+ private float mReorderHintAnimationMagnitude;
+
+ private ArrayList<View> mIntersectingViews = new ArrayList<View>();
+ private Rect mOccupiedRect = new Rect();
+ private int[] mDirectionVector = new int[2];
+ int[] mPreviousReorderDirection = new int[2];
+ private static final int INVALID_DIRECTION = -100;
+ private DropTarget.DragEnforcer mDragEnforcer;
+
+ private final static PorterDuffXfermode sAddBlendMode =
+ new PorterDuffXfermode(PorterDuff.Mode.ADD);
+ private final static Paint sPaint = new Paint();
+
+ public CellLayout(Context context) {
+ this(context, null);
+ }
+
+ public CellLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CellLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mDragEnforcer = new DropTarget.DragEnforcer(context);
+
+ // A ViewGroup usually does not draw, but CellLayout needs to draw a rectangle to show
+ // the user where a dragged item will land when dropped.
+ setWillNotDraw(false);
+ setClipToPadding(false);
+ mLauncher = (Launcher) context;
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CellLayout, defStyle, 0);
+
+ mCellWidth = a.getDimensionPixelSize(R.styleable.CellLayout_cellWidth, 10);
+ mCellHeight = a.getDimensionPixelSize(R.styleable.CellLayout_cellHeight, 10);
+ mWidthGap = mOriginalWidthGap = a.getDimensionPixelSize(R.styleable.CellLayout_widthGap, 0);
+ mHeightGap = mOriginalHeightGap = a.getDimensionPixelSize(R.styleable.CellLayout_heightGap, 0);
+ mMaxGap = a.getDimensionPixelSize(R.styleable.CellLayout_maxGap, 0);
+ mCountX = LauncherModel.getCellCountX();
+ mCountY = LauncherModel.getCellCountY();
+ mOccupied = new boolean[mCountX][mCountY];
+ mTmpOccupied = new boolean[mCountX][mCountY];
+ mPreviousReorderDirection[0] = INVALID_DIRECTION;
+ mPreviousReorderDirection[1] = INVALID_DIRECTION;
+
+ a.recycle();
+
+ setAlwaysDrawnWithCacheEnabled(false);
+
+ final Resources res = getResources();
+ mHotseatScale = (res.getInteger(R.integer.hotseat_item_scale_percentage) / 100f);
+
+ mNormalBackground = res.getDrawable(R.drawable.homescreen_blue_normal_holo);
+ mActiveGlowBackground = res.getDrawable(R.drawable.homescreen_blue_strong_holo);
+
+ mOverScrollLeft = res.getDrawable(R.drawable.overscroll_glow_left);
+ mOverScrollRight = res.getDrawable(R.drawable.overscroll_glow_right);
+ mForegroundPadding =
+ res.getDimensionPixelSize(R.dimen.workspace_overscroll_drawable_padding);
+
+ mReorderHintAnimationMagnitude = (REORDER_HINT_MAGNITUDE *
+ res.getDimensionPixelSize(R.dimen.app_icon_size));
+
+ mNormalBackground.setFilterBitmap(true);
+ mActiveGlowBackground.setFilterBitmap(true);
+
+ // Initialize the data structures used for the drag visualization.
+
+ mEaseOutInterpolator = new DecelerateInterpolator(2.5f); // Quint ease out
+
+
+ mDragCell[0] = mDragCell[1] = -1;
+ for (int i = 0; i < mDragOutlines.length; i++) {
+ mDragOutlines[i] = new Rect(-1, -1, -1, -1);
+ }
+
+ // When dragging things around the home screens, we show a green outline of
+ // where the item will land. The outlines gradually fade out, leaving a trail
+ // behind the drag path.
+ // Set up all the animations that are used to implement this fading.
+ final int duration = res.getInteger(R.integer.config_dragOutlineFadeTime);
+ final float fromAlphaValue = 0;
+ final float toAlphaValue = (float)res.getInteger(R.integer.config_dragOutlineMaxAlpha);
+
+ Arrays.fill(mDragOutlineAlphas, fromAlphaValue);
+
+ for (int i = 0; i < mDragOutlineAnims.length; i++) {
+ final InterruptibleInOutAnimator anim =
+ new InterruptibleInOutAnimator(this, duration, fromAlphaValue, toAlphaValue);
+ anim.getAnimator().setInterpolator(mEaseOutInterpolator);
+ final int thisIndex = i;
+ anim.getAnimator().addUpdateListener(new AnimatorUpdateListener() {
+ public void onAnimationUpdate(ValueAnimator animation) {
+ final Bitmap outline = (Bitmap)anim.getTag();
+
+ // If an animation is started and then stopped very quickly, we can still
+ // get spurious updates we've cleared the tag. Guard against this.
+ if (outline == null) {
+ @SuppressWarnings("all") // suppress dead code warning
+ final boolean debug = false;
+ if (debug) {
+ Object val = animation.getAnimatedValue();
+ Log.d(TAG, "anim " + thisIndex + " update: " + val +
+ ", isStopped " + anim.isStopped());
+ }
+ // Try to prevent it from continuing to run
+ animation.cancel();
+ } else {
+ mDragOutlineAlphas[thisIndex] = (Float) animation.getAnimatedValue();
+ CellLayout.this.invalidate(mDragOutlines[thisIndex]);
+ }
+ }
+ });
+ // The animation holds a reference to the drag outline bitmap as long is it's
+ // running. This way the bitmap can be GCed when the animations are complete.
+ anim.getAnimator().addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if ((Float) ((ValueAnimator) animation).getAnimatedValue() == 0f) {
+ anim.setTag(null);
+ }
+ }
+ });
+ mDragOutlineAnims[i] = anim;
+ }
+
+ mBackgroundRect = new Rect();
+ mForegroundRect = new Rect();
+
+ mShortcutsAndWidgets = new ShortcutAndWidgetContainer(context);
+ mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap,
+ mCountX);
+
+ addView(mShortcutsAndWidgets);
+ }
+
+ static int widthInPortrait(Resources r, int numCells) {
+ // We use this method from Workspace to figure out how many rows/columns Launcher should
+ // have. We ignore the left/right padding on CellLayout because it turns out in our design
+ // the padding extends outside the visible screen size, but it looked fine anyway.
+ int cellWidth = r.getDimensionPixelSize(R.dimen.workspace_cell_width);
+ int minGap = Math.min(r.getDimensionPixelSize(R.dimen.workspace_width_gap),
+ r.getDimensionPixelSize(R.dimen.workspace_height_gap));
+
+ return minGap * (numCells - 1) + cellWidth * numCells;
+ }
+
+ static int heightInLandscape(Resources r, int numCells) {
+ // We use this method from Workspace to figure out how many rows/columns Launcher should
+ // have. We ignore the left/right padding on CellLayout because it turns out in our design
+ // the padding extends outside the visible screen size, but it looked fine anyway.
+ int cellHeight = r.getDimensionPixelSize(R.dimen.workspace_cell_height);
+ int minGap = Math.min(r.getDimensionPixelSize(R.dimen.workspace_width_gap),
+ r.getDimensionPixelSize(R.dimen.workspace_height_gap));
+
+ return minGap * (numCells - 1) + cellHeight * numCells;
+ }
+
+ public void enableHardwareLayers() {
+ mShortcutsAndWidgets.setLayerType(LAYER_TYPE_HARDWARE, sPaint);
+ }
+
+ public void disableHardwareLayers() {
+ mShortcutsAndWidgets.setLayerType(LAYER_TYPE_NONE, sPaint);
+ }
+
+ public void buildHardwareLayer() {
+ mShortcutsAndWidgets.buildLayer();
+ }
+
+ public float getChildrenScale() {
+ return mIsHotseat ? mHotseatScale : 1.0f;
+ }
+
+ public void setGridSize(int x, int y) {
+ mCountX = x;
+ mCountY = y;
+ mOccupied = new boolean[mCountX][mCountY];
+ mTmpOccupied = new boolean[mCountX][mCountY];
+ mTempRectStack.clear();
+ mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap,
+ mCountX);
+ requestLayout();
+ }
+
+ // Set whether or not to invert the layout horizontally if the layout is in RTL mode.
+ public void setInvertIfRtl(boolean invert) {
+ mShortcutsAndWidgets.setInvertIfRtl(invert);
+ }
+
+ private void invalidateBubbleTextView(BubbleTextView icon) {
+ final int padding = icon.getPressedOrFocusedBackgroundPadding();
+ invalidate(icon.getLeft() + getPaddingLeft() - padding,
+ icon.getTop() + getPaddingTop() - padding,
+ icon.getRight() + getPaddingLeft() + padding,
+ icon.getBottom() + getPaddingTop() + padding);
+ }
+
+ void setOverScrollAmount(float r, boolean left) {
+ if (left && mOverScrollForegroundDrawable != mOverScrollLeft) {
+ mOverScrollForegroundDrawable = mOverScrollLeft;
+ } else if (!left && mOverScrollForegroundDrawable != mOverScrollRight) {
+ mOverScrollForegroundDrawable = mOverScrollRight;
+ }
+
+ mForegroundAlpha = (int) Math.round((r * 255));
+ mOverScrollForegroundDrawable.setAlpha(mForegroundAlpha);
+ invalidate();
+ }
+
+ void setPressedOrFocusedIcon(BubbleTextView icon) {
+ // We draw the pressed or focused BubbleTextView's background in CellLayout because it
+ // requires an expanded clip rect (due to the glow's blur radius)
+ BubbleTextView oldIcon = mPressedOrFocusedIcon;
+ mPressedOrFocusedIcon = icon;
+ if (oldIcon != null) {
+ invalidateBubbleTextView(oldIcon);
+ }
+ if (mPressedOrFocusedIcon != null) {
+ invalidateBubbleTextView(mPressedOrFocusedIcon);
+ }
+ }
+
+ void setIsDragOverlapping(boolean isDragOverlapping) {
+ if (mIsDragOverlapping != isDragOverlapping) {
+ mIsDragOverlapping = isDragOverlapping;
+ invalidate();
+ }
+ }
+
+ boolean getIsDragOverlapping() {
+ return mIsDragOverlapping;
+ }
+
+ protected void setOverscrollTransformsDirty(boolean dirty) {
+ mScrollingTransformsDirty = dirty;
+ }
+
+ protected void resetOverscrollTransforms() {
+ if (mScrollingTransformsDirty) {
+ setOverscrollTransformsDirty(false);
+ setTranslationX(0);
+ setRotationY(0);
+ // It doesn't matter if we pass true or false here, the important thing is that we
+ // pass 0, which results in the overscroll drawable not being drawn any more.
+ setOverScrollAmount(0, false);
+ setPivotX(getMeasuredWidth() / 2);
+ setPivotY(getMeasuredHeight() / 2);
+ }
+ }
+
+ public void scaleRect(Rect r, float scale) {
+ if (scale != 1.0f) {
+ r.left = (int) (r.left * scale + 0.5f);
+ r.top = (int) (r.top * scale + 0.5f);
+ r.right = (int) (r.right * scale + 0.5f);
+ r.bottom = (int) (r.bottom * scale + 0.5f);
+ }
+ }
+
+ Rect temp = new Rect();
+ void scaleRectAboutCenter(Rect in, Rect out, float scale) {
+ int cx = in.centerX();
+ int cy = in.centerY();
+ out.set(in);
+ out.offset(-cx, -cy);
+ scaleRect(out, scale);
+ out.offset(cx, cy);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ // When we're large, we are either drawn in a "hover" state (ie when dragging an item to
+ // a neighboring page) or with just a normal background (if backgroundAlpha > 0.0f)
+ // When we're small, we are either drawn normally or in the "accepts drops" state (during
+ // a drag). However, we also drag the mini hover background *over* one of those two
+ // backgrounds
+ if (mBackgroundAlpha > 0.0f) {
+ Drawable bg;
+
+ if (mIsDragOverlapping) {
+ // In the mini case, we draw the active_glow bg *over* the active background
+ bg = mActiveGlowBackground;
+ } else {
+ bg = mNormalBackground;
+ }
+
+ bg.setAlpha((int) (mBackgroundAlpha * mBackgroundAlphaMultiplier * 255));
+ bg.setBounds(mBackgroundRect);
+ bg.draw(canvas);
+ }
+
+ final Paint paint = mDragOutlinePaint;
+ for (int i = 0; i < mDragOutlines.length; i++) {
+ final float alpha = mDragOutlineAlphas[i];
+ if (alpha > 0) {
+ final Rect r = mDragOutlines[i];
+ scaleRectAboutCenter(r, temp, getChildrenScale());
+ final Bitmap b = (Bitmap) mDragOutlineAnims[i].getTag();
+ paint.setAlpha((int)(alpha + .5f));
+ canvas.drawBitmap(b, null, temp, paint);
+ }
+ }
+
+ // We draw the pressed or focused BubbleTextView's background in CellLayout because it
+ // requires an expanded clip rect (due to the glow's blur radius)
+ if (mPressedOrFocusedIcon != null) {
+ final int padding = mPressedOrFocusedIcon.getPressedOrFocusedBackgroundPadding();
+ final Bitmap b = mPressedOrFocusedIcon.getPressedOrFocusedBackground();
+ if (b != null) {
+ canvas.drawBitmap(b,
+ mPressedOrFocusedIcon.getLeft() + getPaddingLeft() - padding,
+ mPressedOrFocusedIcon.getTop() + getPaddingTop() - padding,
+ null);
+ }
+ }
+
+ if (DEBUG_VISUALIZE_OCCUPIED) {
+ int[] pt = new int[2];
+ ColorDrawable cd = new ColorDrawable(Color.RED);
+ cd.setBounds(0, 0, mCellWidth, mCellHeight);
+ for (int i = 0; i < mCountX; i++) {
+ for (int j = 0; j < mCountY; j++) {
+ if (mOccupied[i][j]) {
+ cellToPoint(i, j, pt);
+ canvas.save();
+ canvas.translate(pt[0], pt[1]);
+ cd.draw(canvas);
+ canvas.restore();
+ }
+ }
+ }
+ }
+
+ int previewOffset = FolderRingAnimator.sPreviewSize;
+
+ // The folder outer / inner ring image(s)
+ for (int i = 0; i < mFolderOuterRings.size(); i++) {
+ FolderRingAnimator fra = mFolderOuterRings.get(i);
+
+ // Draw outer ring
+ Drawable d = FolderRingAnimator.sSharedOuterRingDrawable;
+ int width = (int) fra.getOuterRingSize();
+ int height = width;
+ cellToPoint(fra.mCellX, fra.mCellY, mTempLocation);
+
+ int centerX = mTempLocation[0] + mCellWidth / 2;
+ int centerY = mTempLocation[1] + previewOffset / 2;
+
+ canvas.save();
+ canvas.translate(centerX - width / 2, centerY - height / 2);
+ d.setBounds(0, 0, width, height);
+ d.draw(canvas);
+ canvas.restore();
+
+ // Draw inner ring
+ d = FolderRingAnimator.sSharedInnerRingDrawable;
+ width = (int) fra.getInnerRingSize();
+ height = width;
+ cellToPoint(fra.mCellX, fra.mCellY, mTempLocation);
+
+ centerX = mTempLocation[0] + mCellWidth / 2;
+ centerY = mTempLocation[1] + previewOffset / 2;
+ canvas.save();
+ canvas.translate(centerX - width / 2, centerY - width / 2);
+ d.setBounds(0, 0, width, height);
+ d.draw(canvas);
+ canvas.restore();
+ }
+
+ if (mFolderLeaveBehindCell[0] >= 0 && mFolderLeaveBehindCell[1] >= 0) {
+ Drawable d = FolderIcon.sSharedFolderLeaveBehind;
+ int width = d.getIntrinsicWidth();
+ int height = d.getIntrinsicHeight();
+
+ cellToPoint(mFolderLeaveBehindCell[0], mFolderLeaveBehindCell[1], mTempLocation);
+ int centerX = mTempLocation[0] + mCellWidth / 2;
+ int centerY = mTempLocation[1] + previewOffset / 2;
+
+ canvas.save();
+ canvas.translate(centerX - width / 2, centerY - width / 2);
+ d.setBounds(0, 0, width, height);
+ d.draw(canvas);
+ canvas.restore();
+ }
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ super.dispatchDraw(canvas);
+ if (mForegroundAlpha > 0) {
+ mOverScrollForegroundDrawable.setBounds(mForegroundRect);
+ Paint p = ((NinePatchDrawable) mOverScrollForegroundDrawable).getPaint();
+ p.setXfermode(sAddBlendMode);
+ mOverScrollForegroundDrawable.draw(canvas);
+ p.setXfermode(null);
+ }
+ }
+
+ public void showFolderAccept(FolderRingAnimator fra) {
+ mFolderOuterRings.add(fra);
+ }
+
+ public void hideFolderAccept(FolderRingAnimator fra) {
+ if (mFolderOuterRings.contains(fra)) {
+ mFolderOuterRings.remove(fra);
+ }
+ invalidate();
+ }
+
+ public void setFolderLeaveBehindCell(int x, int y) {
+ mFolderLeaveBehindCell[0] = x;
+ mFolderLeaveBehindCell[1] = y;
+ invalidate();
+ }
+
+ public void clearFolderLeaveBehind() {
+ mFolderLeaveBehindCell[0] = -1;
+ mFolderLeaveBehindCell[1] = -1;
+ invalidate();
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return false;
+ }
+
+ public void restoreInstanceState(SparseArray<Parcelable> states) {
+ dispatchRestoreInstanceState(states);
+ }
+
+ @Override
+ public void cancelLongPress() {
+ super.cancelLongPress();
+
+ // Cancel long press for all children
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ child.cancelLongPress();
+ }
+ }
+
+ public void setOnInterceptTouchListener(View.OnTouchListener listener) {
+ mInterceptTouchListener = listener;
+ }
+
+ int getCountX() {
+ return mCountX;
+ }
+
+ int getCountY() {
+ return mCountY;
+ }
+
+ public void setIsHotseat(boolean isHotseat) {
+ mIsHotseat = isHotseat;
+ }
+
+ public boolean addViewToCellLayout(View child, int index, int childId, LayoutParams params,
+ boolean markCells) {
+ final LayoutParams lp = params;
+
+ // Hotseat icons - remove text
+ if (child instanceof BubbleTextView) {
+ BubbleTextView bubbleChild = (BubbleTextView) child;
+
+ Resources res = getResources();
+ if (mIsHotseat) {
+ bubbleChild.setTextColor(res.getColor(android.R.color.transparent));
+ } else {
+ bubbleChild.setTextColor(res.getColor(R.color.workspace_icon_text_color));
+ }
+ }
+
+ child.setScaleX(getChildrenScale());
+ child.setScaleY(getChildrenScale());
+
+ // Generate an id for each view, this assumes we have at most 256x256 cells
+ // per workspace screen
+ if (lp.cellX >= 0 && lp.cellX <= mCountX - 1 && lp.cellY >= 0 && lp.cellY <= mCountY - 1) {
+ // If the horizontal or vertical span is set to -1, it is taken to
+ // mean that it spans the extent of the CellLayout
+ if (lp.cellHSpan < 0) lp.cellHSpan = mCountX;
+ if (lp.cellVSpan < 0) lp.cellVSpan = mCountY;
+
+ child.setId(childId);
+
+ mShortcutsAndWidgets.addView(child, index, lp);
+
+ if (markCells) markCellsAsOccupiedForView(child);
+
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void removeAllViews() {
+ clearOccupiedCells();
+ mShortcutsAndWidgets.removeAllViews();
+ }
+
+ @Override
+ public void removeAllViewsInLayout() {
+ if (mShortcutsAndWidgets.getChildCount() > 0) {
+ clearOccupiedCells();
+ mShortcutsAndWidgets.removeAllViewsInLayout();
+ }
+ }
+
+ public void removeViewWithoutMarkingCells(View view) {
+ mShortcutsAndWidgets.removeView(view);
+ }
+
+ @Override
+ public void removeView(View view) {
+ markCellsAsUnoccupiedForView(view);
+ mShortcutsAndWidgets.removeView(view);
+ }
+
+ @Override
+ public void removeViewAt(int index) {
+ markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(index));
+ mShortcutsAndWidgets.removeViewAt(index);
+ }
+
+ @Override
+ public void removeViewInLayout(View view) {
+ markCellsAsUnoccupiedForView(view);
+ mShortcutsAndWidgets.removeViewInLayout(view);
+ }
+
+ @Override
+ public void removeViews(int start, int count) {
+ for (int i = start; i < start + count; i++) {
+ markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(i));
+ }
+ mShortcutsAndWidgets.removeViews(start, count);
+ }
+
+ @Override
+ public void removeViewsInLayout(int start, int count) {
+ for (int i = start; i < start + count; i++) {
+ markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(i));
+ }
+ mShortcutsAndWidgets.removeViewsInLayout(start, count);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mCellInfo.screen = ((ViewGroup) getParent()).indexOfChild(this);
+ }
+
+ public void setTagToCellInfoForPoint(int touchX, int touchY) {
+ final CellInfo cellInfo = mCellInfo;
+ Rect frame = mRect;
+ final int x = touchX + getScrollX();
+ final int y = touchY + getScrollY();
+ final int count = mShortcutsAndWidgets.getChildCount();
+
+ boolean found = false;
+ for (int i = count - 1; i >= 0; i--) {
+ final View child = mShortcutsAndWidgets.getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ if ((child.getVisibility() == VISIBLE || child.getAnimation() != null) &&
+ lp.isLockedToGrid) {
+ child.getHitRect(frame);
+
+ float scale = child.getScaleX();
+ frame = new Rect(child.getLeft(), child.getTop(), child.getRight(),
+ child.getBottom());
+ // The child hit rect is relative to the CellLayoutChildren parent, so we need to
+ // offset that by this CellLayout's padding to test an (x,y) point that is relative
+ // to this view.
+ frame.offset(getPaddingLeft(), getPaddingTop());
+ frame.inset((int) (frame.width() * (1f - scale) / 2),
+ (int) (frame.height() * (1f - scale) / 2));
+
+ if (frame.contains(x, y)) {
+ cellInfo.cell = child;
+ cellInfo.cellX = lp.cellX;
+ cellInfo.cellY = lp.cellY;
+ cellInfo.spanX = lp.cellHSpan;
+ cellInfo.spanY = lp.cellVSpan;
+ found = true;
+ break;
+ }
+ }
+ }
+
+ mLastDownOnOccupiedCell = found;
+
+ if (!found) {
+ final int cellXY[] = mTmpXY;
+ pointToCellExact(x, y, cellXY);
+
+ cellInfo.cell = null;
+ cellInfo.cellX = cellXY[0];
+ cellInfo.cellY = cellXY[1];
+ cellInfo.spanX = 1;
+ cellInfo.spanY = 1;
+ }
+ setTag(cellInfo);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ // First we clear the tag to ensure that on every touch down we start with a fresh slate,
+ // even in the case where we return early. Not clearing here was causing bugs whereby on
+ // long-press we'd end up picking up an item from a previous drag operation.
+ final int action = ev.getAction();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ clearTagCellInfo();
+ }
+
+ if (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev)) {
+ return true;
+ }
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ setTagToCellInfoForPoint((int) ev.getX(), (int) ev.getY());
+ }
+
+ return false;
+ }
+
+ private void clearTagCellInfo() {
+ final CellInfo cellInfo = mCellInfo;
+ cellInfo.cell = null;
+ cellInfo.cellX = -1;
+ cellInfo.cellY = -1;
+ cellInfo.spanX = 0;
+ cellInfo.spanY = 0;
+ setTag(cellInfo);
+ }
+
+ public CellInfo getTag() {
+ return (CellInfo) super.getTag();
+ }
+
+ /**
+ * Given a point, return the cell that strictly encloses that point
+ * @param x X coordinate of the point
+ * @param y Y coordinate of the point
+ * @param result Array of 2 ints to hold the x and y coordinate of the cell
+ */
+ void pointToCellExact(int x, int y, int[] result) {
+ final int hStartPadding = getPaddingLeft();
+ final int vStartPadding = getPaddingTop();
+
+ result[0] = (x - hStartPadding) / (mCellWidth + mWidthGap);
+ result[1] = (y - vStartPadding) / (mCellHeight + mHeightGap);
+
+ final int xAxis = mCountX;
+ final int yAxis = mCountY;
+
+ if (result[0] < 0) result[0] = 0;
+ if (result[0] >= xAxis) result[0] = xAxis - 1;
+ if (result[1] < 0) result[1] = 0;
+ if (result[1] >= yAxis) result[1] = yAxis - 1;
+ }
+
+ /**
+ * Given a point, return the cell that most closely encloses that point
+ * @param x X coordinate of the point
+ * @param y Y coordinate of the point
+ * @param result Array of 2 ints to hold the x and y coordinate of the cell
+ */
+ void pointToCellRounded(int x, int y, int[] result) {
+ pointToCellExact(x + (mCellWidth / 2), y + (mCellHeight / 2), result);
+ }
+
+ /**
+ * Given a cell coordinate, return the point that represents the upper left corner of that cell
+ *
+ * @param cellX X coordinate of the cell
+ * @param cellY Y coordinate of the cell
+ *
+ * @param result Array of 2 ints to hold the x and y coordinate of the point
+ */
+ void cellToPoint(int cellX, int cellY, int[] result) {
+ final int hStartPadding = getPaddingLeft();
+ final int vStartPadding = getPaddingTop();
+
+ result[0] = hStartPadding + cellX * (mCellWidth + mWidthGap);
+ result[1] = vStartPadding + cellY * (mCellHeight + mHeightGap);
+ }
+
+ /**
+ * Given a cell coordinate, return the point that represents the center of the cell
+ *
+ * @param cellX X coordinate of the cell
+ * @param cellY Y coordinate of the cell
+ *
+ * @param result Array of 2 ints to hold the x and y coordinate of the point
+ */
+ void cellToCenterPoint(int cellX, int cellY, int[] result) {
+ regionToCenterPoint(cellX, cellY, 1, 1, result);
+ }
+
+ /**
+ * Given a cell coordinate and span return the point that represents the center of the regio
+ *
+ * @param cellX X coordinate of the cell
+ * @param cellY Y coordinate of the cell
+ *
+ * @param result Array of 2 ints to hold the x and y coordinate of the point
+ */
+ void regionToCenterPoint(int cellX, int cellY, int spanX, int spanY, int[] result) {
+ final int hStartPadding = getPaddingLeft();
+ final int vStartPadding = getPaddingTop();
+ result[0] = hStartPadding + cellX * (mCellWidth + mWidthGap) +
+ (spanX * mCellWidth + (spanX - 1) * mWidthGap) / 2;
+ result[1] = vStartPadding + cellY * (mCellHeight + mHeightGap) +
+ (spanY * mCellHeight + (spanY - 1) * mHeightGap) / 2;
+ }
+
+ /**
+ * Given a cell coordinate and span fills out a corresponding pixel rect
+ *
+ * @param cellX X coordinate of the cell
+ * @param cellY Y coordinate of the cell
+ * @param result Rect in which to write the result
+ */
+ void regionToRect(int cellX, int cellY, int spanX, int spanY, Rect result) {
+ final int hStartPadding = getPaddingLeft();
+ final int vStartPadding = getPaddingTop();
+ final int left = hStartPadding + cellX * (mCellWidth + mWidthGap);
+ final int top = vStartPadding + cellY * (mCellHeight + mHeightGap);
+ result.set(left, top, left + (spanX * mCellWidth + (spanX - 1) * mWidthGap),
+ top + (spanY * mCellHeight + (spanY - 1) * mHeightGap));
+ }
+
+ public float getDistanceFromCell(float x, float y, int[] cell) {
+ cellToCenterPoint(cell[0], cell[1], mTmpPoint);
+ float distance = (float) Math.sqrt( Math.pow(x - mTmpPoint[0], 2) +
+ Math.pow(y - mTmpPoint[1], 2));
+ return distance;
+ }
+
+ int getCellWidth() {
+ return mCellWidth;
+ }
+
+ int getCellHeight() {
+ return mCellHeight;
+ }
+
+ int getWidthGap() {
+ return mWidthGap;
+ }
+
+ int getHeightGap() {
+ return mHeightGap;
+ }
+
+ Rect getContentRect(Rect r) {
+ if (r == null) {
+ r = new Rect();
+ }
+ int left = getPaddingLeft();
+ int top = getPaddingTop();
+ int right = left + getWidth() - getPaddingLeft() - getPaddingRight();
+ int bottom = top + getHeight() - getPaddingTop() - getPaddingBottom();
+ r.set(left, top, right, bottom);
+ return r;
+ }
+
+ static void getMetrics(Rect metrics, Resources res, int measureWidth, int measureHeight,
+ int countX, int countY, int orientation) {
+ int numWidthGaps = countX - 1;
+ int numHeightGaps = countY - 1;
+
+ int widthGap;
+ int heightGap;
+ int cellWidth;
+ int cellHeight;
+ int paddingLeft;
+ int paddingRight;
+ int paddingTop;
+ int paddingBottom;
+
+ int maxGap = res.getDimensionPixelSize(R.dimen.workspace_max_gap);
+ if (orientation == LANDSCAPE) {
+ cellWidth = res.getDimensionPixelSize(R.dimen.workspace_cell_width_land);
+ cellHeight = res.getDimensionPixelSize(R.dimen.workspace_cell_height_land);
+ widthGap = res.getDimensionPixelSize(R.dimen.workspace_width_gap_land);
+ heightGap = res.getDimensionPixelSize(R.dimen.workspace_height_gap_land);
+ paddingLeft = res.getDimensionPixelSize(R.dimen.cell_layout_left_padding_land);
+ paddingRight = res.getDimensionPixelSize(R.dimen.cell_layout_right_padding_land);
+ paddingTop = res.getDimensionPixelSize(R.dimen.cell_layout_top_padding_land);
+ paddingBottom = res.getDimensionPixelSize(R.dimen.cell_layout_bottom_padding_land);
+ } else {
+ // PORTRAIT
+ cellWidth = res.getDimensionPixelSize(R.dimen.workspace_cell_width_port);
+ cellHeight = res.getDimensionPixelSize(R.dimen.workspace_cell_height_port);
+ widthGap = res.getDimensionPixelSize(R.dimen.workspace_width_gap_port);
+ heightGap = res.getDimensionPixelSize(R.dimen.workspace_height_gap_port);
+ paddingLeft = res.getDimensionPixelSize(R.dimen.cell_layout_left_padding_port);
+ paddingRight = res.getDimensionPixelSize(R.dimen.cell_layout_right_padding_port);
+ paddingTop = res.getDimensionPixelSize(R.dimen.cell_layout_top_padding_port);
+ paddingBottom = res.getDimensionPixelSize(R.dimen.cell_layout_bottom_padding_port);
+ }
+
+ if (widthGap < 0 || heightGap < 0) {
+ int hSpace = measureWidth - paddingLeft - paddingRight;
+ int vSpace = measureHeight - paddingTop - paddingBottom;
+ int hFreeSpace = hSpace - (countX * cellWidth);
+ int vFreeSpace = vSpace - (countY * cellHeight);
+ widthGap = Math.min(maxGap, numWidthGaps > 0 ? (hFreeSpace / numWidthGaps) : 0);
+ heightGap = Math.min(maxGap, numHeightGaps > 0 ? (vFreeSpace / numHeightGaps) : 0);
+ }
+ metrics.set(cellWidth, cellHeight, widthGap, heightGap);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+
+ int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+ int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
+ throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions");
+ }
+
+ int numWidthGaps = mCountX - 1;
+ int numHeightGaps = mCountY - 1;
+
+ if (mOriginalWidthGap < 0 || mOriginalHeightGap < 0) {
+ int hSpace = widthSpecSize - getPaddingLeft() - getPaddingRight();
+ int vSpace = heightSpecSize - getPaddingTop() - getPaddingBottom();
+ int hFreeSpace = hSpace - (mCountX * mCellWidth);
+ int vFreeSpace = vSpace - (mCountY * mCellHeight);
+ mWidthGap = Math.min(mMaxGap, numWidthGaps > 0 ? (hFreeSpace / numWidthGaps) : 0);
+ mHeightGap = Math.min(mMaxGap,numHeightGaps > 0 ? (vFreeSpace / numHeightGaps) : 0);
+ mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap,
+ mCountX);
+ } else {
+ mWidthGap = mOriginalWidthGap;
+ mHeightGap = mOriginalHeightGap;
+ }
+
+ // Initial values correspond to widthSpecMode == MeasureSpec.EXACTLY
+ int newWidth = widthSpecSize;
+ int newHeight = heightSpecSize;
+ if (widthSpecMode == MeasureSpec.AT_MOST) {
+ newWidth = getPaddingLeft() + getPaddingRight() + (mCountX * mCellWidth) +
+ ((mCountX - 1) * mWidthGap);
+ newHeight = getPaddingTop() + getPaddingBottom() + (mCountY * mCellHeight) +
+ ((mCountY - 1) * mHeightGap);
+ setMeasuredDimension(newWidth, newHeight);
+ }
+
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(newWidth - getPaddingLeft() -
+ getPaddingRight(), MeasureSpec.EXACTLY);
+ int childheightMeasureSpec = MeasureSpec.makeMeasureSpec(newHeight - getPaddingTop() -
+ getPaddingBottom(), MeasureSpec.EXACTLY);
+ child.measure(childWidthMeasureSpec, childheightMeasureSpec);
+ }
+ setMeasuredDimension(newWidth, newHeight);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ child.layout(getPaddingLeft(), getPaddingTop(),
+ r - l - getPaddingRight(), b - t - getPaddingBottom());
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ mBackgroundRect.set(0, 0, w, h);
+ mForegroundRect.set(mForegroundPadding, mForegroundPadding,
+ w - mForegroundPadding, h - mForegroundPadding);
+ }
+
+ @Override
+ protected void setChildrenDrawingCacheEnabled(boolean enabled) {
+ mShortcutsAndWidgets.setChildrenDrawingCacheEnabled(enabled);
+ }
+
+ @Override
+ protected void setChildrenDrawnWithCacheEnabled(boolean enabled) {
+ mShortcutsAndWidgets.setChildrenDrawnWithCacheEnabled(enabled);
+ }
+
+ public float getBackgroundAlpha() {
+ return mBackgroundAlpha;
+ }
+
+ public void setBackgroundAlphaMultiplier(float multiplier) {
+ if (mBackgroundAlphaMultiplier != multiplier) {
+ mBackgroundAlphaMultiplier = multiplier;
+ invalidate();
+ }
+ }
+
+ public float getBackgroundAlphaMultiplier() {
+ return mBackgroundAlphaMultiplier;
+ }
+
+ public void setBackgroundAlpha(float alpha) {
+ if (mBackgroundAlpha != alpha) {
+ mBackgroundAlpha = alpha;
+ invalidate();
+ }
+ }
+
+ public void setShortcutAndWidgetAlpha(float alpha) {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ getChildAt(i).setAlpha(alpha);
+ }
+ }
+
+ public ShortcutAndWidgetContainer getShortcutsAndWidgets() {
+ if (getChildCount() > 0) {
+ return (ShortcutAndWidgetContainer) getChildAt(0);
+ }
+ return null;
+ }
+
+ public View getChildAt(int x, int y) {
+ return mShortcutsAndWidgets.getChildAt(x, y);
+ }
+
+ public boolean animateChildToPosition(final View child, int cellX, int cellY, int duration,
+ int delay, boolean permanent, boolean adjustOccupied) {
+ ShortcutAndWidgetContainer clc = getShortcutsAndWidgets();
+ boolean[][] occupied = mOccupied;
+ if (!permanent) {
+ occupied = mTmpOccupied;
+ }
+
+ if (clc.indexOfChild(child) != -1) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final ItemInfo info = (ItemInfo) child.getTag();
+
+ // We cancel any existing animations
+ if (mReorderAnimators.containsKey(lp)) {
+ mReorderAnimators.get(lp).cancel();
+ mReorderAnimators.remove(lp);
+ }
+
+ final int oldX = lp.x;
+ final int oldY = lp.y;
+ if (adjustOccupied) {
+ occupied[lp.cellX][lp.cellY] = false;
+ occupied[cellX][cellY] = true;
+ }
+ lp.isLockedToGrid = true;
+ if (permanent) {
+ lp.cellX = info.cellX = cellX;
+ lp.cellY = info.cellY = cellY;
+ } else {
+ lp.tmpCellX = cellX;
+ lp.tmpCellY = cellY;
+ }
+ clc.setupLp(lp);
+ lp.isLockedToGrid = false;
+ final int newX = lp.x;
+ final int newY = lp.y;
+
+ lp.x = oldX;
+ lp.y = oldY;
+
+ // Exit early if we're not actually moving the view
+ if (oldX == newX && oldY == newY) {
+ lp.isLockedToGrid = true;
+ return true;
+ }
+
+ ValueAnimator va = LauncherAnimUtils.ofFloat(child, 0f, 1f);
+ va.setDuration(duration);
+ mReorderAnimators.put(lp, va);
+
+ va.addUpdateListener(new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ float r = ((Float) animation.getAnimatedValue()).floatValue();
+ lp.x = (int) ((1 - r) * oldX + r * newX);
+ lp.y = (int) ((1 - r) * oldY + r * newY);
+ child.requestLayout();
+ }
+ });
+ va.addListener(new AnimatorListenerAdapter() {
+ boolean cancelled = false;
+ public void onAnimationEnd(Animator animation) {
+ // If the animation was cancelled, it means that another animation
+ // has interrupted this one, and we don't want to lock the item into
+ // place just yet.
+ if (!cancelled) {
+ lp.isLockedToGrid = true;
+ child.requestLayout();
+ }
+ if (mReorderAnimators.containsKey(lp)) {
+ mReorderAnimators.remove(lp);
+ }
+ }
+ public void onAnimationCancel(Animator animation) {
+ cancelled = true;
+ }
+ });
+ va.setStartDelay(delay);
+ va.start();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Estimate where the top left cell of the dragged item will land if it is dropped.
+ *
+ * @param originX The X value of the top left corner of the item
+ * @param originY The Y value of the top left corner of the item
+ * @param spanX The number of horizontal cells that the item spans
+ * @param spanY The number of vertical cells that the item spans
+ * @param result The estimated drop cell X and Y.
+ */
+ void estimateDropCell(int originX, int originY, int spanX, int spanY, int[] result) {
+ final int countX = mCountX;
+ final int countY = mCountY;
+
+ // pointToCellRounded takes the top left of a cell but will pad that with
+ // cellWidth/2 and cellHeight/2 when finding the matching cell
+ pointToCellRounded(originX, originY, result);
+
+ // If the item isn't fully on this screen, snap to the edges
+ int rightOverhang = result[0] + spanX - countX;
+ if (rightOverhang > 0) {
+ result[0] -= rightOverhang; // Snap to right
+ }
+ result[0] = Math.max(0, result[0]); // Snap to left
+ int bottomOverhang = result[1] + spanY - countY;
+ if (bottomOverhang > 0) {
+ result[1] -= bottomOverhang; // Snap to bottom
+ }
+ result[1] = Math.max(0, result[1]); // Snap to top
+ }
+
+ void visualizeDropLocation(View v, Bitmap dragOutline, int originX, int originY, int cellX,
+ int cellY, int spanX, int spanY, boolean resize, Point dragOffset, Rect dragRegion) {
+ final int oldDragCellX = mDragCell[0];
+ final int oldDragCellY = mDragCell[1];
+
+ if (v != null && dragOffset == null) {
+ mDragCenter.set(originX + (v.getWidth() / 2), originY + (v.getHeight() / 2));
+ } else {
+ mDragCenter.set(originX, originY);
+ }
+
+ if (dragOutline == null && v == null) {
+ return;
+ }
+
+ if (cellX != oldDragCellX || cellY != oldDragCellY) {
+ mDragCell[0] = cellX;
+ mDragCell[1] = cellY;
+ // Find the top left corner of the rect the object will occupy
+ final int[] topLeft = mTmpPoint;
+ cellToPoint(cellX, cellY, topLeft);
+
+ int left = topLeft[0];
+ int top = topLeft[1];
+
+ if (v != null && dragOffset == null) {
+ // When drawing the drag outline, it did not account for margin offsets
+ // added by the view's parent.
+ MarginLayoutParams lp = (MarginLayoutParams) v.getLayoutParams();
+ left += lp.leftMargin;
+ top += lp.topMargin;
+
+ // Offsets due to the size difference between the View and the dragOutline.
+ // There is a size difference to account for the outer blur, which may lie
+ // outside the bounds of the view.
+ top += (v.getHeight() - dragOutline.getHeight()) / 2;
+ // We center about the x axis
+ left += ((mCellWidth * spanX) + ((spanX - 1) * mWidthGap)
+ - dragOutline.getWidth()) / 2;
+ } else {
+ if (dragOffset != null && dragRegion != null) {
+ // Center the drag region *horizontally* in the cell and apply a drag
+ // outline offset
+ left += dragOffset.x + ((mCellWidth * spanX) + ((spanX - 1) * mWidthGap)
+ - dragRegion.width()) / 2;
+ top += dragOffset.y;
+ } else {
+ // Center the drag outline in the cell
+ left += ((mCellWidth * spanX) + ((spanX - 1) * mWidthGap)
+ - dragOutline.getWidth()) / 2;
+ top += ((mCellHeight * spanY) + ((spanY - 1) * mHeightGap)
+ - dragOutline.getHeight()) / 2;
+ }
+ }
+ final int oldIndex = mDragOutlineCurrent;
+ mDragOutlineAnims[oldIndex].animateOut();
+ mDragOutlineCurrent = (oldIndex + 1) % mDragOutlines.length;
+ Rect r = mDragOutlines[mDragOutlineCurrent];
+ r.set(left, top, left + dragOutline.getWidth(), top + dragOutline.getHeight());
+ if (resize) {
+ cellToRect(cellX, cellY, spanX, spanY, r);
+ }
+
+ mDragOutlineAnims[mDragOutlineCurrent].setTag(dragOutline);
+ mDragOutlineAnims[mDragOutlineCurrent].animateIn();
+ }
+ }
+
+ public void clearDragOutlines() {
+ final int oldIndex = mDragOutlineCurrent;
+ mDragOutlineAnims[oldIndex].animateOut();
+ mDragCell[0] = mDragCell[1] = -1;
+ }
+
+ /**
+ * Find a vacant area that will fit the given bounds nearest the requested
+ * cell location. Uses Euclidean distance to score multiple vacant areas.
+ *
+ * @param pixelX The X location at which you want to search for a vacant area.
+ * @param pixelY The Y location at which you want to search for a vacant area.
+ * @param spanX Horizontal span of the object.
+ * @param spanY Vertical span of the object.
+ * @param result Array in which to place the result, or null (in which case a new array will
+ * be allocated)
+ * @return The X, Y cell of a vacant area that can contain this object,
+ * nearest the requested location.
+ */
+ int[] findNearestVacantArea(int pixelX, int pixelY, int spanX, int spanY,
+ int[] result) {
+ return findNearestVacantArea(pixelX, pixelY, spanX, spanY, null, result);
+ }
+
+ /**
+ * Find a vacant area that will fit the given bounds nearest the requested
+ * cell location. Uses Euclidean distance to score multiple vacant areas.
+ *
+ * @param pixelX The X location at which you want to search for a vacant area.
+ * @param pixelY The Y location at which you want to search for a vacant area.
+ * @param minSpanX The minimum horizontal span required
+ * @param minSpanY The minimum vertical span required
+ * @param spanX Horizontal span of the object.
+ * @param spanY Vertical span of the object.
+ * @param result Array in which to place the result, or null (in which case a new array will
+ * be allocated)
+ * @return The X, Y cell of a vacant area that can contain this object,
+ * nearest the requested location.
+ */
+ int[] findNearestVacantArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX,
+ int spanY, int[] result, int[] resultSpan) {
+ return findNearestVacantArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, null,
+ result, resultSpan);
+ }
+
+ /**
+ * Find a vacant area that will fit the given bounds nearest the requested
+ * cell location. Uses Euclidean distance to score multiple vacant areas.
+ *
+ * @param pixelX The X location at which you want to search for a vacant area.
+ * @param pixelY The Y location at which you want to search for a vacant area.
+ * @param spanX Horizontal span of the object.
+ * @param spanY Vertical span of the object.
+ * @param ignoreOccupied If true, the result can be an occupied cell
+ * @param result Array in which to place the result, or null (in which case a new array will
+ * be allocated)
+ * @return The X, Y cell of a vacant area that can contain this object,
+ * nearest the requested location.
+ */
+ int[] findNearestArea(int pixelX, int pixelY, int spanX, int spanY, View ignoreView,
+ boolean ignoreOccupied, int[] result) {
+ return findNearestArea(pixelX, pixelY, spanX, spanY,
+ spanX, spanY, ignoreView, ignoreOccupied, result, null, mOccupied);
+ }
+
+ private final Stack<Rect> mTempRectStack = new Stack<Rect>();
+ private void lazyInitTempRectStack() {
+ if (mTempRectStack.isEmpty()) {
+ for (int i = 0; i < mCountX * mCountY; i++) {
+ mTempRectStack.push(new Rect());
+ }
+ }
+ }
+
+ private void recycleTempRects(Stack<Rect> used) {
+ while (!used.isEmpty()) {
+ mTempRectStack.push(used.pop());
+ }
+ }
+
+ /**
+ * Find a vacant area that will fit the given bounds nearest the requested
+ * cell location. Uses Euclidean distance to score multiple vacant areas.
+ *
+ * @param pixelX The X location at which you want to search for a vacant area.
+ * @param pixelY The Y location at which you want to search for a vacant area.
+ * @param minSpanX The minimum horizontal span required
+ * @param minSpanY The minimum vertical span required
+ * @param spanX Horizontal span of the object.
+ * @param spanY Vertical span of the object.
+ * @param ignoreOccupied If true, the result can be an occupied cell
+ * @param result Array in which to place the result, or null (in which case a new array will
+ * be allocated)
+ * @return The X, Y cell of a vacant area that can contain this object,
+ * nearest the requested location.
+ */
+ int[] findNearestArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY,
+ View ignoreView, boolean ignoreOccupied, int[] result, int[] resultSpan,
+ boolean[][] occupied) {
+ lazyInitTempRectStack();
+ // mark space take by ignoreView as available (method checks if ignoreView is null)
+ markCellsAsUnoccupiedForView(ignoreView, occupied);
+
+ // For items with a spanX / spanY > 1, the passed in point (pixelX, pixelY) corresponds
+ // to the center of the item, but we are searching based on the top-left cell, so
+ // we translate the point over to correspond to the top-left.
+ pixelX -= (mCellWidth + mWidthGap) * (spanX - 1) / 2f;
+ pixelY -= (mCellHeight + mHeightGap) * (spanY - 1) / 2f;
+
+ // Keep track of best-scoring drop area
+ final int[] bestXY = result != null ? result : new int[2];
+ double bestDistance = Double.MAX_VALUE;
+ final Rect bestRect = new Rect(-1, -1, -1, -1);
+ final Stack<Rect> validRegions = new Stack<Rect>();
+
+ final int countX = mCountX;
+ final int countY = mCountY;
+
+ if (minSpanX <= 0 || minSpanY <= 0 || spanX <= 0 || spanY <= 0 ||
+ spanX < minSpanX || spanY < minSpanY) {
+ return bestXY;
+ }
+
+ for (int y = 0; y < countY - (minSpanY - 1); y++) {
+ inner:
+ for (int x = 0; x < countX - (minSpanX - 1); x++) {
+ int ySize = -1;
+ int xSize = -1;
+ if (ignoreOccupied) {
+ // First, let's see if this thing fits anywhere
+ for (int i = 0; i < minSpanX; i++) {
+ for (int j = 0; j < minSpanY; j++) {
+ if (occupied[x + i][y + j]) {
+ continue inner;
+ }
+ }
+ }
+ xSize = minSpanX;
+ ySize = minSpanY;
+
+ // We know that the item will fit at _some_ acceptable size, now let's see
+ // how big we can make it. We'll alternate between incrementing x and y spans
+ // until we hit a limit.
+ boolean incX = true;
+ boolean hitMaxX = xSize >= spanX;
+ boolean hitMaxY = ySize >= spanY;
+ while (!(hitMaxX && hitMaxY)) {
+ if (incX && !hitMaxX) {
+ for (int j = 0; j < ySize; j++) {
+ if (x + xSize > countX -1 || occupied[x + xSize][y + j]) {
+ // We can't move out horizontally
+ hitMaxX = true;
+ }
+ }
+ if (!hitMaxX) {
+ xSize++;
+ }
+ } else if (!hitMaxY) {
+ for (int i = 0; i < xSize; i++) {
+ if (y + ySize > countY - 1 || occupied[x + i][y + ySize]) {
+ // We can't move out vertically
+ hitMaxY = true;
+ }
+ }
+ if (!hitMaxY) {
+ ySize++;
+ }
+ }
+ hitMaxX |= xSize >= spanX;
+ hitMaxY |= ySize >= spanY;
+ incX = !incX;
+ }
+ incX = true;
+ hitMaxX = xSize >= spanX;
+ hitMaxY = ySize >= spanY;
+ }
+ final int[] cellXY = mTmpXY;
+ cellToCenterPoint(x, y, cellXY);
+
+ // We verify that the current rect is not a sub-rect of any of our previous
+ // candidates. In this case, the current rect is disqualified in favour of the
+ // containing rect.
+ Rect currentRect = mTempRectStack.pop();
+ currentRect.set(x, y, x + xSize, y + ySize);
+ boolean contained = false;
+ for (Rect r : validRegions) {
+ if (r.contains(currentRect)) {
+ contained = true;
+ break;
+ }
+ }
+ validRegions.push(currentRect);
+ double distance = Math.sqrt(Math.pow(cellXY[0] - pixelX, 2)
+ + Math.pow(cellXY[1] - pixelY, 2));
+
+ if ((distance <= bestDistance && !contained) ||
+ currentRect.contains(bestRect)) {
+ bestDistance = distance;
+ bestXY[0] = x;
+ bestXY[1] = y;
+ if (resultSpan != null) {
+ resultSpan[0] = xSize;
+ resultSpan[1] = ySize;
+ }
+ bestRect.set(currentRect);
+ }
+ }
+ }
+ // re-mark space taken by ignoreView as occupied
+ markCellsAsOccupiedForView(ignoreView, occupied);
+
+ // Return -1, -1 if no suitable location found
+ if (bestDistance == Double.MAX_VALUE) {
+ bestXY[0] = -1;
+ bestXY[1] = -1;
+ }
+ recycleTempRects(validRegions);
+ return bestXY;
+ }
+
+ /**
+ * Find a vacant area that will fit the given bounds nearest the requested
+ * cell location, and will also weigh in a suggested direction vector of the
+ * desired location. This method computers distance based on unit grid distances,
+ * not pixel distances.
+ *
+ * @param cellX The X cell nearest to which you want to search for a vacant area.
+ * @param cellY The Y cell nearest which you want to search for a vacant area.
+ * @param spanX Horizontal span of the object.
+ * @param spanY Vertical span of the object.
+ * @param direction The favored direction in which the views should move from x, y
+ * @param exactDirectionOnly If this parameter is true, then only solutions where the direction
+ * matches exactly. Otherwise we find the best matching direction.
+ * @param occoupied The array which represents which cells in the CellLayout are occupied
+ * @param blockOccupied The array which represents which cells in the specified block (cellX,
+ * cellY, spanX, spanY) are occupied. This is used when try to move a group of views.
+ * @param result Array in which to place the result, or null (in which case a new array will
+ * be allocated)
+ * @return The X, Y cell of a vacant area that can contain this object,
+ * nearest the requested location.
+ */
+ private int[] findNearestArea(int cellX, int cellY, int spanX, int spanY, int[] direction,
+ boolean[][] occupied, boolean blockOccupied[][], int[] result) {
+ // Keep track of best-scoring drop area
+ final int[] bestXY = result != null ? result : new int[2];
+ float bestDistance = Float.MAX_VALUE;
+ int bestDirectionScore = Integer.MIN_VALUE;
+
+ final int countX = mCountX;
+ final int countY = mCountY;
+
+ for (int y = 0; y < countY - (spanY - 1); y++) {
+ inner:
+ for (int x = 0; x < countX - (spanX - 1); x++) {
+ // First, let's see if this thing fits anywhere
+ for (int i = 0; i < spanX; i++) {
+ for (int j = 0; j < spanY; j++) {
+ if (occupied[x + i][y + j] && (blockOccupied == null || blockOccupied[i][j])) {
+ continue inner;
+ }
+ }
+ }
+
+ float distance = (float)
+ Math.sqrt((x - cellX) * (x - cellX) + (y - cellY) * (y - cellY));
+ int[] curDirection = mTmpPoint;
+ computeDirectionVector(x - cellX, y - cellY, curDirection);
+ // The direction score is just the dot product of the two candidate direction
+ // and that passed in.
+ int curDirectionScore = direction[0] * curDirection[0] +
+ direction[1] * curDirection[1];
+ boolean exactDirectionOnly = false;
+ boolean directionMatches = direction[0] == curDirection[0] &&
+ direction[0] == curDirection[0];
+ if ((directionMatches || !exactDirectionOnly) &&
+ Float.compare(distance, bestDistance) < 0 || (Float.compare(distance,
+ bestDistance) == 0 && curDirectionScore > bestDirectionScore)) {
+ bestDistance = distance;
+ bestDirectionScore = curDirectionScore;
+ bestXY[0] = x;
+ bestXY[1] = y;
+ }
+ }
+ }
+
+ // Return -1, -1 if no suitable location found
+ if (bestDistance == Float.MAX_VALUE) {
+ bestXY[0] = -1;
+ bestXY[1] = -1;
+ }
+ return bestXY;
+ }
+
+ private boolean addViewToTempLocation(View v, Rect rectOccupiedByPotentialDrop,
+ int[] direction, ItemConfiguration currentState) {
+ CellAndSpan c = currentState.map.get(v);
+ boolean success = false;
+ markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false);
+ markCellsForRect(rectOccupiedByPotentialDrop, mTmpOccupied, true);
+
+ findNearestArea(c.x, c.y, c.spanX, c.spanY, direction, mTmpOccupied, null, mTempLocation);
+
+ if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) {
+ c.x = mTempLocation[0];
+ c.y = mTempLocation[1];
+ success = true;
+ }
+ markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true);
+ return success;
+ }
+
+ /**
+ * This helper class defines a cluster of views. It helps with defining complex edges
+ * of the cluster and determining how those edges interact with other views. The edges
+ * essentially define a fine-grained boundary around the cluster of views -- like a more
+ * precise version of a bounding box.
+ */
+ private class ViewCluster {
+ final static int LEFT = 0;
+ final static int TOP = 1;
+ final static int RIGHT = 2;
+ final static int BOTTOM = 3;
+
+ ArrayList<View> views;
+ ItemConfiguration config;
+ Rect boundingRect = new Rect();
+
+ int[] leftEdge = new int[mCountY];
+ int[] rightEdge = new int[mCountY];
+ int[] topEdge = new int[mCountX];
+ int[] bottomEdge = new int[mCountX];
+ boolean leftEdgeDirty, rightEdgeDirty, topEdgeDirty, bottomEdgeDirty, boundingRectDirty;
+
+ @SuppressWarnings("unchecked")
+ public ViewCluster(ArrayList<View> views, ItemConfiguration config) {
+ this.views = (ArrayList<View>) views.clone();
+ this.config = config;
+ resetEdges();
+ }
+
+ void resetEdges() {
+ for (int i = 0; i < mCountX; i++) {
+ topEdge[i] = -1;
+ bottomEdge[i] = -1;
+ }
+ for (int i = 0; i < mCountY; i++) {
+ leftEdge[i] = -1;
+ rightEdge[i] = -1;
+ }
+ leftEdgeDirty = true;
+ rightEdgeDirty = true;
+ bottomEdgeDirty = true;
+ topEdgeDirty = true;
+ boundingRectDirty = true;
+ }
+
+ void computeEdge(int which, int[] edge) {
+ int count = views.size();
+ for (int i = 0; i < count; i++) {
+ CellAndSpan cs = config.map.get(views.get(i));
+ switch (which) {
+ case LEFT:
+ int left = cs.x;
+ for (int j = cs.y; j < cs.y + cs.spanY; j++) {
+ if (left < edge[j] || edge[j] < 0) {
+ edge[j] = left;
+ }
+ }
+ break;
+ case RIGHT:
+ int right = cs.x + cs.spanX;
+ for (int j = cs.y; j < cs.y + cs.spanY; j++) {
+ if (right > edge[j]) {
+ edge[j] = right;
+ }
+ }
+ break;
+ case TOP:
+ int top = cs.y;
+ for (int j = cs.x; j < cs.x + cs.spanX; j++) {
+ if (top < edge[j] || edge[j] < 0) {
+ edge[j] = top;
+ }
+ }
+ break;
+ case BOTTOM:
+ int bottom = cs.y + cs.spanY;
+ for (int j = cs.x; j < cs.x + cs.spanX; j++) {
+ if (bottom > edge[j]) {
+ edge[j] = bottom;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ boolean isViewTouchingEdge(View v, int whichEdge) {
+ CellAndSpan cs = config.map.get(v);
+
+ int[] edge = getEdge(whichEdge);
+
+ switch (whichEdge) {
+ case LEFT:
+ for (int i = cs.y; i < cs.y + cs.spanY; i++) {
+ if (edge[i] == cs.x + cs.spanX) {
+ return true;
+ }
+ }
+ break;
+ case RIGHT:
+ for (int i = cs.y; i < cs.y + cs.spanY; i++) {
+ if (edge[i] == cs.x) {
+ return true;
+ }
+ }
+ break;
+ case TOP:
+ for (int i = cs.x; i < cs.x + cs.spanX; i++) {
+ if (edge[i] == cs.y + cs.spanY) {
+ return true;
+ }
+ }
+ break;
+ case BOTTOM:
+ for (int i = cs.x; i < cs.x + cs.spanX; i++) {
+ if (edge[i] == cs.y) {
+ return true;
+ }
+ }
+ break;
+ }
+ return false;
+ }
+
+ void shift(int whichEdge, int delta) {
+ for (View v: views) {
+ CellAndSpan c = config.map.get(v);
+ switch (whichEdge) {
+ case LEFT:
+ c.x -= delta;
+ break;
+ case RIGHT:
+ c.x += delta;
+ break;
+ case TOP:
+ c.y -= delta;
+ break;
+ case BOTTOM:
+ default:
+ c.y += delta;
+ break;
+ }
+ }
+ resetEdges();
+ }
+
+ public void addView(View v) {
+ views.add(v);
+ resetEdges();
+ }
+
+ public Rect getBoundingRect() {
+ if (boundingRectDirty) {
+ boolean first = true;
+ for (View v: views) {
+ CellAndSpan c = config.map.get(v);
+ if (first) {
+ boundingRect.set(c.x, c.y, c.x + c.spanX, c.y + c.spanY);
+ first = false;
+ } else {
+ boundingRect.union(c.x, c.y, c.x + c.spanX, c.y + c.spanY);
+ }
+ }
+ }
+ return boundingRect;
+ }
+
+ public int[] getEdge(int which) {
+ switch (which) {
+ case LEFT:
+ return getLeftEdge();
+ case RIGHT:
+ return getRightEdge();
+ case TOP:
+ return getTopEdge();
+ case BOTTOM:
+ default:
+ return getBottomEdge();
+ }
+ }
+
+ public int[] getLeftEdge() {
+ if (leftEdgeDirty) {
+ computeEdge(LEFT, leftEdge);
+ }
+ return leftEdge;
+ }
+
+ public int[] getRightEdge() {
+ if (rightEdgeDirty) {
+ computeEdge(RIGHT, rightEdge);
+ }
+ return rightEdge;
+ }
+
+ public int[] getTopEdge() {
+ if (topEdgeDirty) {
+ computeEdge(TOP, topEdge);
+ }
+ return topEdge;
+ }
+
+ public int[] getBottomEdge() {
+ if (bottomEdgeDirty) {
+ computeEdge(BOTTOM, bottomEdge);
+ }
+ return bottomEdge;
+ }
+
+ PositionComparator comparator = new PositionComparator();
+ class PositionComparator implements Comparator<View> {
+ int whichEdge = 0;
+ public int compare(View left, View right) {
+ CellAndSpan l = config.map.get(left);
+ CellAndSpan r = config.map.get(right);
+ switch (whichEdge) {
+ case LEFT:
+ return (r.x + r.spanX) - (l.x + l.spanX);
+ case RIGHT:
+ return l.x - r.x;
+ case TOP:
+ return (r.y + r.spanY) - (l.y + l.spanY);
+ case BOTTOM:
+ default:
+ return l.y - r.y;
+ }
+ }
+ }
+
+ public void sortConfigurationForEdgePush(int edge) {
+ comparator.whichEdge = edge;
+ Collections.sort(config.sortedViews, comparator);
+ }
+ }
+
+ private boolean pushViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop,
+ int[] direction, View dragView, ItemConfiguration currentState) {
+
+ ViewCluster cluster = new ViewCluster(views, currentState);
+ Rect clusterRect = cluster.getBoundingRect();
+ int whichEdge;
+ int pushDistance;
+ boolean fail = false;
+
+ // Determine the edge of the cluster that will be leading the push and how far
+ // the cluster must be shifted.
+ if (direction[0] < 0) {
+ whichEdge = ViewCluster.LEFT;
+ pushDistance = clusterRect.right - rectOccupiedByPotentialDrop.left;
+ } else if (direction[0] > 0) {
+ whichEdge = ViewCluster.RIGHT;
+ pushDistance = rectOccupiedByPotentialDrop.right - clusterRect.left;
+ } else if (direction[1] < 0) {
+ whichEdge = ViewCluster.TOP;
+ pushDistance = clusterRect.bottom - rectOccupiedByPotentialDrop.top;
+ } else {
+ whichEdge = ViewCluster.BOTTOM;
+ pushDistance = rectOccupiedByPotentialDrop.bottom - clusterRect.top;
+ }
+
+ // Break early for invalid push distance.
+ if (pushDistance <= 0) {
+ return false;
+ }
+
+ // Mark the occupied state as false for the group of views we want to move.
+ for (View v: views) {
+ CellAndSpan c = currentState.map.get(v);
+ markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false);
+ }
+
+ // We save the current configuration -- if we fail to find a solution we will revert
+ // to the initial state. The process of finding a solution modifies the configuration
+ // in place, hence the need for revert in the failure case.
+ currentState.save();
+
+ // The pushing algorithm is simplified by considering the views in the order in which
+ // they would be pushed by the cluster. For example, if the cluster is leading with its
+ // left edge, we consider sort the views by their right edge, from right to left.
+ cluster.sortConfigurationForEdgePush(whichEdge);
+
+ while (pushDistance > 0 && !fail) {
+ for (View v: currentState.sortedViews) {
+ // For each view that isn't in the cluster, we see if the leading edge of the
+ // cluster is contacting the edge of that view. If so, we add that view to the
+ // cluster.
+ if (!cluster.views.contains(v) && v != dragView) {
+ if (cluster.isViewTouchingEdge(v, whichEdge)) {
+ LayoutParams lp = (LayoutParams) v.getLayoutParams();
+ if (!lp.canReorder) {
+ // The push solution includes the all apps button, this is not viable.
+ fail = true;
+ break;
+ }
+ cluster.addView(v);
+ CellAndSpan c = currentState.map.get(v);
+
+ // Adding view to cluster, mark it as not occupied.
+ markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false);
+ }
+ }
+ }
+ pushDistance--;
+
+ // The cluster has been completed, now we move the whole thing over in the appropriate
+ // direction.
+ cluster.shift(whichEdge, 1);
+ }
+
+ boolean foundSolution = false;
+ clusterRect = cluster.getBoundingRect();
+
+ // Due to the nature of the algorithm, the only check required to verify a valid solution
+ // is to ensure that completed shifted cluster lies completely within the cell layout.
+ if (!fail && clusterRect.left >= 0 && clusterRect.right <= mCountX && clusterRect.top >= 0 &&
+ clusterRect.bottom <= mCountY) {
+ foundSolution = true;
+ } else {
+ currentState.restore();
+ }
+
+ // In either case, we set the occupied array as marked for the location of the views
+ for (View v: cluster.views) {
+ CellAndSpan c = currentState.map.get(v);
+ markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true);
+ }
+
+ return foundSolution;
+ }
+
+ private boolean addViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop,
+ int[] direction, View dragView, ItemConfiguration currentState) {
+ if (views.size() == 0) return true;
+
+ boolean success = false;
+ Rect boundingRect = null;
+ // We construct a rect which represents the entire group of views passed in
+ for (View v: views) {
+ CellAndSpan c = currentState.map.get(v);
+ if (boundingRect == null) {
+ boundingRect = new Rect(c.x, c.y, c.x + c.spanX, c.y + c.spanY);
+ } else {
+ boundingRect.union(c.x, c.y, c.x + c.spanX, c.y + c.spanY);
+ }
+ }
+
+ // Mark the occupied state as false for the group of views we want to move.
+ for (View v: views) {
+ CellAndSpan c = currentState.map.get(v);
+ markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false);
+ }
+
+ boolean[][] blockOccupied = new boolean[boundingRect.width()][boundingRect.height()];
+ int top = boundingRect.top;
+ int left = boundingRect.left;
+ // We mark more precisely which parts of the bounding rect are truly occupied, allowing
+ // for interlocking.
+ for (View v: views) {
+ CellAndSpan c = currentState.map.get(v);
+ markCellsForView(c.x - left, c.y - top, c.spanX, c.spanY, blockOccupied, true);
+ }
+
+ markCellsForRect(rectOccupiedByPotentialDrop, mTmpOccupied, true);
+
+ findNearestArea(boundingRect.left, boundingRect.top, boundingRect.width(),
+ boundingRect.height(), direction, mTmpOccupied, blockOccupied, mTempLocation);
+
+ // If we successfuly found a location by pushing the block of views, we commit it
+ if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) {
+ int deltaX = mTempLocation[0] - boundingRect.left;
+ int deltaY = mTempLocation[1] - boundingRect.top;
+ for (View v: views) {
+ CellAndSpan c = currentState.map.get(v);
+ c.x += deltaX;
+ c.y += deltaY;
+ }
+ success = true;
+ }
+
+ // In either case, we set the occupied array as marked for the location of the views
+ for (View v: views) {
+ CellAndSpan c = currentState.map.get(v);
+ markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true);
+ }
+ return success;
+ }
+
+ private void markCellsForRect(Rect r, boolean[][] occupied, boolean value) {
+ markCellsForView(r.left, r.top, r.width(), r.height(), occupied, value);
+ }
+
+ // This method tries to find a reordering solution which satisfies the push mechanic by trying
+ // to push items in each of the cardinal directions, in an order based on the direction vector
+ // passed.
+ private boolean attemptPushInDirection(ArrayList<View> intersectingViews, Rect occupied,
+ int[] direction, View ignoreView, ItemConfiguration solution) {
+ if ((Math.abs(direction[0]) + Math.abs(direction[1])) > 1) {
+ // If the direction vector has two non-zero components, we try pushing
+ // separately in each of the components.
+ int temp = direction[1];
+ direction[1] = 0;
+
+ if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+ ignoreView, solution)) {
+ return true;
+ }
+ direction[1] = temp;
+ temp = direction[0];
+ direction[0] = 0;
+
+ if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+ ignoreView, solution)) {
+ return true;
+ }
+ // Revert the direction
+ direction[0] = temp;
+
+ // Now we try pushing in each component of the opposite direction
+ direction[0] *= -1;
+ direction[1] *= -1;
+ temp = direction[1];
+ direction[1] = 0;
+ if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+ ignoreView, solution)) {
+ return true;
+ }
+
+ direction[1] = temp;
+ temp = direction[0];
+ direction[0] = 0;
+ if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+ ignoreView, solution)) {
+ return true;
+ }
+ // revert the direction
+ direction[0] = temp;
+ direction[0] *= -1;
+ direction[1] *= -1;
+
+ } else {
+ // If the direction vector has a single non-zero component, we push first in the
+ // direction of the vector
+ if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+ ignoreView, solution)) {
+ return true;
+ }
+ // Then we try the opposite direction
+ direction[0] *= -1;
+ direction[1] *= -1;
+ if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+ ignoreView, solution)) {
+ return true;
+ }
+ // Switch the direction back
+ direction[0] *= -1;
+ direction[1] *= -1;
+
+ // If we have failed to find a push solution with the above, then we try
+ // to find a solution by pushing along the perpendicular axis.
+
+ // Swap the components
+ int temp = direction[1];
+ direction[1] = direction[0];
+ direction[0] = temp;
+ if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+ ignoreView, solution)) {
+ return true;
+ }
+
+ // Then we try the opposite direction
+ direction[0] *= -1;
+ direction[1] *= -1;
+ if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+ ignoreView, solution)) {
+ return true;
+ }
+ // Switch the direction back
+ direction[0] *= -1;
+ direction[1] *= -1;
+
+ // Swap the components back
+ temp = direction[1];
+ direction[1] = direction[0];
+ direction[0] = temp;
+ }
+ return false;
+ }
+
+ private boolean rearrangementExists(int cellX, int cellY, int spanX, int spanY, int[] direction,
+ View ignoreView, ItemConfiguration solution) {
+ // Return early if get invalid cell positions
+ if (cellX < 0 || cellY < 0) return false;
+
+ mIntersectingViews.clear();
+ mOccupiedRect.set(cellX, cellY, cellX + spanX, cellY + spanY);
+
+ // Mark the desired location of the view currently being dragged.
+ if (ignoreView != null) {
+ CellAndSpan c = solution.map.get(ignoreView);
+ if (c != null) {
+ c.x = cellX;
+ c.y = cellY;
+ }
+ }
+ Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY);
+ Rect r1 = new Rect();
+ for (View child: solution.map.keySet()) {
+ if (child == ignoreView) continue;
+ CellAndSpan c = solution.map.get(child);
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ r1.set(c.x, c.y, c.x + c.spanX, c.y + c.spanY);
+ if (Rect.intersects(r0, r1)) {
+ if (!lp.canReorder) {
+ return false;
+ }
+ mIntersectingViews.add(child);
+ }
+ }
+
+ // First we try to find a solution which respects the push mechanic. That is,
+ // we try to find a solution such that no displaced item travels through another item
+ // without also displacing that item.
+ if (attemptPushInDirection(mIntersectingViews, mOccupiedRect, direction, ignoreView,
+ solution)) {
+ return true;
+ }
+
+ // Next we try moving the views as a block, but without requiring the push mechanic.
+ if (addViewsToTempLocation(mIntersectingViews, mOccupiedRect, direction, ignoreView,
+ solution)) {
+ return true;
+ }
+
+ // Ok, they couldn't move as a block, let's move them individually
+ for (View v : mIntersectingViews) {
+ if (!addViewToTempLocation(v, mOccupiedRect, direction, solution)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /*
+ * Returns a pair (x, y), where x,y are in {-1, 0, 1} corresponding to vector between
+ * the provided point and the provided cell
+ */
+ private void computeDirectionVector(float deltaX, float deltaY, int[] result) {
+ double angle = Math.atan(((float) deltaY) / deltaX);
+
+ result[0] = 0;
+ result[1] = 0;
+ if (Math.abs(Math.cos(angle)) > 0.5f) {
+ result[0] = (int) Math.signum(deltaX);
+ }
+ if (Math.abs(Math.sin(angle)) > 0.5f) {
+ result[1] = (int) Math.signum(deltaY);
+ }
+ }
+
+ private void copyOccupiedArray(boolean[][] occupied) {
+ for (int i = 0; i < mCountX; i++) {
+ for (int j = 0; j < mCountY; j++) {
+ occupied[i][j] = mOccupied[i][j];
+ }
+ }
+ }
+
+ ItemConfiguration simpleSwap(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX,
+ int spanY, int[] direction, View dragView, boolean decX, ItemConfiguration solution) {
+ // Copy the current state into the solution. This solution will be manipulated as necessary.
+ copyCurrentStateToSolution(solution, false);
+ // Copy the current occupied array into the temporary occupied array. This array will be
+ // manipulated as necessary to find a solution.
+ copyOccupiedArray(mTmpOccupied);
+
+ // We find the nearest cell into which we would place the dragged item, assuming there's
+ // nothing in its way.
+ int result[] = new int[2];
+ result = findNearestArea(pixelX, pixelY, spanX, spanY, result);
+
+ boolean success = false;
+ // First we try the exact nearest position of the item being dragged,
+ // we will then want to try to move this around to other neighbouring positions
+ success = rearrangementExists(result[0], result[1], spanX, spanY, direction, dragView,
+ solution);
+
+ if (!success) {
+ // We try shrinking the widget down to size in an alternating pattern, shrink 1 in
+ // x, then 1 in y etc.
+ if (spanX > minSpanX && (minSpanY == spanY || decX)) {
+ return simpleSwap(pixelX, pixelY, minSpanX, minSpanY, spanX - 1, spanY, direction,
+ dragView, false, solution);
+ } else if (spanY > minSpanY) {
+ return simpleSwap(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY - 1, direction,
+ dragView, true, solution);
+ }
+ solution.isSolution = false;
+ } else {
+ solution.isSolution = true;
+ solution.dragViewX = result[0];
+ solution.dragViewY = result[1];
+ solution.dragViewSpanX = spanX;
+ solution.dragViewSpanY = spanY;
+ }
+ return solution;
+ }
+
+ private void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) {
+ int childCount = mShortcutsAndWidgets.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = mShortcutsAndWidgets.getChildAt(i);
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ CellAndSpan c;
+ if (temp) {
+ c = new CellAndSpan(lp.tmpCellX, lp.tmpCellY, lp.cellHSpan, lp.cellVSpan);
+ } else {
+ c = new CellAndSpan(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan);
+ }
+ solution.add(child, c);
+ }
+ }
+
+ private void copySolutionToTempState(ItemConfiguration solution, View dragView) {
+ for (int i = 0; i < mCountX; i++) {
+ for (int j = 0; j < mCountY; j++) {
+ mTmpOccupied[i][j] = false;
+ }
+ }
+
+ int childCount = mShortcutsAndWidgets.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = mShortcutsAndWidgets.getChildAt(i);
+ if (child == dragView) continue;
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ CellAndSpan c = solution.map.get(child);
+ if (c != null) {
+ lp.tmpCellX = c.x;
+ lp.tmpCellY = c.y;
+ lp.cellHSpan = c.spanX;
+ lp.cellVSpan = c.spanY;
+ markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true);
+ }
+ }
+ markCellsForView(solution.dragViewX, solution.dragViewY, solution.dragViewSpanX,
+ solution.dragViewSpanY, mTmpOccupied, true);
+ }
+
+ private void animateItemsToSolution(ItemConfiguration solution, View dragView, boolean
+ commitDragView) {
+
+ boolean[][] occupied = DESTRUCTIVE_REORDER ? mOccupied : mTmpOccupied;
+ for (int i = 0; i < mCountX; i++) {
+ for (int j = 0; j < mCountY; j++) {
+ occupied[i][j] = false;
+ }
+ }
+
+ int childCount = mShortcutsAndWidgets.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = mShortcutsAndWidgets.getChildAt(i);
+ if (child == dragView) continue;
+ CellAndSpan c = solution.map.get(child);
+ if (c != null) {
+ animateChildToPosition(child, c.x, c.y, REORDER_ANIMATION_DURATION, 0,
+ DESTRUCTIVE_REORDER, false);
+ markCellsForView(c.x, c.y, c.spanX, c.spanY, occupied, true);
+ }
+ }
+ if (commitDragView) {
+ markCellsForView(solution.dragViewX, solution.dragViewY, solution.dragViewSpanX,
+ solution.dragViewSpanY, occupied, true);
+ }
+ }
+
+ // This method starts or changes the reorder hint animations
+ private void beginOrAdjustHintAnimations(ItemConfiguration solution, View dragView, int delay) {
+ int childCount = mShortcutsAndWidgets.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = mShortcutsAndWidgets.getChildAt(i);
+ if (child == dragView) continue;
+ CellAndSpan c = solution.map.get(child);
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (c != null) {
+ ReorderHintAnimation rha = new ReorderHintAnimation(child, lp.cellX, lp.cellY,
+ c.x, c.y, c.spanX, c.spanY);
+ rha.animate();
+ }
+ }
+ }
+
+ // Class which represents the reorder hint animations. These animations show that an item is
+ // in a temporary state, and hint at where the item will return to.
+ class ReorderHintAnimation {
+ View child;
+ float finalDeltaX;
+ float finalDeltaY;
+ float initDeltaX;
+ float initDeltaY;
+ float finalScale;
+ float initScale;
+ private static final int DURATION = 300;
+ Animator a;
+
+ public ReorderHintAnimation(View child, int cellX0, int cellY0, int cellX1, int cellY1,
+ int spanX, int spanY) {
+ regionToCenterPoint(cellX0, cellY0, spanX, spanY, mTmpPoint);
+ final int x0 = mTmpPoint[0];
+ final int y0 = mTmpPoint[1];
+ regionToCenterPoint(cellX1, cellY1, spanX, spanY, mTmpPoint);
+ final int x1 = mTmpPoint[0];
+ final int y1 = mTmpPoint[1];
+ final int dX = x1 - x0;
+ final int dY = y1 - y0;
+ finalDeltaX = 0;
+ finalDeltaY = 0;
+ if (dX == dY && dX == 0) {
+ } else {
+ if (dY == 0) {
+ finalDeltaX = - Math.signum(dX) * mReorderHintAnimationMagnitude;
+ } else if (dX == 0) {
+ finalDeltaY = - Math.signum(dY) * mReorderHintAnimationMagnitude;
+ } else {
+ double angle = Math.atan( (float) (dY) / dX);
+ finalDeltaX = (int) (- Math.signum(dX) *
+ Math.abs(Math.cos(angle) * mReorderHintAnimationMagnitude));
+ finalDeltaY = (int) (- Math.signum(dY) *
+ Math.abs(Math.sin(angle) * mReorderHintAnimationMagnitude));
+ }
+ }
+ initDeltaX = child.getTranslationX();
+ initDeltaY = child.getTranslationY();
+ finalScale = getChildrenScale() - 4.0f / child.getWidth();
+ initScale = child.getScaleX();
+ this.child = child;
+ }
+
+ void animate() {
+ if (mShakeAnimators.containsKey(child)) {
+ ReorderHintAnimation oldAnimation = mShakeAnimators.get(child);
+ oldAnimation.cancel();
+ mShakeAnimators.remove(child);
+ if (finalDeltaX == 0 && finalDeltaY == 0) {
+ completeAnimationImmediately();
+ return;
+ }
+ }
+ if (finalDeltaX == 0 && finalDeltaY == 0) {
+ return;
+ }
+ ValueAnimator va = LauncherAnimUtils.ofFloat(child, 0f, 1f);
+ a = va;
+ va.setRepeatMode(ValueAnimator.REVERSE);
+ va.setRepeatCount(ValueAnimator.INFINITE);
+ va.setDuration(DURATION);
+ va.setStartDelay((int) (Math.random() * 60));
+ va.addUpdateListener(new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ float r = ((Float) animation.getAnimatedValue()).floatValue();
+ float x = r * finalDeltaX + (1 - r) * initDeltaX;
+ float y = r * finalDeltaY + (1 - r) * initDeltaY;
+ child.setTranslationX(x);
+ child.setTranslationY(y);
+ float s = r * finalScale + (1 - r) * initScale;
+ child.setScaleX(s);
+ child.setScaleY(s);
+ }
+ });
+ va.addListener(new AnimatorListenerAdapter() {
+ public void onAnimationRepeat(Animator animation) {
+ // We make sure to end only after a full period
+ initDeltaX = 0;
+ initDeltaY = 0;
+ initScale = getChildrenScale();
+ }
+ });
+ mShakeAnimators.put(child, this);
+ va.start();
+ }
+
+ private void cancel() {
+ if (a != null) {
+ a.cancel();
+ }
+ }
+
+ private void completeAnimationImmediately() {
+ if (a != null) {
+ a.cancel();
+ }
+
+ AnimatorSet s = LauncherAnimUtils.createAnimatorSet();
+ a = s;
+ s.playTogether(
+ LauncherAnimUtils.ofFloat(child, "scaleX", getChildrenScale()),
+ LauncherAnimUtils.ofFloat(child, "scaleY", getChildrenScale()),
+ LauncherAnimUtils.ofFloat(child, "translationX", 0f),
+ LauncherAnimUtils.ofFloat(child, "translationY", 0f)
+ );
+ s.setDuration(REORDER_ANIMATION_DURATION);
+ s.setInterpolator(new android.view.animation.DecelerateInterpolator(1.5f));
+ s.start();
+ }
+ }
+
+ private void completeAndClearReorderHintAnimations() {
+ for (ReorderHintAnimation a: mShakeAnimators.values()) {
+ a.completeAnimationImmediately();
+ }
+ mShakeAnimators.clear();
+ }
+
+ private void commitTempPlacement() {
+ for (int i = 0; i < mCountX; i++) {
+ for (int j = 0; j < mCountY; j++) {
+ mOccupied[i][j] = mTmpOccupied[i][j];
+ }
+ }
+ int childCount = mShortcutsAndWidgets.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = mShortcutsAndWidgets.getChildAt(i);
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ ItemInfo info = (ItemInfo) child.getTag();
+ // We do a null check here because the item info can be null in the case of the
+ // AllApps button in the hotseat.
+ if (info != null) {
+ if (info.cellX != lp.tmpCellX || info.cellY != lp.tmpCellY ||
+ info.spanX != lp.cellHSpan || info.spanY != lp.cellVSpan) {
+ info.requiresDbUpdate = true;
+ }
+ info.cellX = lp.cellX = lp.tmpCellX;
+ info.cellY = lp.cellY = lp.tmpCellY;
+ info.spanX = lp.cellHSpan;
+ info.spanY = lp.cellVSpan;
+ }
+ }
+ mLauncher.getWorkspace().updateItemLocationsInDatabase(this);
+ }
+
+ public void setUseTempCoords(boolean useTempCoords) {
+ int childCount = mShortcutsAndWidgets.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ LayoutParams lp = (LayoutParams) mShortcutsAndWidgets.getChildAt(i).getLayoutParams();
+ lp.useTmpCoords = useTempCoords;
+ }
+ }
+
+ ItemConfiguration findConfigurationNoShuffle(int pixelX, int pixelY, int minSpanX, int minSpanY,
+ int spanX, int spanY, View dragView, ItemConfiguration solution) {
+ int[] result = new int[2];
+ int[] resultSpan = new int[2];
+ findNearestVacantArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, null, result,
+ resultSpan);
+ if (result[0] >= 0 && result[1] >= 0) {
+ copyCurrentStateToSolution(solution, false);
+ solution.dragViewX = result[0];
+ solution.dragViewY = result[1];
+ solution.dragViewSpanX = resultSpan[0];
+ solution.dragViewSpanY = resultSpan[1];
+ solution.isSolution = true;
+ } else {
+ solution.isSolution = false;
+ }
+ return solution;
+ }
+
+ public void prepareChildForDrag(View child) {
+ markCellsAsUnoccupiedForView(child);
+ }
+
+ /* This seems like it should be obvious and straight-forward, but when the direction vector
+ needs to match with the notion of the dragView pushing other views, we have to employ
+ a slightly more subtle notion of the direction vector. The question is what two points is
+ the vector between? The center of the dragView and its desired destination? Not quite, as
+ this doesn't necessarily coincide with the interaction of the dragView and items occupying
+ those cells. Instead we use some heuristics to often lock the vector to up, down, left
+ or right, which helps make pushing feel right.
+ */
+ private void getDirectionVectorForDrop(int dragViewCenterX, int dragViewCenterY, int spanX,
+ int spanY, View dragView, int[] resultDirection) {
+ int[] targetDestination = new int[2];
+
+ findNearestArea(dragViewCenterX, dragViewCenterY, spanX, spanY, targetDestination);
+ Rect dragRect = new Rect();
+ regionToRect(targetDestination[0], targetDestination[1], spanX, spanY, dragRect);
+ dragRect.offset(dragViewCenterX - dragRect.centerX(), dragViewCenterY - dragRect.centerY());
+
+ Rect dropRegionRect = new Rect();
+ getViewsIntersectingRegion(targetDestination[0], targetDestination[1], spanX, spanY,
+ dragView, dropRegionRect, mIntersectingViews);
+
+ int dropRegionSpanX = dropRegionRect.width();
+ int dropRegionSpanY = dropRegionRect.height();
+
+ regionToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(),
+ dropRegionRect.height(), dropRegionRect);
+
+ int deltaX = (dropRegionRect.centerX() - dragViewCenterX) / spanX;
+ int deltaY = (dropRegionRect.centerY() - dragViewCenterY) / spanY;
+
+ if (dropRegionSpanX == mCountX || spanX == mCountX) {
+ deltaX = 0;
+ }
+ if (dropRegionSpanY == mCountY || spanY == mCountY) {
+ deltaY = 0;
+ }
+
+ if (deltaX == 0 && deltaY == 0) {
+ // No idea what to do, give a random direction.
+ resultDirection[0] = 1;
+ resultDirection[1] = 0;
+ } else {
+ computeDirectionVector(deltaX, deltaY, resultDirection);
+ }
+ }
+
+ // For a given cell and span, fetch the set of views intersecting the region.
+ private void getViewsIntersectingRegion(int cellX, int cellY, int spanX, int spanY,
+ View dragView, Rect boundingRect, ArrayList<View> intersectingViews) {
+ if (boundingRect != null) {
+ boundingRect.set(cellX, cellY, cellX + spanX, cellY + spanY);
+ }
+ intersectingViews.clear();
+ Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY);
+ Rect r1 = new Rect();
+ final int count = mShortcutsAndWidgets.getChildCount();
+ for (int i = 0; i < count; i++) {
+ View child = mShortcutsAndWidgets.getChildAt(i);
+ if (child == dragView) continue;
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ r1.set(lp.cellX, lp.cellY, lp.cellX + lp.cellHSpan, lp.cellY + lp.cellVSpan);
+ if (Rect.intersects(r0, r1)) {
+ mIntersectingViews.add(child);
+ if (boundingRect != null) {
+ boundingRect.union(r1);
+ }
+ }
+ }
+ }
+
+ boolean isNearestDropLocationOccupied(int pixelX, int pixelY, int spanX, int spanY,
+ View dragView, int[] result) {
+ result = findNearestArea(pixelX, pixelY, spanX, spanY, result);
+ getViewsIntersectingRegion(result[0], result[1], spanX, spanY, dragView, null,
+ mIntersectingViews);
+ return !mIntersectingViews.isEmpty();
+ }
+
+ void revertTempState() {
+ if (!isItemPlacementDirty() || DESTRUCTIVE_REORDER) return;
+ final int count = mShortcutsAndWidgets.getChildCount();
+ for (int i = 0; i < count; i++) {
+ View child = mShortcutsAndWidgets.getChildAt(i);
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.cellY) {
+ lp.tmpCellX = lp.cellX;
+ lp.tmpCellY = lp.cellY;
+ animateChildToPosition(child, lp.cellX, lp.cellY, REORDER_ANIMATION_DURATION,
+ 0, false, false);
+ }
+ }
+ completeAndClearReorderHintAnimations();
+ setItemPlacementDirty(false);
+ }
+
+ boolean createAreaForResize(int cellX, int cellY, int spanX, int spanY,
+ View dragView, int[] direction, boolean commit) {
+ int[] pixelXY = new int[2];
+ regionToCenterPoint(cellX, cellY, spanX, spanY, pixelXY);
+
+ // First we determine if things have moved enough to cause a different layout
+ ItemConfiguration swapSolution = simpleSwap(pixelXY[0], pixelXY[1], spanX, spanY,
+ spanX, spanY, direction, dragView, true, new ItemConfiguration());
+
+ setUseTempCoords(true);
+ if (swapSolution != null && swapSolution.isSolution) {
+ // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother
+ // committing anything or animating anything as we just want to determine if a solution
+ // exists
+ copySolutionToTempState(swapSolution, dragView);
+ setItemPlacementDirty(true);
+ animateItemsToSolution(swapSolution, dragView, commit);
+
+ if (commit) {
+ commitTempPlacement();
+ completeAndClearReorderHintAnimations();
+ setItemPlacementDirty(false);
+ } else {
+ beginOrAdjustHintAnimations(swapSolution, dragView,
+ REORDER_ANIMATION_DURATION);
+ }
+ mShortcutsAndWidgets.requestLayout();
+ }
+ return swapSolution.isSolution;
+ }
+
+ int[] createArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY,
+ View dragView, int[] result, int resultSpan[], int mode) {
+ // First we determine if things have moved enough to cause a different layout
+ result = findNearestArea(pixelX, pixelY, spanX, spanY, result);
+
+ if (resultSpan == null) {
+ resultSpan = new int[2];
+ }
+
+ // When we are checking drop validity or actually dropping, we don't recompute the
+ // direction vector, since we want the solution to match the preview, and it's possible
+ // that the exact position of the item has changed to result in a new reordering outcome.
+ if ((mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL || mode == MODE_ACCEPT_DROP)
+ && mPreviousReorderDirection[0] != INVALID_DIRECTION) {
+ mDirectionVector[0] = mPreviousReorderDirection[0];
+ mDirectionVector[1] = mPreviousReorderDirection[1];
+ // We reset this vector after drop
+ if (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
+ mPreviousReorderDirection[0] = INVALID_DIRECTION;
+ mPreviousReorderDirection[1] = INVALID_DIRECTION;
+ }
+ } else {
+ getDirectionVectorForDrop(pixelX, pixelY, spanX, spanY, dragView, mDirectionVector);
+ mPreviousReorderDirection[0] = mDirectionVector[0];
+ mPreviousReorderDirection[1] = mDirectionVector[1];
+ }
+
+ ItemConfiguration swapSolution = simpleSwap(pixelX, pixelY, minSpanX, minSpanY,
+ spanX, spanY, mDirectionVector, dragView, true, new ItemConfiguration());
+
+ // We attempt the approach which doesn't shuffle views at all
+ ItemConfiguration noShuffleSolution = findConfigurationNoShuffle(pixelX, pixelY, minSpanX,
+ minSpanY, spanX, spanY, dragView, new ItemConfiguration());
+
+ ItemConfiguration finalSolution = null;
+ if (swapSolution.isSolution && swapSolution.area() >= noShuffleSolution.area()) {
+ finalSolution = swapSolution;
+ } else if (noShuffleSolution.isSolution) {
+ finalSolution = noShuffleSolution;
+ }
+
+ boolean foundSolution = true;
+ if (!DESTRUCTIVE_REORDER) {
+ setUseTempCoords(true);
+ }
+
+ if (finalSolution != null) {
+ result[0] = finalSolution.dragViewX;
+ result[1] = finalSolution.dragViewY;
+ resultSpan[0] = finalSolution.dragViewSpanX;
+ resultSpan[1] = finalSolution.dragViewSpanY;
+
+ // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother
+ // committing anything or animating anything as we just want to determine if a solution
+ // exists
+ if (mode == MODE_DRAG_OVER || mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
+ if (!DESTRUCTIVE_REORDER) {
+ copySolutionToTempState(finalSolution, dragView);
+ }
+ setItemPlacementDirty(true);
+ animateItemsToSolution(finalSolution, dragView, mode == MODE_ON_DROP);
+
+ if (!DESTRUCTIVE_REORDER &&
+ (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL)) {
+ commitTempPlacement();
+ completeAndClearReorderHintAnimations();
+ setItemPlacementDirty(false);
+ } else {
+ beginOrAdjustHintAnimations(finalSolution, dragView,
+ REORDER_ANIMATION_DURATION);
+ }
+ }
+ } else {
+ foundSolution = false;
+ result[0] = result[1] = resultSpan[0] = resultSpan[1] = -1;
+ }
+
+ if ((mode == MODE_ON_DROP || !foundSolution) && !DESTRUCTIVE_REORDER) {
+ setUseTempCoords(false);
+ }
+
+ mShortcutsAndWidgets.requestLayout();
+ return result;
+ }
+
+ void setItemPlacementDirty(boolean dirty) {
+ mItemPlacementDirty = dirty;
+ }
+ boolean isItemPlacementDirty() {
+ return mItemPlacementDirty;
+ }
+
+ private class ItemConfiguration {
+ HashMap<View, CellAndSpan> map = new HashMap<View, CellAndSpan>();
+ private HashMap<View, CellAndSpan> savedMap = new HashMap<View, CellAndSpan>();
+ ArrayList<View> sortedViews = new ArrayList<View>();
+ boolean isSolution = false;
+ int dragViewX, dragViewY, dragViewSpanX, dragViewSpanY;
+
+ void save() {
+ // Copy current state into savedMap
+ for (View v: map.keySet()) {
+ map.get(v).copy(savedMap.get(v));
+ }
+ }
+
+ void restore() {
+ // Restore current state from savedMap
+ for (View v: savedMap.keySet()) {
+ savedMap.get(v).copy(map.get(v));
+ }
+ }
+
+ void add(View v, CellAndSpan cs) {
+ map.put(v, cs);
+ savedMap.put(v, new CellAndSpan());
+ sortedViews.add(v);
+ }
+
+ int area() {
+ return dragViewSpanX * dragViewSpanY;
+ }
+ }
+
+ private class CellAndSpan {
+ int x, y;
+ int spanX, spanY;
+
+ public CellAndSpan() {
+ }
+
+ public void copy(CellAndSpan copy) {
+ copy.x = x;
+ copy.y = y;
+ copy.spanX = spanX;
+ copy.spanY = spanY;
+ }
+
+ public CellAndSpan(int x, int y, int spanX, int spanY) {
+ this.x = x;
+ this.y = y;
+ this.spanX = spanX;
+ this.spanY = spanY;
+ }
+
+ public String toString() {
+ return "(" + x + ", " + y + ": " + spanX + ", " + spanY + ")";
+ }
+
+ }
+
+ /**
+ * Find a vacant area that will fit the given bounds nearest the requested
+ * cell location. Uses Euclidean distance to score multiple vacant areas.
+ *
+ * @param pixelX The X location at which you want to search for a vacant area.
+ * @param pixelY The Y location at which you want to search for a vacant area.
+ * @param spanX Horizontal span of the object.
+ * @param spanY Vertical span of the object.
+ * @param ignoreView Considers space occupied by this view as unoccupied
+ * @param result Previously returned value to possibly recycle.
+ * @return The X, Y cell of a vacant area that can contain this object,
+ * nearest the requested location.
+ */
+ int[] findNearestVacantArea(
+ int pixelX, int pixelY, int spanX, int spanY, View ignoreView, int[] result) {
+ return findNearestArea(pixelX, pixelY, spanX, spanY, ignoreView, true, result);
+ }
+
+ /**
+ * Find a vacant area that will fit the given bounds nearest the requested
+ * cell location. Uses Euclidean distance to score multiple vacant areas.
+ *
+ * @param pixelX The X location at which you want to search for a vacant area.
+ * @param pixelY The Y location at which you want to search for a vacant area.
+ * @param minSpanX The minimum horizontal span required
+ * @param minSpanY The minimum vertical span required
+ * @param spanX Horizontal span of the object.
+ * @param spanY Vertical span of the object.
+ * @param ignoreView Considers space occupied by this view as unoccupied
+ * @param result Previously returned value to possibly recycle.
+ * @return The X, Y cell of a vacant area that can contain this object,
+ * nearest the requested location.
+ */
+ int[] findNearestVacantArea(int pixelX, int pixelY, int minSpanX, int minSpanY,
+ int spanX, int spanY, View ignoreView, int[] result, int[] resultSpan) {
+ return findNearestArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, ignoreView, true,
+ result, resultSpan, mOccupied);
+ }
+
+ /**
+ * Find a starting cell position that will fit the given bounds nearest the requested
+ * cell location. Uses Euclidean distance to score multiple vacant areas.
+ *
+ * @param pixelX The X location at which you want to search for a vacant area.
+ * @param pixelY The Y location at which you want to search for a vacant area.
+ * @param spanX Horizontal span of the object.
+ * @param spanY Vertical span of the object.
+ * @param ignoreView Considers space occupied by this view as unoccupied
+ * @param result Previously returned value to possibly recycle.
+ * @return The X, Y cell of a vacant area that can contain this object,
+ * nearest the requested location.
+ */
+ int[] findNearestArea(
+ int pixelX, int pixelY, int spanX, int spanY, int[] result) {
+ return findNearestArea(pixelX, pixelY, spanX, spanY, null, false, result);
+ }
+
+ boolean existsEmptyCell() {
+ return findCellForSpan(null, 1, 1);
+ }
+
+ /**
+ * Finds the upper-left coordinate of the first rectangle in the grid that can
+ * hold a cell of the specified dimensions. If intersectX and intersectY are not -1,
+ * then this method will only return coordinates for rectangles that contain the cell
+ * (intersectX, intersectY)
+ *
+ * @param cellXY The array that will contain the position of a vacant cell if such a cell
+ * can be found.
+ * @param spanX The horizontal span of the cell we want to find.
+ * @param spanY The vertical span of the cell we want to find.
+ *
+ * @return True if a vacant cell of the specified dimension was found, false otherwise.
+ */
+ boolean findCellForSpan(int[] cellXY, int spanX, int spanY) {
+ return findCellForSpanThatIntersectsIgnoring(cellXY, spanX, spanY, -1, -1, null, mOccupied);
+ }
+
+ /**
+ * Like above, but ignores any cells occupied by the item "ignoreView"
+ *
+ * @param cellXY The array that will contain the position of a vacant cell if such a cell
+ * can be found.
+ * @param spanX The horizontal span of the cell we want to find.
+ * @param spanY The vertical span of the cell we want to find.
+ * @param ignoreView The home screen item we should treat as not occupying any space
+ * @return
+ */
+ boolean findCellForSpanIgnoring(int[] cellXY, int spanX, int spanY, View ignoreView) {
+ return findCellForSpanThatIntersectsIgnoring(cellXY, spanX, spanY, -1, -1,
+ ignoreView, mOccupied);
+ }
+
+ /**
+ * Like above, but if intersectX and intersectY are not -1, then this method will try to
+ * return coordinates for rectangles that contain the cell [intersectX, intersectY]
+ *
+ * @param spanX The horizontal span of the cell we want to find.
+ * @param spanY The vertical span of the cell we want to find.
+ * @param ignoreView The home screen item we should treat as not occupying any space
+ * @param intersectX The X coordinate of the cell that we should try to overlap
+ * @param intersectX The Y coordinate of the cell that we should try to overlap
+ *
+ * @return True if a vacant cell of the specified dimension was found, false otherwise.
+ */
+ boolean findCellForSpanThatIntersects(int[] cellXY, int spanX, int spanY,
+ int intersectX, int intersectY) {
+ return findCellForSpanThatIntersectsIgnoring(
+ cellXY, spanX, spanY, intersectX, intersectY, null, mOccupied);
+ }
+
+ /**
+ * The superset of the above two methods
+ */
+ boolean findCellForSpanThatIntersectsIgnoring(int[] cellXY, int spanX, int spanY,
+ int intersectX, int intersectY, View ignoreView, boolean occupied[][]) {
+ // mark space take by ignoreView as available (method checks if ignoreView is null)
+ markCellsAsUnoccupiedForView(ignoreView, occupied);
+
+ boolean foundCell = false;
+ while (true) {
+ int startX = 0;
+ if (intersectX >= 0) {
+ startX = Math.max(startX, intersectX - (spanX - 1));
+ }
+ int endX = mCountX - (spanX - 1);
+ if (intersectX >= 0) {
+ endX = Math.min(endX, intersectX + (spanX - 1) + (spanX == 1 ? 1 : 0));
+ }
+ int startY = 0;
+ if (intersectY >= 0) {
+ startY = Math.max(startY, intersectY - (spanY - 1));
+ }
+ int endY = mCountY - (spanY - 1);
+ if (intersectY >= 0) {
+ endY = Math.min(endY, intersectY + (spanY - 1) + (spanY == 1 ? 1 : 0));
+ }
+
+ for (int y = startY; y < endY && !foundCell; y++) {
+ inner:
+ for (int x = startX; x < endX; x++) {
+ for (int i = 0; i < spanX; i++) {
+ for (int j = 0; j < spanY; j++) {
+ if (occupied[x + i][y + j]) {
+ // small optimization: we can skip to after the column we just found
+ // an occupied cell
+ x += i;
+ continue inner;
+ }
+ }
+ }
+ if (cellXY != null) {
+ cellXY[0] = x;
+ cellXY[1] = y;
+ }
+ foundCell = true;
+ break;
+ }
+ }
+ if (intersectX == -1 && intersectY == -1) {
+ break;
+ } else {
+ // if we failed to find anything, try again but without any requirements of
+ // intersecting
+ intersectX = -1;
+ intersectY = -1;
+ continue;
+ }
+ }
+
+ // re-mark space taken by ignoreView as occupied
+ markCellsAsOccupiedForView(ignoreView, occupied);
+ return foundCell;
+ }
+
+ /**
+ * A drag event has begun over this layout.
+ * It may have begun over this layout (in which case onDragChild is called first),
+ * or it may have begun on another layout.
+ */
+ void onDragEnter() {
+ mDragEnforcer.onDragEnter();
+ mDragging = true;
+ }
+
+ /**
+ * Called when drag has left this CellLayout or has been completed (successfully or not)
+ */
+ void onDragExit() {
+ mDragEnforcer.onDragExit();
+ // This can actually be called when we aren't in a drag, e.g. when adding a new
+ // item to this layout via the customize drawer.
+ // Guard against that case.
+ if (mDragging) {
+ mDragging = false;
+ }
+
+ // Invalidate the drag data
+ mDragCell[0] = mDragCell[1] = -1;
+ mDragOutlineAnims[mDragOutlineCurrent].animateOut();
+ mDragOutlineCurrent = (mDragOutlineCurrent + 1) % mDragOutlineAnims.length;
+ revertTempState();
+ setIsDragOverlapping(false);
+ }
+
+ /**
+ * Mark a child as having been dropped.
+ * At the beginning of the drag operation, the child may have been on another
+ * screen, but it is re-parented before this method is called.
+ *
+ * @param child The child that is being dropped
+ */
+ void onDropChild(View child) {
+ if (child != null) {
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ lp.dropped = true;
+ child.requestLayout();
+ }
+ }
+
+ /**
+ * Computes a bounding rectangle for a range of cells
+ *
+ * @param cellX X coordinate of upper left corner expressed as a cell position
+ * @param cellY Y coordinate of upper left corner expressed as a cell position
+ * @param cellHSpan Width in cells
+ * @param cellVSpan Height in cells
+ * @param resultRect Rect into which to put the results
+ */
+ public void cellToRect(int cellX, int cellY, int cellHSpan, int cellVSpan, Rect resultRect) {
+ final int cellWidth = mCellWidth;
+ final int cellHeight = mCellHeight;
+ final int widthGap = mWidthGap;
+ final int heightGap = mHeightGap;
+
+ final int hStartPadding = getPaddingLeft();
+ final int vStartPadding = getPaddingTop();
+
+ int width = cellHSpan * cellWidth + ((cellHSpan - 1) * widthGap);
+ int height = cellVSpan * cellHeight + ((cellVSpan - 1) * heightGap);
+
+ int x = hStartPadding + cellX * (cellWidth + widthGap);
+ int y = vStartPadding + cellY * (cellHeight + heightGap);
+
+ resultRect.set(x, y, x + width, y + height);
+ }
+
+ /**
+ * Computes the required horizontal and vertical cell spans to always
+ * fit the given rectangle.
+ *
+ * @param width Width in pixels
+ * @param height Height in pixels
+ * @param result An array of length 2 in which to store the result (may be null).
+ */
+ public int[] rectToCell(int width, int height, int[] result) {
+ return rectToCell(getResources(), width, height, result);
+ }
+
+ public static int[] rectToCell(Resources resources, int width, int height, int[] result) {
+ // Always assume we're working with the smallest span to make sure we
+ // reserve enough space in both orientations.
+ int actualWidth = resources.getDimensionPixelSize(R.dimen.workspace_cell_width);
+ int actualHeight = resources.getDimensionPixelSize(R.dimen.workspace_cell_height);
+ int smallerSize = Math.min(actualWidth, actualHeight);
+
+ // Always round up to next largest cell
+ int spanX = (int) Math.ceil(width / (float) smallerSize);
+ int spanY = (int) Math.ceil(height / (float) smallerSize);
+
+ if (result == null) {
+ return new int[] { spanX, spanY };
+ }
+ result[0] = spanX;
+ result[1] = spanY;
+ return result;
+ }
+
+ public int[] cellSpansToSize(int hSpans, int vSpans) {
+ int[] size = new int[2];
+ size[0] = hSpans * mCellWidth + (hSpans - 1) * mWidthGap;
+ size[1] = vSpans * mCellHeight + (vSpans - 1) * mHeightGap;
+ return size;
+ }
+
+ /**
+ * Calculate the grid spans needed to fit given item
+ */
+ public void calculateSpans(ItemInfo info) {
+ final int minWidth;
+ final int minHeight;
+
+ if (info instanceof LauncherAppWidgetInfo) {
+ minWidth = ((LauncherAppWidgetInfo) info).minWidth;
+ minHeight = ((LauncherAppWidgetInfo) info).minHeight;
+ } else if (info instanceof PendingAddWidgetInfo) {
+ minWidth = ((PendingAddWidgetInfo) info).minWidth;
+ minHeight = ((PendingAddWidgetInfo) info).minHeight;
+ } else {
+ // It's not a widget, so it must be 1x1
+ info.spanX = info.spanY = 1;
+ return;
+ }
+ int[] spans = rectToCell(minWidth, minHeight, null);
+ info.spanX = spans[0];
+ info.spanY = spans[1];
+ }
+
+ /**
+ * Find the first vacant cell, if there is one.
+ *
+ * @param vacant Holds the x and y coordinate of the vacant cell
+ * @param spanX Horizontal cell span.
+ * @param spanY Vertical cell span.
+ *
+ * @return True if a vacant cell was found
+ */
+ public boolean getVacantCell(int[] vacant, int spanX, int spanY) {
+
+ return findVacantCell(vacant, spanX, spanY, mCountX, mCountY, mOccupied);
+ }
+
+ static boolean findVacantCell(int[] vacant, int spanX, int spanY,
+ int xCount, int yCount, boolean[][] occupied) {
+
+ for (int y = 0; y < yCount; y++) {
+ for (int x = 0; x < xCount; x++) {
+ boolean available = !occupied[x][y];
+out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) {
+ for (int j = y; j < y + spanY - 1 && y < yCount; j++) {
+ available = available && !occupied[i][j];
+ if (!available) break out;
+ }
+ }
+
+ if (available) {
+ vacant[0] = x;
+ vacant[1] = y;
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private void clearOccupiedCells() {
+ for (int x = 0; x < mCountX; x++) {
+ for (int y = 0; y < mCountY; y++) {
+ mOccupied[x][y] = false;
+ }
+ }
+ }
+
+ public void onMove(View view, int newCellX, int newCellY, int newSpanX, int newSpanY) {
+ markCellsAsUnoccupiedForView(view);
+ markCellsForView(newCellX, newCellY, newSpanX, newSpanY, mOccupied, true);
+ }
+
+ public void markCellsAsOccupiedForView(View view) {
+ markCellsAsOccupiedForView(view, mOccupied);
+ }
+ public void markCellsAsOccupiedForView(View view, boolean[][] occupied) {
+ if (view == null || view.getParent() != mShortcutsAndWidgets) return;
+ LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, occupied, true);
+ }
+
+ public void markCellsAsUnoccupiedForView(View view) {
+ markCellsAsUnoccupiedForView(view, mOccupied);
+ }
+ public void markCellsAsUnoccupiedForView(View view, boolean occupied[][]) {
+ if (view == null || view.getParent() != mShortcutsAndWidgets) return;
+ LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, occupied, false);
+ }
+
+ private void markCellsForView(int cellX, int cellY, int spanX, int spanY, boolean[][] occupied,
+ boolean value) {
+ if (cellX < 0 || cellY < 0) return;
+ for (int x = cellX; x < cellX + spanX && x < mCountX; x++) {
+ for (int y = cellY; y < cellY + spanY && y < mCountY; y++) {
+ occupied[x][y] = value;
+ }
+ }
+ }
+
+ public int getDesiredWidth() {
+ return getPaddingLeft() + getPaddingRight() + (mCountX * mCellWidth) +
+ (Math.max((mCountX - 1), 0) * mWidthGap);
+ }
+
+ public int getDesiredHeight() {
+ return getPaddingTop() + getPaddingBottom() + (mCountY * mCellHeight) +
+ (Math.max((mCountY - 1), 0) * mHeightGap);
+ }
+
+ public boolean isOccupied(int x, int y) {
+ if (x < mCountX && y < mCountY) {
+ return mOccupied[x][y];
+ } else {
+ throw new RuntimeException("Position exceeds the bound of this CellLayout");
+ }
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new CellLayout.LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof CellLayout.LayoutParams;
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return new CellLayout.LayoutParams(p);
+ }
+
+ public static class CellLayoutAnimationController extends LayoutAnimationController {
+ public CellLayoutAnimationController(Animation animation, float delay) {
+ super(animation, delay);
+ }
+
+ @Override
+ protected long getDelayForView(View view) {
+ return (int) (Math.random() * 150);
+ }
+ }
+
+ public static class LayoutParams extends ViewGroup.MarginLayoutParams {
+ /**
+ * Horizontal location of the item in the grid.
+ */
+ @ViewDebug.ExportedProperty
+ public int cellX;
+
+ /**
+ * Vertical location of the item in the grid.
+ */
+ @ViewDebug.ExportedProperty
+ public int cellY;
+
+ /**
+ * Temporary horizontal location of the item in the grid during reorder
+ */
+ public int tmpCellX;
+
+ /**
+ * Temporary vertical location of the item in the grid during reorder
+ */
+ public int tmpCellY;
+
+ /**
+ * Indicates that the temporary coordinates should be used to layout the items
+ */
+ public boolean useTmpCoords;
+
+ /**
+ * Number of cells spanned horizontally by the item.
+ */
+ @ViewDebug.ExportedProperty
+ public int cellHSpan;
+
+ /**
+ * Number of cells spanned vertically by the item.
+ */
+ @ViewDebug.ExportedProperty
+ public int cellVSpan;
+
+ /**
+ * Indicates whether the item will set its x, y, width and height parameters freely,
+ * or whether these will be computed based on cellX, cellY, cellHSpan and cellVSpan.
+ */
+ public boolean isLockedToGrid = true;
+
+ /**
+ * Indicates whether this item can be reordered. Always true except in the case of the
+ * the AllApps button.
+ */
+ public boolean canReorder = true;
+
+ // X coordinate of the view in the layout.
+ @ViewDebug.ExportedProperty
+ int x;
+ // Y coordinate of the view in the layout.
+ @ViewDebug.ExportedProperty
+ int y;
+
+ boolean dropped;
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ cellHSpan = 1;
+ cellVSpan = 1;
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ cellHSpan = 1;
+ cellVSpan = 1;
+ }
+
+ public LayoutParams(LayoutParams source) {
+ super(source);
+ this.cellX = source.cellX;
+ this.cellY = source.cellY;
+ this.cellHSpan = source.cellHSpan;
+ this.cellVSpan = source.cellVSpan;
+ }
+
+ public LayoutParams(int cellX, int cellY, int cellHSpan, int cellVSpan) {
+ super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ this.cellX = cellX;
+ this.cellY = cellY;
+ this.cellHSpan = cellHSpan;
+ this.cellVSpan = cellVSpan;
+ }
+
+ public void setup(int cellWidth, int cellHeight, int widthGap, int heightGap,
+ boolean invertHorizontally, int colCount) {
+ if (isLockedToGrid) {
+ final int myCellHSpan = cellHSpan;
+ final int myCellVSpan = cellVSpan;
+ int myCellX = useTmpCoords ? tmpCellX : cellX;
+ int myCellY = useTmpCoords ? tmpCellY : cellY;
+
+ if (invertHorizontally) {
+ myCellX = colCount - myCellX - cellHSpan;
+ }
+
+ width = myCellHSpan * cellWidth + ((myCellHSpan - 1) * widthGap) -
+ leftMargin - rightMargin;
+ height = myCellVSpan * cellHeight + ((myCellVSpan - 1) * heightGap) -
+ topMargin - bottomMargin;
+ x = (int) (myCellX * (cellWidth + widthGap) + leftMargin);
+ y = (int) (myCellY * (cellHeight + heightGap) + topMargin);
+ }
+ }
+
+ public String toString() {
+ return "(" + this.cellX + ", " + this.cellY + ")";
+ }
+
+ public void setWidth(int width) {
+ this.width = width;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public void setHeight(int height) {
+ this.height = height;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public void setX(int x) {
+ this.x = x;
+ }
+
+ public int getX() {
+ return x;
+ }
+
+ public void setY(int y) {
+ this.y = y;
+ }
+
+ public int getY() {
+ return y;
+ }
+ }
+
+ // This class stores info for two purposes:
+ // 1. When dragging items (mDragInfo in Workspace), we store the View, its cellX & cellY,
+ // its spanX, spanY, and the screen it is on
+ // 2. When long clicking on an empty cell in a CellLayout, we save information about the
+ // cellX and cellY coordinates and which page was clicked. We then set this as a tag on
+ // the CellLayout that was long clicked
+ static final class CellInfo {
+ View cell;
+ int cellX = -1;
+ int cellY = -1;
+ int spanX;
+ int spanY;
+ int screen;
+ long container;
+
+ @Override
+ public String toString() {
+ return "Cell[view=" + (cell == null ? "null" : cell.getClass())
+ + ", x=" + cellX + ", y=" + cellY + "]";
+ }
+ }
+
+ public boolean lastDownOnOccupiedCell() {
+ return mLastDownOnOccupiedCell;
+ }
+}