diff options
Diffstat (limited to 'src')
47 files changed, 4601 insertions, 2086 deletions
diff --git a/src/com/android/launcher2/AppWidgetResizeFrame.java b/src/com/android/launcher2/AppWidgetResizeFrame.java index 6d132ebf0..c01a882cb 100644 --- a/src/com/android/launcher2/AppWidgetResizeFrame.java +++ b/src/com/android/launcher2/AppWidgetResizeFrame.java @@ -80,7 +80,7 @@ public class AppWidgetResizeFrame extends FrameLayout { mWorkspace = (Workspace) dragLayer.findViewById(R.id.workspace); final AppWidgetProviderInfo info = widgetView.getAppWidgetInfo(); - int[] result = mLauncher.getMinResizeSpanForWidget(info, null); + int[] result = mLauncher.getMinSpanForWidget(info, null); mMinHSpan = result[0]; mMinVSpan = result[1]; diff --git a/src/com/android/launcher2/AppsCustomizePagedView.java b/src/com/android/launcher2/AppsCustomizePagedView.java index 7f0edde75..840ec459d 100644 --- a/src/com/android/launcher2/AppsCustomizePagedView.java +++ b/src/com/android/launcher2/AppsCustomizePagedView.java @@ -19,6 +19,7 @@ package com.android.launcher2; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; +import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; @@ -33,9 +34,10 @@ import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.Canvas; import android.graphics.MaskFilter; +import android.graphics.Matrix; import android.graphics.Paint; -import android.graphics.PorterDuff; import android.graphics.Rect; +import android.graphics.RectF; import android.graphics.TableMaskFilter; import android.graphics.drawable.Drawable; import android.os.AsyncTask; @@ -75,8 +77,7 @@ interface AsyncTaskCallback { */ class AsyncTaskPageData { enum Type { - LoadWidgetPreviewData, - LoadHolographicIconsData + LoadWidgetPreviewData } AsyncTaskPageData(int p, ArrayList<Object> l, ArrayList<Bitmap> si, AsyncTaskCallback bgR, @@ -167,7 +168,9 @@ class AppsCustomizeAsyncTask extends AsyncTask<AsyncTaskPageData, Void, AsyncTas * The Apps/Customize page that displays all the applications, widgets, and shortcuts. */ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implements - AllAppsView, View.OnClickListener, View.OnKeyListener, DragSource { + AllAppsView, View.OnClickListener, View.OnKeyListener, DragSource, + PagedViewIcon.PressedCallback, PagedViewWidget.ShortPressListener, + LauncherTransitionable { static final String LOG_TAG = "AppsCustomizePagedView"; /** @@ -186,6 +189,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen // Save and Restore private int mSaveInstanceStateItemIndex = -1; + private PagedViewIcon mPressedIcon; // Content private ArrayList<ApplicationInfo> mApps; @@ -200,7 +204,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen private Canvas mCanvas; private Drawable mDefaultWidgetBackground; private IconCache mIconCache; - private int mDragViewMultiplyColor; // Dimens private int mContentWidth; @@ -226,9 +229,23 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen // Previews & outlines ArrayList<AppsCustomizeAsyncTask> mRunningTasks; - private HolographicOutlineHelper mHolographicOutlineHelper; private static final int sPageSleepDelay = 200; + private Runnable mInflateWidgetRunnable = null; + private Runnable mBindWidgetRunnable = null; + static final int WIDGET_NO_CLEANUP_REQUIRED = -1; + static final int WIDGET_BOUND = 0; + static final int WIDGET_INFLATED = 1; + int mWidgetCleanupState = WIDGET_NO_CLEANUP_REQUIRED; + int mWidgetLoadingId = -1; + PendingAddWidgetInfo mCreateWidgetInfo = null; + private boolean mDraggingWidget = false; + + // Deferral of loading widget previews during launcher transitions + private boolean mInTransition; + private ArrayList<AsyncTaskPageData> mDeferredSyncWidgetPageItems = + new ArrayList<AsyncTaskPageData>(); + public AppsCustomizePagedView(Context context, AttributeSet attrs) { super(context, attrs); mLayoutInflater = LayoutInflater.from(context); @@ -236,7 +253,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen mApps = new ArrayList<ApplicationInfo>(); mWidgets = new ArrayList<Object>(); mIconCache = ((LauncherApplication) context.getApplicationContext()).getIconCache(); - mHolographicOutlineHelper = new HolographicOutlineHelper(); mCanvas = new Canvas(); mRunningTasks = new ArrayList<AppsCustomizeAsyncTask>(); @@ -244,7 +260,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen Resources resources = context.getResources(); mDefaultWidgetBackground = resources.getDrawable(R.drawable.default_widget_preview_holo); mAppIconSize = resources.getDimensionPixelSize(R.dimen.app_icon_size); - mDragViewMultiplyColor = resources.getColor(R.color.drag_view_multiply_color); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AppsCustomizePagedView, 0, 0); mMaxAppCellCountX = a.getInt(R.styleable.AppsCustomizePagedView_maxAppCellCountX, -1); @@ -495,12 +510,12 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen if (v instanceof PagedViewIcon) { // Animate some feedback to the click final ApplicationInfo appInfo = (ApplicationInfo) v.getTag(); - animateClickFeedback(v, new Runnable() { - @Override - public void run() { - mLauncher.startActivitySafely(appInfo.intent, appInfo); - } - }); + mLauncher.startActivitySafely(appInfo.intent, appInfo); + + // Lock the drawable state to pressed until we return to Launcher + if (mPressedIcon != null) { + mPressedIcon.lockDrawableState(); + } } else if (v instanceof PagedViewWidget) { // Let the user know that they have to long press to add a widget Toast.makeText(getContext(), R.string.long_press_widget_to_add, @@ -537,7 +552,72 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen mLauncher.getWorkspace().beginDragShared(v, this); } + private void loadWidgetInBackground(final PendingAddWidgetInfo info) { + final AppWidgetProviderInfo pInfo = info.info; + if (pInfo.configure != null) { + return; + } + + mBindWidgetRunnable = new Runnable() { + @Override + public void run() { + mWidgetLoadingId = mLauncher.getAppWidgetHost().allocateAppWidgetId(); + AppWidgetManager.getInstance(mLauncher).bindAppWidgetId(mWidgetLoadingId, + info.componentName); + mWidgetCleanupState = WIDGET_BOUND; + } + }; + post(mBindWidgetRunnable); + + mInflateWidgetRunnable = new Runnable() { + @Override + public void run() { + AppWidgetHostView hostView = + mLauncher.getAppWidgetHost().createView(mContext, mWidgetLoadingId, pInfo); + info.boundWidget = hostView; + mWidgetCleanupState = WIDGET_INFLATED; + hostView.setVisibility(INVISIBLE); + mLauncher.getDragLayer().addView(hostView); + } + }; + post(mInflateWidgetRunnable); + } + + @Override + public void onShortPress(View v) { + // We are anticipating a long press, and we use this time to load bind and instantiate + // the widget. This will need to be cleaned up if it turns out no long press occurs. + mCreateWidgetInfo = new PendingAddWidgetInfo((PendingAddWidgetInfo) v.getTag()); + loadWidgetInBackground(mCreateWidgetInfo); + } + + private void cleanupWidgetPreloading() { + PendingAddWidgetInfo info = mCreateWidgetInfo; + mCreateWidgetInfo = null; + if (mWidgetCleanupState >= 0 && mWidgetLoadingId != -1) { + mLauncher.getAppWidgetHost().deleteAppWidgetId(mWidgetLoadingId); + } + if (mWidgetCleanupState == WIDGET_BOUND) { + removeCallbacks(mInflateWidgetRunnable); + } else if (mWidgetCleanupState == WIDGET_INFLATED) { + AppWidgetHostView widget = info.boundWidget; + int widgetId = widget.getAppWidgetId(); + mLauncher.getAppWidgetHost().deleteAppWidgetId(widgetId); + mLauncher.getDragLayer().removeView(widget); + } + mWidgetCleanupState = WIDGET_NO_CLEANUP_REQUIRED; + mWidgetLoadingId = -1; + } + + @Override + public void cleanUpShortPress(View v) { + if (!mDraggingWidget) { + cleanupWidgetPreloading(); + } + } + private void beginDraggingWidget(View v) { + mDraggingWidget = true; // Get the widget preview as the drag representation ImageView image = (ImageView) v.findViewById(R.id.widget_preview); PendingAddItemInfo createItemInfo = (PendingAddItemInfo) v.getTag(); @@ -545,16 +625,37 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen // Compose the drag image Bitmap preview; Bitmap outline; + float scale = 1f; if (createItemInfo instanceof PendingAddWidgetInfo) { - PendingAddWidgetInfo createWidgetInfo = (PendingAddWidgetInfo) createItemInfo; + PendingAddWidgetInfo createWidgetInfo = mCreateWidgetInfo; + createItemInfo = createWidgetInfo; int[] spanXY = mLauncher.getSpanForWidget(createWidgetInfo, null); + int[] size = mLauncher.getWorkspace().estimateItemSize(spanXY[0], + spanXY[1], createWidgetInfo, true); createItemInfo.spanX = spanXY[0]; createItemInfo.spanY = spanXY[1]; - - int[] maxSize = mLauncher.getWorkspace().estimateItemSize(spanXY[0], spanXY[1], - createWidgetInfo, true); + int[] minSpanXY = mLauncher.getMinSpanForWidget(createWidgetInfo, null); + createWidgetInfo.minSpanX = minSpanXY[0]; + createWidgetInfo.minSpanY = minSpanXY[1]; + + FastBitmapDrawable previewDrawable = (FastBitmapDrawable) image.getDrawable(); + float minScale = 1.25f; + int minWidth, minHeight; + minWidth = Math.max((int) (previewDrawable.getIntrinsicWidth() * minScale), size[0]); + minHeight = Math.max((int) (previewDrawable.getIntrinsicHeight() * minScale), size[1]); preview = getWidgetPreview(createWidgetInfo.componentName, createWidgetInfo.previewImage, - createWidgetInfo.icon, spanXY[0], spanXY[1], maxSize[0], maxSize[1]); + createWidgetInfo.icon, spanXY[0], spanXY[1], minWidth, minHeight); + + // Determine the image view drawable scale relative to the preview + float[] mv = new float[9]; + Matrix m = new Matrix(); + m.setRectToRect( + new RectF(0f, 0f, (float) preview.getWidth(), (float) preview.getHeight()), + new RectF(0f, 0f, (float) previewDrawable.getIntrinsicWidth(), + (float) previewDrawable.getIntrinsicHeight()), + Matrix.ScaleToFit.START); + m.getValues(mv); + scale = (float) mv[0]; } else { // Workaround for the fact that we don't keep the original ResolveInfo associated with // the shortcut around. To get the icon, we just render the preview image (which has @@ -581,51 +682,98 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen // Save the preview for the outline generation, then dim the preview outline = Bitmap.createScaledBitmap(preview, preview.getWidth(), preview.getHeight(), false); - mCanvas.setBitmap(preview); - mCanvas.drawColor(mDragViewMultiplyColor, PorterDuff.Mode.MULTIPLY); - mCanvas.setBitmap(null); // Start the drag alphaClipPaint = null; mLauncher.lockScreenOrientationOnLargeUI(); mLauncher.getWorkspace().onDragStartedWithItem(createItemInfo, outline, alphaClipPaint); mDragController.startDrag(image, preview, this, createItemInfo, - DragController.DRAG_ACTION_COPY, null); + DragController.DRAG_ACTION_COPY, null, scale); outline.recycle(); preview.recycle(); } - @Override - protected boolean beginDragging(View v) { - // Dismiss the cling - mLauncher.dismissAllAppsCling(null); + @Override + protected boolean beginDragging(final View v) { if (!super.beginDragging(v)) return false; - // Go into spring loaded mode (must happen before we startDrag()) - mLauncher.enterSpringLoadedDragMode(); - if (v instanceof PagedViewIcon) { beginDraggingApplication(v); } else if (v instanceof PagedViewWidget) { beginDraggingWidget(v); } + + // We delay entering spring-loaded mode slightly to make sure the UI + // thready is free of any work. + postDelayed(new Runnable() { + @Override + public void run() { + // We don't enter spring-loaded mode if the drag has been cancelled + if (mLauncher.getDragController().isDragging()) { + // Dismiss the cling + mLauncher.dismissAllAppsCling(null); + + // Reset the alpha on the dragged icon before we drag + resetDrawableState(); + + // Go into spring loaded mode (must happen before we startDrag()) + mLauncher.enterSpringLoadedDragMode(); + } + } + }, 150); + return true; } - private void endDragging(View target, boolean success) { + + /** + * Clean up after dragging. + * + * @param target where the item was dragged to (can be null if the item was flung) + */ + private void endDragging(View target, boolean isFlingToDelete, boolean success) { mLauncher.getWorkspace().onDragStopped(success); - if (!success || (target != mLauncher.getWorkspace() && + if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && !(target instanceof DeleteDropTarget))) { // Exit spring loaded mode if we have not successfully dropped or have not handled the // drop in Workspace mLauncher.exitSpringLoadedDragMode(); } mLauncher.unlockScreenOrientationOnLargeUI(); + } + + @Override + public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { + mInTransition = true; + if (toWorkspace) { + cancelAllTasks(); + } + } + + @Override + public View getContent() { + return null; + } + + @Override + public void onLauncherTransitionStep(Launcher l, float t) { + } + @Override + public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { + mInTransition = false; + for (AsyncTaskPageData d : mDeferredSyncWidgetPageItems) { + onSyncWidgetPageItems(d); + } + mDeferredSyncWidgetPageItems.clear(); } @Override - public void onDropCompleted(View target, DragObject d, boolean success) { - endDragging(target, success); + public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, + boolean success) { + // Return early and wait for onFlingToDeleteCompleted if this was the result of a fling + if (isFlingToDelete) return; + + endDragging(target, false, success); // Display an error message if the drag failed due to there not being enough space on the // target layout we were dropping on. @@ -643,9 +791,26 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen } } if (showOutOfSpaceMessage) { - mLauncher.showOutOfSpaceMessage(); + mLauncher.showOutOfSpaceMessage(false); } + + d.deferDragViewCleanupPostAnimation = false; + cleanupWidgetPreloading(); } + mDraggingWidget = false; + } + + @Override + public void onFlingToDeleteCompleted() { + // We just dismiss the drag when we fling, so cleanup here + endDragging(null, true, true); + cleanupWidgetPreloading(); + mDraggingWidget = false; + } + + @Override + public boolean supportsFlingToDelete() { + return true; } @Override @@ -673,6 +838,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); task.cancel(false); iter.remove(); + mDirtyPageContent.set(task.page, true); } } @@ -692,7 +858,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); while (iter.hasNext()) { AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - int pageIndex = task.page + mNumAppsPages; + int pageIndex = task.page; if ((mNextPage > mCurrentPage && pageIndex >= mCurrentPage) || (mNextPage < mCurrentPage && pageIndex <= mCurrentPage)) { task.setThreadPriority(getThreadPriorityForPage(pageIndex)); @@ -759,7 +925,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen ApplicationInfo info = mApps.get(i); PagedViewIcon icon = (PagedViewIcon) mLayoutInflater.inflate( R.layout.apps_customize_application, layout, false); - icon.applyFromApplicationInfo(info, true, mHolographicOutlineHelper); + icon.applyFromApplicationInfo(info, true, this); icon.setOnClickListener(this); icon.setOnLongClickListener(this); icon.setOnTouchListener(this); @@ -775,12 +941,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen } layout.createHardwareLayers(); - - /* TEMPORARILY DISABLE HOLOGRAPHIC ICONS - if (mFadeInAdjacentScreens) { - prepareGenerateHoloOutlinesTask(page, items, images); - } - */ } /** @@ -800,7 +960,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen int minPageDiff = Integer.MAX_VALUE; while (iter.hasNext()) { AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - minPageDiff = Math.abs(task.page + mNumAppsPages - toPage); + minPageDiff = Math.abs(task.page - toPage); } int rawPageDiff = Math.abs(page - toPage); @@ -835,7 +995,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); while (iter.hasNext()) { AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - int taskPage = task.page + mNumAppsPages; + int taskPage = task.page; if (taskPage < getAssociatedLowerPageBound(mCurrentPage) || taskPage > getAssociatedUpperPageBound(mCurrentPage)) { task.cancel(false); @@ -846,7 +1006,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen } // We introduce a slight delay to order the loading of side pages so that we don't thrash - final int sleepMs = getSleepForPage(page + mNumAppsPages); + final int sleepMs = getSleepForPage(page); AsyncTaskPageData pageData = new AsyncTaskPageData(page, widgets, cellWidth, cellHeight, new AsyncTaskCallback() { @Override @@ -866,96 +1026,20 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen new AsyncTaskCallback() { @Override public void run(AppsCustomizeAsyncTask task, AsyncTaskPageData data) { - try { - mRunningTasks.remove(task); - if (task.isCancelled()) return; - onSyncWidgetPageItems(data); - } finally { - data.cleanup(task.isCancelled()); - } + mRunningTasks.remove(task); + if (task.isCancelled()) return; + // do cleanup inside onSyncWidgetPageItems + onSyncWidgetPageItems(data); } }); // Ensure that the task is appropriately prioritized and runs in parallel AppsCustomizeAsyncTask t = new AppsCustomizeAsyncTask(page, AsyncTaskPageData.Type.LoadWidgetPreviewData); - t.setThreadPriority(getThreadPriorityForPage(page + mNumAppsPages)); + t.setThreadPriority(getThreadPriorityForPage(page)); t.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, pageData); mRunningTasks.add(t); } - /** - * Creates and executes a new AsyncTask to load the outlines for a page of content. - */ - private void prepareGenerateHoloOutlinesTask(int page, ArrayList<Object> items, - ArrayList<Bitmap> images) { - // Prune old tasks for this page - Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); - while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - int taskPage = task.page; - if ((taskPage == page) && - (task.dataType == AsyncTaskPageData.Type.LoadHolographicIconsData)) { - task.cancel(false); - iter.remove(); - } - } - - AsyncTaskPageData pageData = new AsyncTaskPageData(page, items, images, - new AsyncTaskCallback() { - @Override - public void run(AppsCustomizeAsyncTask task, AsyncTaskPageData data) { - try { - // Ensure that this task starts running at the correct priority - task.syncThreadPriority(); - - ArrayList<Bitmap> images = data.generatedImages; - ArrayList<Bitmap> srcImages = data.sourceImages; - int count = srcImages.size(); - Canvas c = new Canvas(); - for (int i = 0; i < count && !task.isCancelled(); ++i) { - // Before work on each item, ensure that this task is running at the correct - // priority - task.syncThreadPriority(); - - Bitmap b = srcImages.get(i); - Bitmap outline = Bitmap.createBitmap(b.getWidth(), b.getHeight(), - Bitmap.Config.ARGB_8888); - - c.setBitmap(outline); - c.save(); - c.drawBitmap(b, 0, 0, null); - c.restore(); - c.setBitmap(null); - - images.add(outline); - } - } finally { - if (task.isCancelled()) { - data.cleanup(true); - } - } - } - }, - new AsyncTaskCallback() { - @Override - public void run(AppsCustomizeAsyncTask task, AsyncTaskPageData data) { - try { - mRunningTasks.remove(task); - if (task.isCancelled()) return; - onHolographicPageItemsLoaded(data); - } finally { - data.cleanup(task.isCancelled()); - } - } - }); - - // Ensure that the outline task always runs in the background, serially - AppsCustomizeAsyncTask t = - new AppsCustomizeAsyncTask(page, AsyncTaskPageData.Type.LoadHolographicIconsData); - t.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - t.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, pageData); - mRunningTasks.add(t); - } /* * Widgets PagedView implementation @@ -985,9 +1069,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen d.setBounds(x, y, x + w, y + h); d.draw(c); d.setBounds(oldBounds); // Restore the bounds - if (multiplyColor != 0xFFFFFFFF) { - c.drawColor(mDragViewMultiplyColor, PorterDuff.Mode.MULTIPLY); - } c.setBitmap(null); } } @@ -1025,11 +1106,9 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen if (widgetPreviewExists) { bitmapWidth = drawable.getIntrinsicWidth(); bitmapHeight = drawable.getIntrinsicHeight(); - - // Cap the size so widget previews don't appear larger than the actual widget - maxWidth = Math.min(maxWidth, mWidgetSpacingLayout.estimateCellWidth(cellHSpan)); - maxHeight = Math.min(maxHeight, mWidgetSpacingLayout.estimateCellHeight(cellVSpan)); } else { + if (cellHSpan < 1) cellHSpan = 1; + if (cellVSpan < 1) cellVSpan = 1; // Determine the size of the bitmap for the preview image we will generate // TODO: This actually uses the apps customize cell layout params, where as we make want // the Workspace params for more accuracy. @@ -1102,13 +1181,13 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen - ((mWidgetCountY - 1) * mWidgetHeightGap)) / mWidgetCountY); // Prepare the set of widgets to load previews for in the background - int offset = page * numItemsPerPage; + int offset = (page - mNumAppsPages) * numItemsPerPage; for (int i = offset; i < Math.min(offset + numItemsPerPage, mWidgets.size()); ++i) { items.add(mWidgets.get(i)); } // Prepopulate the pages with the other widget info, and fill in the previews later - final PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(page + mNumAppsPages); + final PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(page); layout.setColumnCount(layout.getCellCountX()); for (int i = 0; i < items.size(); ++i) { Object rawInfo = items.get(i); @@ -1120,9 +1199,9 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen AppWidgetProviderInfo info = (AppWidgetProviderInfo) rawInfo; createItemInfo = new PendingAddWidgetInfo(info, null, null); int[] cellSpans = mLauncher.getSpanForWidget(info, null); - widget.applyFromAppWidgetProviderInfo(info, -1, cellSpans, - mHolographicOutlineHelper); + widget.applyFromAppWidgetProviderInfo(info, -1, cellSpans); widget.setTag(createItemInfo); + widget.setShortPressListener(this); } else if (rawInfo instanceof ResolveInfo) { // Fill in the shortcuts information ResolveInfo info = (ResolveInfo) rawInfo; @@ -1130,7 +1209,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen createItemInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; createItemInfo.componentName = new ComponentName(info.activityInfo.packageName, info.activityInfo.name); - widget.applyFromResolveInfo(mPackageManager, info, mHolographicOutlineHelper); + widget.applyFromResolveInfo(mPackageManager, info); widget.setTag(createItemInfo); } widget.setOnClickListener(this); @@ -1204,8 +1283,13 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen if (rawInfo instanceof AppWidgetProviderInfo) { AppWidgetProviderInfo info = (AppWidgetProviderInfo) rawInfo; int[] cellSpans = mLauncher.getSpanForWidget(info, null); + + int maxWidth = Math.min(data.maxImageWidth, + mWidgetSpacingLayout.estimateCellWidth(cellSpans[0])); + int maxHeight = Math.min(data.maxImageHeight, + mWidgetSpacingLayout.estimateCellHeight(cellSpans[1])); Bitmap b = getWidgetPreview(info.provider, info.previewImage, info.icon, - cellSpans[0], cellSpans[1], data.maxImageWidth, data.maxImageHeight); + cellSpans[0], cellSpans[1], maxWidth, maxHeight); images.add(b); } else if (rawInfo instanceof ResolveInfo) { // Fill in the shortcuts information @@ -1214,58 +1298,38 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen } } } - private void onSyncWidgetPageItems(AsyncTaskPageData data) { - int page = data.page; - PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(page + mNumAppsPages); - ArrayList<Object> items = data.items; - int count = items.size(); - for (int i = 0; i < count; ++i) { - PagedViewWidget widget = (PagedViewWidget) layout.getChildAt(i); - if (widget != null) { - Bitmap preview = data.generatedImages.get(i); - widget.applyPreview(new FastBitmapDrawable(preview), i); - } - } - - layout.createHardwareLayer(); - invalidate(); - - /* TEMPORARILY DISABLE HOLOGRAPHIC ICONS - if (mFadeInAdjacentScreens) { - prepareGenerateHoloOutlinesTask(data.page, data.items, data.generatedImages); - } - */ - - // Update all thread priorities - Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); - while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - int pageIndex = task.page + mNumAppsPages; - task.setThreadPriority(getThreadPriorityForPage(pageIndex)); + private void onSyncWidgetPageItems(AsyncTaskPageData data) { + if (mInTransition) { + mDeferredSyncWidgetPageItems.add(data); + return; } - } - private void onHolographicPageItemsLoaded(AsyncTaskPageData data) { - // Invalidate early to short-circuit children invalidates - invalidate(); + try { + int page = data.page; + PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(page); - int page = data.page; - ViewGroup layout = (ViewGroup) getPageAt(page); - if (layout instanceof PagedViewCellLayout) { - PagedViewCellLayout cl = (PagedViewCellLayout) layout; - int count = cl.getPageChildCount(); - if (count != data.generatedImages.size()) return; + ArrayList<Object> items = data.items; + int count = items.size(); for (int i = 0; i < count; ++i) { - PagedViewIcon icon = (PagedViewIcon) cl.getChildOnPageAt(i); - icon.setHolographicOutline(data.generatedImages.get(i)); + PagedViewWidget widget = (PagedViewWidget) layout.getChildAt(i); + if (widget != null) { + Bitmap preview = data.generatedImages.get(i); + widget.applyPreview(new FastBitmapDrawable(preview), i); + } } - } else { - int count = layout.getChildCount(); - if (count != data.generatedImages.size()) return; - for (int i = 0; i < count; ++i) { - View v = layout.getChildAt(i); - ((PagedViewWidget) v).setHolographicOutline(data.generatedImages.get(i)); + + layout.createHardwareLayer(); + invalidate(); + + // Update all thread priorities + Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); + while (iter.hasNext()) { + AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); + int pageIndex = task.page; + task.setThreadPriority(getThreadPriorityForPage(pageIndex)); } + } finally { + data.cleanup(false); } } @@ -1279,7 +1343,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen PagedViewGridLayout layout = new PagedViewGridLayout(context, mWidgetCountX, mWidgetCountY); setupPage(layout); - addView(layout, new PagedViewGridLayout.LayoutParams(LayoutParams.MATCH_PARENT, + addView(layout, new PagedView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } @@ -1295,14 +1359,14 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen if (page < mNumAppsPages) { syncAppsPageItems(page, immediate); } else { - syncWidgetPageItems(page - mNumAppsPages, immediate); + syncWidgetPageItems(page, immediate); } } // We want our pages to be z-ordered such that the further a page is to the left, the higher // it is in the z-order. This is important to insure touch events are handled correctly. View getPageAt(int index) { - return getChildAt(getChildCount() - index - 1); + return getChildAt(indexToPage(index)); } @Override @@ -1490,6 +1554,9 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen @Override public void reset() { + // If we have reset, then we should not continue to restore the previous state + mSaveInstanceStateItemIndex = -1; + AppsCustomizeTabHost tabHost = getTabHost(); String tag = tabHost.getCurrentTabTag(); if (tag != null) { @@ -1497,6 +1564,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen tabHost.setCurrentTabFromContent(ContentType.Applications); } } + if (mCurrentPage != 0) { invalidatePageData(0); } @@ -1540,6 +1608,22 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen cancelAllTasks(); } + @Override + public void iconPressed(PagedViewIcon icon) { + // Reset the previously pressed icon and store a reference to the pressed icon so that + // we can reset it on return to Launcher (in Launcher.onResume()) + if (mPressedIcon != null) { + mPressedIcon.resetDrawableState(); + } + mPressedIcon = icon; + } + + public void resetDrawableState() { + if (mPressedIcon != null) { + mPressedIcon.resetDrawableState(); + mPressedIcon = null; + } + } /* * We load an extra page on each side to prevent flashes from scrolling and loading of the diff --git a/src/com/android/launcher2/AppsCustomizeTabHost.java b/src/com/android/launcher2/AppsCustomizeTabHost.java index 2963240fa..0199d01c6 100644 --- a/src/com/android/launcher2/AppsCustomizeTabHost.java +++ b/src/com/android/launcher2/AppsCustomizeTabHost.java @@ -53,12 +53,19 @@ public class AppsCustomizeTabHost extends TabHost implements LauncherTransitiona private LinearLayout mContent; private boolean mInTransition; + private boolean mTransitioningToWorkspace; private boolean mResetAfterTransition; - private Animator mLauncherTransition; + private Runnable mRelayoutAndMakeVisible; public AppsCustomizeTabHost(Context context, AttributeSet attrs) { super(context, attrs); mLayoutInflater = LayoutInflater.from(context); + mRelayoutAndMakeVisible = new Runnable() { + public void run() { + mTabs.requestLayout(); + mTabsContainer.setAlpha(1f); + } + }; } /** @@ -144,19 +151,29 @@ public class AppsCustomizeTabHost extends TabHost implements LauncherTransitiona if (contentWidth > 0 && mTabs.getLayoutParams().width != contentWidth) { // Set the width and show the tab bar mTabs.getLayoutParams().width = contentWidth; - post(new Runnable() { - public void run() { - mTabs.requestLayout(); - mTabsContainer.setAlpha(1f); - } - }); + post(mRelayoutAndMakeVisible); } } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } + public boolean onInterceptTouchEvent(MotionEvent ev) { + // If we are mid transition then intercept touch events here so we can ignore them + if (mInTransition) { + return true; + } + return super.onInterceptTouchEvent(ev); + }; + @Override public boolean onTouchEvent(MotionEvent event) { + // Allow touch events to fall through if we are transitioning to the workspace + if (mInTransition) { + if (mTransitioningToWorkspace) { + return super.onTouchEvent(event); + } + } + // Intercept all touch events up to the bottom of the AppsCustomizePane so they do not fall // through to the workspace and trigger showWorkspace() if (event.getY() < mAppsCustomizePane.getBottom()) { @@ -238,8 +255,8 @@ public class AppsCustomizeTabHost extends TabHost implements LauncherTransitiona PagedViewWidget.setDeletePreviewsWhenDetachedFromWindow(true); mAnimationBuffer.setAlpha(1f); mAnimationBuffer.setVisibility(View.VISIBLE); - LayoutParams p = new FrameLayout.LayoutParams(child.getWidth(), - child.getHeight()); + LayoutParams p = new FrameLayout.LayoutParams(child.getMeasuredWidth(), + child.getMeasuredHeight()); p.setMargins((int) child.getLeft(), (int) child.getTop(), 0, 0); mAnimationBuffer.addView(child, p); } @@ -338,61 +355,72 @@ public class AppsCustomizeTabHost extends TabHost implements LauncherTransitiona // force building the layer, so you don't get a blip early in an animation // when the layer is created layer buildLayer(); + + // Let the GC system know that now is a good time to do any garbage + // collection; makes it less likely we'll get a GC during the all apps + // to workspace animation + System.gc(); } } - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - if (mLauncherTransition != null) { - enableAndBuildHardwareLayer(); - mLauncherTransition.start(); - mLauncherTransition = null; - } + @Override + public View getContent() { + return mContent; } /* LauncherTransitionable overrides */ @Override - public boolean onLauncherTransitionStart(Launcher l, Animator animation, boolean toWorkspace) { + public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { + mAppsCustomizePane.onLauncherTransitionStart(l, animated, toWorkspace); mInTransition = true; - boolean delayLauncherTransitionUntilLayout = false; - boolean animated = (animation != null); - mLauncherTransition = null; - - // if the content wasn't visible before, delay the launcher animation until after a call - // to layout -- this prevents a blip - if (animated && mContent.getVisibility() == GONE) { - mLauncherTransition = animation; - delayLauncherTransitionUntilLayout = true; - } - mContent.setVisibility(VISIBLE); + mTransitioningToWorkspace = toWorkspace; + + if (toWorkspace) { + // Going from All Apps -> Workspace + setVisibilityOfSiblingsWithLowerZOrder(VISIBLE); + // Stop the scrolling indicator - we don't want All Apps to be invalidating itself + // during the transition, especially since it has a hardware layer set on it + mAppsCustomizePane.cancelScrollingIndicatorAnimations(); + } else { + // Going from Workspace -> All Apps + mContent.setVisibility(VISIBLE); - if (!toWorkspace) { // Make sure the current page is loaded (we start loading the side pages after the // transition to prevent slowing down the animation) mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage(), true); - } - if (animated && !delayLauncherTransitionUntilLayout) { - enableAndBuildHardwareLayer(); - } - if (!toWorkspace && !LauncherApplication.isScreenLarge()) { - mAppsCustomizePane.showScrollingIndicator(false); + if (!LauncherApplication.isScreenLarge()) { + mAppsCustomizePane.showScrollingIndicator(true); + } } + if (mResetAfterTransition) { mAppsCustomizePane.reset(); mResetAfterTransition = false; } - return delayLauncherTransitionUntilLayout; + + if (animated) { + enableAndBuildHardwareLayer(); + } + } + + @Override + public void onLauncherTransitionStep(Launcher l, float t) { + // Do nothing } @Override - public void onLauncherTransitionEnd(Launcher l, Animator animation, boolean toWorkspace) { + public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { + mAppsCustomizePane.onLauncherTransitionEnd(l, animated, toWorkspace); mInTransition = false; - if (animation != null) { + if (animated) { setLayerType(LAYER_TYPE_NONE, null); } if (!toWorkspace) { + // Going from Workspace -> All Apps + setVisibilityOfSiblingsWithLowerZOrder(INVISIBLE); + // Dismiss the workspace cling and show the all apps cling (if not already shown) l.dismissWorkspaceCling(null); mAppsCustomizePane.showAllAppsCling(); @@ -406,7 +434,27 @@ public class AppsCustomizeTabHost extends TabHost implements LauncherTransitiona } } - public void onResume() { + private void setVisibilityOfSiblingsWithLowerZOrder(int visibility) { + ViewGroup parent = (ViewGroup) getParent(); + final int count = parent.getChildCount(); + if (!isChildrenDrawingOrderEnabled()) { + for (int i = 0; i < count; i++) { + final View child = parent.getChildAt(i); + if (child == this) { + break; + } else { + if (child.getVisibility() == GONE) { + continue; + } + child.setVisibility(visibility); + } + } + } else { + throw new RuntimeException("Failed; can't get z-order of views"); + } + } + + public void onWindowVisible() { if (getVisibility() == VISIBLE) { mContent.setVisibility(VISIBLE); // We unload the widget previews when the UI is hidden, so need to reload pages diff --git a/src/com/android/launcher2/BubbleTextView.java b/src/com/android/launcher2/BubbleTextView.java index dc498a403..a775ed5ab 100644 --- a/src/com/android/launcher2/BubbleTextView.java +++ b/src/com/android/launcher2/BubbleTextView.java @@ -47,8 +47,6 @@ public class BubbleTextView extends TextView { static final float PADDING_H = 8.0f; static final float PADDING_V = 3.0f; - private Paint mPaint; - private float mBubbleColorAlpha; private int mPrevAlpha = -1; private final HolographicOutlineHelper mOutlineHelper = new HolographicOutlineHelper(); @@ -65,6 +63,7 @@ public class BubbleTextView extends TextView { private Drawable mBackground; private boolean mStayPressed; + private CheckLongPressHelper mLongPressHelper; public BubbleTextView(Context context) { super(context); @@ -82,13 +81,11 @@ public class BubbleTextView extends TextView { } private void init() { + mLongPressHelper = new CheckLongPressHelper(this); mBackground = getBackground(); final Resources res = getContext().getResources(); int bubbleColor = res.getColor(R.color.bubble_dark_background); - mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mPaint.setColor(bubbleColor); - mBubbleColorAlpha = Color.alpha(bubbleColor) / 255.0f; mFocusedOutlineColor = mFocusedGlowColor = mPressedOutlineColor = mPressedGlowColor = res.getColor(android.R.color.holo_blue_light); @@ -134,7 +131,7 @@ public class BubbleTextView extends TextView { mPressedOrFocusedBackground = null; } if (isFocused()) { - if (mLayout == null) { + if (getLayout() == null) { // In some cases, we get focus before we have been layed out. Set the // background to null so that it will get created when the view is drawn. mPressedOrFocusedBackground = null; @@ -176,6 +173,8 @@ public class BubbleTextView extends TextView { // The translate of scrollX and scrollY is necessary when drawing TextViews, because // they set scrollX and scrollY to large values to achieve centered text destCanvas.save(); + destCanvas.scale(getScaleX(), getScaleY(), + (getWidth() + padding) / 2, (getHeight() + padding) / 2); destCanvas.translate(-getScrollX() + padding / 2, -getScrollY() + padding / 2); destCanvas.clipRect(clipRect, Op.REPLACE); draw(destCanvas); @@ -222,6 +221,8 @@ public class BubbleTextView extends TextView { } else { mDidInvalidateForPressedState = false; } + + mLongPressHelper.postCheckForLongPress(); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: @@ -230,6 +231,8 @@ public class BubbleTextView extends TextView { if (!isPressed()) { mPressedOrFocusedBackground = null; } + + mLongPressHelper.cancelLongPress(); break; } return result; @@ -244,8 +247,8 @@ public class BubbleTextView extends TextView { } void setCellLayoutPressedOrFocusedIcon() { - if (getParent() instanceof CellLayoutChildren) { - CellLayoutChildren parent = (CellLayoutChildren) getParent(); + if (getParent() instanceof ShortcutAndWidgetContainer) { + ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) getParent(); if (parent != null) { CellLayout layout = (CellLayout) parent.getParent(); layout.setPressedOrFocusedIcon((mPressedOrFocusedBackground != null) ? this : null); @@ -286,6 +289,14 @@ public class BubbleTextView extends TextView { canvas.translate(-scrollX, -scrollY); } } + + // If text is transparent, don't draw any shadow + if (getCurrentTextColor() == getResources().getColor(android.R.color.transparent)) { + getPaint().clearShadowLayer(); + super.draw(canvas); + return; + } + // We enhance the shadow by drawing the shadow twice getPaint().setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR); super.draw(canvas); @@ -313,9 +324,15 @@ public class BubbleTextView extends TextView { protected boolean onSetAlpha(int alpha) { if (mPrevAlpha != alpha) { mPrevAlpha = alpha; - mPaint.setAlpha((int) (alpha * mBubbleColorAlpha)); super.onSetAlpha(alpha); } return true; } + + @Override + public void cancelLongPress() { + super.cancelLongPress(); + + mLongPressHelper.cancelLongPress(); + } } diff --git a/src/com/android/launcher2/ButtonDropTarget.java b/src/com/android/launcher2/ButtonDropTarget.java index 4ff7c9669..e9f8ce8b5 100644 --- a/src/com/android/launcher2/ButtonDropTarget.java +++ b/src/com/android/launcher2/ButtonDropTarget.java @@ -18,7 +18,8 @@ package com.android.launcher2; import android.content.Context; import android.content.res.Resources; -import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.Rect; import android.util.AttributeSet; import android.widget.TextView; @@ -41,7 +42,7 @@ public class ButtonDropTarget extends TextView implements DropTarget, DragContro protected boolean mActive; /** The paint applied to the drag view on hover */ - protected final Paint mHoverPaint = new Paint(); + protected int mHoverColor = 0; public ButtonDropTarget(Context context, AttributeSet attrs) { this(context, attrs, 0); @@ -70,8 +71,12 @@ public class ButtonDropTarget extends TextView implements DropTarget, DragContro public void onDrop(DragObject d) { } + public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { + // Do nothing + } + public void onDragEnter(DragObject d) { - d.dragView.setPaint(mHoverPaint); + d.dragView.setColor(mHoverColor); } public void onDragOver(DragObject d) { @@ -79,7 +84,7 @@ public class ButtonDropTarget extends TextView implements DropTarget, DragContro } public void onDragExit(DragObject d) { - d.dragView.setPaint(null); + d.dragView.setColor(0); } public void onDragStart(DragSource source, Object info, int dragAction) { @@ -100,6 +105,26 @@ public class ButtonDropTarget extends TextView implements DropTarget, DragContro outRect.bottom += mBottomDragPadding; } + Rect getIconRect(int itemWidth, int itemHeight, int drawableWidth, int drawableHeight) { + DragLayer dragLayer = mLauncher.getDragLayer(); + + // Find the rect to animate to (the view is center aligned) + Rect to = new Rect(); + dragLayer.getViewRectRelativeToSelf(this, to); + int width = drawableWidth; + int height = drawableHeight; + int left = to.left + getPaddingLeft(); + int top = to.top + (getMeasuredHeight() - height) / 2; + to.set(left, top, left + width, top + height); + + // Center the destination rect about the trash icon + int xOffset = (int) -(itemWidth - width) / 2; + int yOffset = (int) -(itemHeight - height) / 2; + to.offset(xOffset, yOffset); + + return to; + } + @Override public DropTarget getDropTargetDelegate(DragObject d) { return null; diff --git a/src/com/android/launcher2/CellLayout.java b/src/com/android/launcher2/CellLayout.java index 8aae809b9..199c41a59 100644 --- a/src/com/android/launcher2/CellLayout.java +++ b/src/com/android/launcher2/CellLayout.java @@ -18,8 +18,6 @@ package com.android.launcher2; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.PropertyValuesHolder; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; @@ -28,13 +26,14 @@ 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.PointF; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; -import android.graphics.RectF; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.NinePatchDrawable; import android.util.AttributeSet; @@ -53,10 +52,12 @@ import com.android.launcher2.FolderIcon.FolderRingAnimator; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.Stack; public class CellLayout extends ViewGroup { static final String TAG = "CellLayout"; + private Launcher mLauncher; private int mOriginalCellWidth; private int mOriginalCellHeight; private int mCellWidth; @@ -83,6 +84,7 @@ public class CellLayout extends ViewGroup { int[] mTempLocation = new int[2]; boolean[][] mOccupied; + boolean[][] mTmpOccupied; private boolean mLastDownOnOccupiedCell = false; private OnTouchListener mInterceptTouchListener; @@ -109,7 +111,7 @@ public class CellLayout extends ViewGroup { // These arrays are used to implement the drag visualization on x-large screens. // They are used as circular arrays, indexed by mDragOutlineCurrent. - private Point[] mDragOutlines = new Point[4]; + private Rect[] mDragOutlines = new Rect[4]; private float[] mDragOutlineAlphas = new float[mDragOutlines.length]; private InterruptibleInOutAnimator[] mDragOutlineAnims = new InterruptibleInOutAnimator[mDragOutlines.length]; @@ -124,8 +126,12 @@ public class CellLayout extends ViewGroup { private InterruptibleInOutAnimator mCrosshairsAnimator = null; private float mCrosshairsVisibility = 0.0f; - private HashMap<CellLayout.LayoutParams, ObjectAnimator> mReorderAnimators = new - HashMap<CellLayout.LayoutParams, ObjectAnimator>(); + 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]; @@ -133,7 +139,28 @@ public class CellLayout extends ViewGroup { private boolean mDragging = false; private TimeInterpolator mEaseOutInterpolator; - private CellLayoutChildren mChildren; + private ShortcutAndWidgetContainer mShortcutsAndWidgets; + + private boolean mIsHotseat = false; + private float mChildScale = 1f; + private float mHotseatChildScale = 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; + + private static final float REORDER_HINT_MAGNITUDE = 0.27f; + 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; public CellLayout(Context context) { this(context, null); @@ -149,6 +176,7 @@ public class CellLayout extends ViewGroup { // 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); + mLauncher = (Launcher) context; TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CellLayout, defStyle, 0); @@ -162,6 +190,9 @@ public class CellLayout extends ViewGroup { 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(); @@ -177,9 +208,21 @@ public class CellLayout extends ViewGroup { 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); + int iconScale = res.getInteger(R.integer.app_icon_scale_percent); + if (iconScale >= 0) { + mChildScale = iconScale / 100f; + } + int hotseatIconScale = res.getInteger(R.integer.app_icon_hotseat_scale_percent); + if (hotseatIconScale >= 0) { + mHotseatChildScale = hotseatIconScale / 100f; + } + // Initialize the data structures used for the drag visualization. mCrosshairsDrawable = res.getDrawable(R.drawable.gardening_crosshairs); @@ -198,7 +241,7 @@ public class CellLayout extends ViewGroup { mDragCell[0] = mDragCell[1] = -1; for (int i = 0; i < mDragOutlines.length; i++) { - mDragOutlines[i] = new Point(-1, -1); + mDragOutlines[i] = new Rect(-1, -1, -1, -1); } // When dragging things around the home screens, we show a green outline of @@ -232,10 +275,7 @@ public class CellLayout extends ViewGroup { animation.cancel(); } else { mDragOutlineAlphas[thisIndex] = (Float) animation.getAnimatedValue(); - final int left = mDragOutlines[thisIndex].x; - final int top = mDragOutlines[thisIndex].y; - CellLayout.this.invalidate(left, top, - left + outline.getWidth(), top + outline.getHeight()); + CellLayout.this.invalidate(mDragOutlines[thisIndex]); } } }); @@ -255,9 +295,9 @@ public class CellLayout extends ViewGroup { mBackgroundRect = new Rect(); mForegroundRect = new Rect(); - mChildren = new CellLayoutChildren(context); - mChildren.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap); - addView(mChildren); + mShortcutsAndWidgets = new ShortcutAndWidgetContainer(context); + mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap); + addView(mShortcutsAndWidgets); } static int widthInPortrait(Resources r, int numCells) { @@ -283,13 +323,15 @@ public class CellLayout extends ViewGroup { } public void enableHardwareLayers() { - mChildren.enableHardwareLayers(); + mShortcutsAndWidgets.enableHardwareLayers(); } public void setGridSize(int x, int y) { mCountX = x; mCountY = y; mOccupied = new boolean[mCountX][mCountY]; + mTmpOccupied = new boolean[mCountX][mCountY]; + mTempRectStack.clear(); requestLayout(); } @@ -326,13 +368,6 @@ public class CellLayout extends ViewGroup { } } - public CellLayoutChildren getChildrenLayout() { - if (getChildCount() > 0) { - return (CellLayoutChildren) getChildAt(0); - } - return null; - } - void setIsDragOverlapping(boolean isDragOverlapping) { if (mIsDragOverlapping != isDragOverlapping) { mIsDragOverlapping = isDragOverlapping; @@ -419,10 +454,10 @@ public class CellLayout extends ViewGroup { for (int i = 0; i < mDragOutlines.length; i++) { final float alpha = mDragOutlineAlphas[i]; if (alpha > 0) { - final Point p = mDragOutlines[i]; + final Rect r = mDragOutlines[i]; final Bitmap b = (Bitmap) mDragOutlineAnims[i].getTag(); paint.setAlpha((int)(alpha + .5f)); - canvas.drawBitmap(b, p.x, p.y, paint); + canvas.drawBitmap(b, null, r, paint); } } @@ -439,6 +474,23 @@ public class CellLayout extends ViewGroup { } } + if (DEBUG_VISUALIZE_OCCUPIED) { + int[] pt = new int[2]; + ColorDrawable cd = new ColorDrawable(Color.RED); + cd.setBounds(0, 0, 80, 80); + 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(); + } + } + } + } + // The folder outer / inner ring image(s) for (int i = 0; i < mFolderOuterRings.size(); i++) { FolderRingAnimator fra = mFolderOuterRings.get(i); @@ -554,10 +606,63 @@ public class CellLayout extends ViewGroup { return mCountY; } + public void setIsHotseat(boolean isHotseat) { + mIsHotseat = isHotseat; + } + + public float getChildrenScale() { + return mIsHotseat ? mHotseatChildScale : mChildScale; + } + public boolean addViewToCellLayout( View child, int index, int childId, LayoutParams params, boolean markCells) { + return addViewToCellLayout(child, index, childId, params, markCells, false); + } + + private void scaleChild(BubbleTextView bubbleChild, float pivot, float scale) { + // If we haven't measured the child yet, do it now + // (this happens if we're being dropped from all-apps + if (bubbleChild.getLayoutParams() instanceof LayoutParams && + (bubbleChild.getMeasuredWidth() | bubbleChild.getMeasuredHeight()) == 0) { + getShortcutsAndWidgets().measureChild(bubbleChild); + } + + bubbleChild.setScaleX(scale); + bubbleChild.setScaleY(scale); + } + + private void resetChild(BubbleTextView bubbleChild) { + bubbleChild.setScaleX(1f); + bubbleChild.setScaleY(1f); + + bubbleChild.setTextColor(getResources().getColor(R.color.workspace_icon_text_color)); + } + + public boolean addViewToCellLayout(View child, int index, int childId, LayoutParams params, + boolean markCells, boolean allApps) { final LayoutParams lp = params; + // Hotseat icons - scale down and remove text + // Don't scale the all apps button + // scale percent set to -1 means do not scale + // Only scale BubbleTextViews + if (child instanceof BubbleTextView) { + BubbleTextView bubbleChild = (BubbleTextView) child; + + // Start the child with 100% scale and visible text + resetChild(bubbleChild); + + if (mIsHotseat && !allApps && mHotseatChildScale >= 0) { + // Scale/make transparent for a hotseat + scaleChild(bubbleChild, 0f, mHotseatChildScale); + + bubbleChild.setTextColor(getResources().getColor(android.R.color.transparent)); + } else if (mChildScale >= 0) { + // Else possibly still scale it if we need to for smaller icons + scaleChild(bubbleChild, 0f, mChildScale); + } + } + // 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) { @@ -568,7 +673,7 @@ public class CellLayout extends ViewGroup { child.setId(childId); - mChildren.addView(child, index, lp); + mShortcutsAndWidgets.addView(child, index, lp); if (markCells) markCellsAsOccupiedForView(child); @@ -580,61 +685,53 @@ public class CellLayout extends ViewGroup { @Override public void removeAllViews() { clearOccupiedCells(); - mChildren.removeAllViews(); + mShortcutsAndWidgets.removeAllViews(); } @Override public void removeAllViewsInLayout() { - if (mChildren.getChildCount() > 0) { + if (mShortcutsAndWidgets.getChildCount() > 0) { clearOccupiedCells(); - mChildren.removeAllViewsInLayout(); + mShortcutsAndWidgets.removeAllViewsInLayout(); } } public void removeViewWithoutMarkingCells(View view) { - mChildren.removeView(view); + mShortcutsAndWidgets.removeView(view); } @Override public void removeView(View view) { markCellsAsUnoccupiedForView(view); - mChildren.removeView(view); + mShortcutsAndWidgets.removeView(view); } @Override public void removeViewAt(int index) { - markCellsAsUnoccupiedForView(mChildren.getChildAt(index)); - mChildren.removeViewAt(index); + markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(index)); + mShortcutsAndWidgets.removeViewAt(index); } @Override public void removeViewInLayout(View view) { markCellsAsUnoccupiedForView(view); - mChildren.removeViewInLayout(view); + mShortcutsAndWidgets.removeViewInLayout(view); } @Override public void removeViews(int start, int count) { for (int i = start; i < start + count; i++) { - markCellsAsUnoccupiedForView(mChildren.getChildAt(i)); + markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(i)); } - mChildren.removeViews(start, count); + mShortcutsAndWidgets.removeViews(start, count); } @Override public void removeViewsInLayout(int start, int count) { for (int i = start; i < start + count; i++) { - markCellsAsUnoccupiedForView(mChildren.getChildAt(i)); + markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(i)); } - mChildren.removeViewsInLayout(start, count); - } - - public void drawChildren(Canvas canvas) { - mChildren.draw(canvas); - } - - void buildChildrenLayer() { - mChildren.buildLayer(); + mShortcutsAndWidgets.removeViewsInLayout(start, count); } @Override @@ -645,24 +742,29 @@ public class CellLayout extends ViewGroup { public void setTagToCellInfoForPoint(int touchX, int touchY) { final CellInfo cellInfo = mCellInfo; - final Rect frame = mRect; + Rect frame = mRect; final int x = touchX + mScrollX; final int y = touchY + mScrollY; - final int count = mChildren.getChildCount(); + final int count = mShortcutsAndWidgets.getChildCount(); boolean found = false; for (int i = count - 1; i >= 0; i--) { - final View child = mChildren.getChildAt(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(mPaddingLeft, mPaddingTop); + frame.inset((int) (frame.width() * (1f - scale) / 2), + (int) (frame.height() * (1f - scale) / 2)); if (frame.contains(x, y)) { cellInfo.cell = child; @@ -709,6 +811,7 @@ public class CellLayout extends ViewGroup { if (action == MotionEvent.ACTION_DOWN) { setTagToCellInfoForPoint((int) ev.getX(), (int) ev.getY()); } + return false; } @@ -775,7 +878,7 @@ public class CellLayout extends ViewGroup { } /** - * Given a cell coordinate, return the point that represents the upper left corner of that cell + * 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 @@ -783,11 +886,47 @@ public class CellLayout extends ViewGroup { * @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)); + } - result[0] = hStartPadding + cellX * (mCellWidth + mWidthGap) + mCellWidth / 2; - result[1] = vStartPadding + cellY * (mCellHeight + mHeightGap) + mCellHeight / 2; + 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() { @@ -842,7 +981,7 @@ public class CellLayout extends ViewGroup { int vFreeSpace = vSpace - (mCountY * mOriginalCellHeight); mWidthGap = Math.min(mMaxGap, numWidthGaps > 0 ? (hFreeSpace / numWidthGaps) : 0); mHeightGap = Math.min(mMaxGap,numHeightGaps > 0 ? (vFreeSpace / numHeightGaps) : 0); - mChildren.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap); + mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap); } else { mWidthGap = mOriginalWidthGap; mHeightGap = mOriginalHeightGap; @@ -891,22 +1030,18 @@ public class CellLayout extends ViewGroup { @Override protected void setChildrenDrawingCacheEnabled(boolean enabled) { - mChildren.setChildrenDrawingCacheEnabled(enabled); + mShortcutsAndWidgets.setChildrenDrawingCacheEnabled(enabled); } @Override protected void setChildrenDrawnWithCacheEnabled(boolean enabled) { - mChildren.setChildrenDrawnWithCacheEnabled(enabled); + mShortcutsAndWidgets.setChildrenDrawnWithCacheEnabled(enabled); } public float getBackgroundAlpha() { return mBackgroundAlpha; } - public void setFastBackgroundAlpha(float alpha) { - mBackgroundAlpha = alpha; - } - public void setBackgroundAlphaMultiplier(float multiplier) { mBackgroundAlphaMultiplier = multiplier; } @@ -916,50 +1051,39 @@ public class CellLayout extends ViewGroup { } public void setBackgroundAlpha(float alpha) { - mBackgroundAlpha = alpha; - invalidate(); - } - - // Need to return true to let the view system know we know how to handle alpha-- this is - // because when our children have an alpha of 0.0f, they are still rendering their "dimmed" - // versions - @Override - protected boolean onSetAlpha(int alpha) { - return true; - } - - public void setAlpha(float alpha) { - setChildrenAlpha(alpha); - super.setAlpha(alpha); - } - - public void setFastAlpha(float alpha) { - setFastChildrenAlpha(alpha); - super.setFastAlpha(alpha); + if (mBackgroundAlpha != alpha) { + mBackgroundAlpha = alpha; + invalidate(); + } } - private void setChildrenAlpha(float alpha) { + public void setShortcutAndWidgetAlpha(float alpha) { final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { getChildAt(i).setAlpha(alpha); } } - private void setFastChildrenAlpha(float alpha) { - final int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - getChildAt(i).setFastAlpha(alpha); + public ShortcutAndWidgetContainer getShortcutsAndWidgets() { + if (getChildCount() > 0) { + return (ShortcutAndWidgetContainer) getChildAt(0); } + return null; } public View getChildAt(int x, int y) { - return mChildren.getChildAt(x, y); + return mShortcutsAndWidgets.getChildAt(x, y); } public boolean animateChildToPosition(final View child, int cellX, int cellY, int duration, - int delay) { - CellLayoutChildren clc = getChildrenLayout(); - if (clc.indexOfChild(child) != -1 && !mOccupied[cellX][cellY]) { + 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(); @@ -969,34 +1093,48 @@ public class CellLayout extends ViewGroup { mReorderAnimators.remove(lp); } - int oldX = lp.x; - int oldY = lp.y; - mOccupied[lp.cellX][lp.cellY] = false; - mOccupied[cellX][cellY] = true; - + 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; - lp.cellX = info.cellX = cellX; - lp.cellY = info.cellY = cellY; + 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; - int newX = lp.x; - int newY = lp.y; + final int newX = lp.x; + final int newY = lp.y; lp.x = oldX; lp.y = oldY; - child.requestLayout(); - PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", oldX, newX); - PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", oldY, newY); - ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(lp, x, y); - oa.setDuration(duration); - mReorderAnimators.put(lp, oa); - oa.addUpdateListener(new AnimatorUpdateListener() { + // Exit early if we're not actually moving the view + if (oldX == newX && oldY == newY) { + lp.isLockedToGrid = true; + return true; + } + + ValueAnimator va = ValueAnimator.ofFloat(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(); } }); - oa.addListener(new AnimatorListenerAdapter() { + va.addListener(new AnimatorListenerAdapter() { boolean cancelled = false; public void onAnimationEnd(Animator animation) { // If the animation was cancelled, it means that another animation @@ -1004,6 +1142,7 @@ public class CellLayout extends ViewGroup { // place just yet. if (!cancelled) { lp.isLockedToGrid = true; + child.requestLayout(); } if (mReorderAnimators.containsKey(lp)) { mReorderAnimators.remove(lp); @@ -1013,8 +1152,8 @@ public class CellLayout extends ViewGroup { cancelled = true; } }); - oa.setStartDelay(delay); - oa.start(); + va.setStartDelay(delay); + va.start(); return true; } return false; @@ -1050,12 +1189,11 @@ public class CellLayout extends ViewGroup { result[1] = Math.max(0, result[1]); // Snap to top } - void visualizeDropLocation(View v, Bitmap dragOutline, int originX, int originY, - int spanX, int spanY, Point dragOffset, Rect dragRegion) { - + 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]; - final int[] nearest = findNearestVacantArea(originX, originY, spanX, spanY, v, mDragCell); + if (v != null && dragOffset == null) { mDragCenter.set(originX + (v.getWidth() / 2), originY + (v.getHeight() / 2)); } else { @@ -1069,10 +1207,12 @@ public class CellLayout extends ViewGroup { return; } - if (nearest != null && (nearest[0] != oldDragCellX || nearest[1] != oldDragCellY)) { + 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(nearest[0], nearest[1], topLeft); + cellToPoint(cellX, cellY, topLeft); int left = topLeft[0]; int top = topLeft[1]; @@ -1106,12 +1246,15 @@ public class CellLayout extends ViewGroup { - 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); + } - mDragOutlines[mDragOutlineCurrent].set(left, top); mDragOutlineAnims[mDragOutlineCurrent].setTag(dragOutline); mDragOutlineAnims[mDragOutlineCurrent].animateIn(); } @@ -1125,8 +1268,7 @@ public class CellLayout extends ViewGroup { public void clearDragOutlines() { final int oldIndex = mDragOutlineCurrent; mDragOutlineAnims[oldIndex].animateOut(); - mDragCell[0] = -1; - mDragCell[1] = -1; + mDragCell[0] = mDragCell[1] = -1; } /** @@ -1142,8 +1284,8 @@ public class CellLayout extends ViewGroup { * @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) { + int[] findNearestVacantArea(int pixelX, int pixelY, int spanX, int spanY, + int[] result) { return findNearestVacantArea(pixelX, pixelY, spanX, spanY, null, result); } @@ -1153,6 +1295,27 @@ public class CellLayout extends ViewGroup { * * @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 @@ -1163,8 +1326,47 @@ public class CellLayout extends ViewGroup { */ 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); + 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 @@ -1175,49 +1377,956 @@ public class CellLayout extends ViewGroup { // 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; - final boolean[][] occupied = mOccupied; - for (int y = 0; y < countY - (spanY - 1); y++) { + 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 - (spanX - 1); x++) { + for (int x = 0; x < countX - (minSpanX - 1); x++) { + int ySize = -1; + int xSize = -1; if (ignoreOccupied) { - for (int i = 0; i < spanX; i++) { - for (int j = 0; j < spanY; j++) { + // 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]) { - // small optimization: we can skip to after the column we - // just found an occupied cell - x += i; 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) { + + 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); + 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 int[] findNearestAreaInDirection(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]; + bestXY[0] = -1; + bestXY[1] = -1; + float bestDistance = Float.MAX_VALUE; + + // We use this to march in a single direction + if ((direction[0] != 0 && direction[1] != 0) || + (direction[0] == 0 && direction[1] == 0)) { + return bestXY; + } + + // This will only incrememnet one of x or y based on the assertion above + int x = cellX + direction[0]; + int y = cellY + direction[1]; + while (x >= 0 && x + spanX <= mCountX && y >= 0 && y + spanY <= mCountY) { + + boolean fail = false; + 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])) { + fail = true; + } + } + } + if (!fail) { + float distance = (float) + Math.sqrt((x - cellX) * (x - cellX) + (y - cellY) * (y - cellY)); + if (Float.compare(distance, bestDistance) < 0) { + bestDistance = distance; + bestXY[0] = x; + bestXY[1] = y; + } + } + x += direction[0]; + y += direction[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 method looks in the specified direction to see if there is an additional view + // immediately adjecent in that direction + private boolean addViewInDirection(ArrayList<View> views, Rect boundingRect, int[] direction, + boolean[][] occupied, View dragView, ItemConfiguration currentState) { + boolean found = false; + + int childCount = mShortcutsAndWidgets.getChildCount(); + Rect r0 = new Rect(boundingRect); + Rect r1 = new Rect(); + + int deltaX = 0; + int deltaY = 0; + if (direction[1] < 0) { + r0.set(r0.left, r0.top - 1, r0.right, r0.bottom); + deltaY = -1; + } else if (direction[1] > 0) { + r0.set(r0.left, r0.top, r0.right, r0.bottom + 1); + deltaY = 1; + } else if (direction[0] < 0) { + r0.set(r0.left - 1, r0.top, r0.right, r0.bottom); + deltaX = -1; + } else if (direction[0] > 0) { + r0.set(r0.left, r0.top, r0.right + 1, r0.bottom); + deltaX = 1; + } + + for (int i = 0; i < childCount; i++) { + View child = mShortcutsAndWidgets.getChildAt(i); + if (views.contains(child) || child == dragView) continue; + CellAndSpan c = currentState.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; + } + boolean pushed = false; + for (int x = c.x; x < c.x + c.spanX; x++) { + for (int y = c.y; y < c.y + c.spanY; y++) { + boolean inBounds = x - deltaX >= 0 && x -deltaX < mCountX + && y - deltaY >= 0 && y - deltaY < mCountY; + if (inBounds && occupied[x - deltaX][y - deltaY]) { + pushed = true; + } + } + } + if (pushed) { + views.add(child); + boundingRect.union(c.x, c.y, c.x + c.spanX, c.y + c.spanY); + found = true; + } + } + } + return found; + } + + private boolean addViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop, + int[] direction, boolean push, 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); + } + } + + @SuppressWarnings("unchecked") + ArrayList<View> dup = (ArrayList<View>) views.clone(); + // We try and expand the group of views in the direction vector passed, based on + // whether they are physically adjacent, ie. based on "push mechanics". + while (push && addViewInDirection(dup, boundingRect, direction, mTmpOccupied, dragView, + currentState)) { + } + + // Mark the occupied state as false for the group of views we want to move. + for (View v: dup) { + 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 tetris-style interlocking. + for (View v: dup) { + CellAndSpan c = currentState.map.get(v); + markCellsForView(c.x - left, c.y - top, c.spanX, c.spanY, blockOccupied, true); + } + + markCellsForRect(rectOccupiedByPotentialDrop, mTmpOccupied, true); + + if (push) { + findNearestAreaInDirection(boundingRect.left, boundingRect.top, boundingRect.width(), + boundingRect.height(), direction, mTmpOccupied, blockOccupied, mTempLocation); + } else { + 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: dup) { + 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: dup) { + 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); + } + + private boolean rearrangementExists(int cellX, int cellY, int spanX, int spanY, int[] direction, + View ignoreView, ItemConfiguration solution) { + + 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); + } + } + + // We try to move the intersecting views as a block using the push mechanic + if (addViewsToTempLocation(mIntersectingViews, mOccupiedRect, direction, true, ignoreView, + solution)) { + return true; + } + // Try the opposite direction + direction[0] *= -1; + direction[1] *= -1; + if (addViewsToTempLocation(mIntersectingViews, mOccupiedRect, direction, true, ignoreView, + solution)) { + return true; + } + // Switch the direction back + direction[0] *= -1; + direction[1] *= -1; + + // Next we try moving the views as a block , but without requiring the push mechanic + if (addViewsToTempLocation(mIntersectingViews, mOccupiedRect, direction, false, 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.map.put(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(); + int timeForPriorAnimationToComplete = getMaxCompletionTime(); + 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(timeForPriorAnimationToComplete); + } + } + } + + // 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 deltaX; + float deltaY; + private static final int DURATION = 140; + private int repeatCount; + private boolean cancelOnCycleComplete = false; + ValueAnimator va; + + 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; + deltaX = 0; + deltaY = 0; + if (dX == dY && dX == 0) { + } else { + if (dY == 0) { + deltaX = mReorderHintAnimationMagnitude; + } else if (dX == 0) { + deltaY = mReorderHintAnimationMagnitude; + } else { + double angle = Math.atan( (float) (dY) / dX); + deltaX = (int) (Math.cos(angle) * mReorderHintAnimationMagnitude); + deltaY = (int) (Math.sin(angle) * mReorderHintAnimationMagnitude); + } + } + this.child = child; + } + + void animate(int delay) { + if (mShakeAnimators.containsKey(child)) { + ReorderHintAnimation oldAnimation = mShakeAnimators.get(child); + oldAnimation.completeAnimation(); + mShakeAnimators.remove(child); + } + if (deltaX == 0 && deltaY == 0) { + return; + } + va = ValueAnimator.ofFloat(0f, 1f); + va.setRepeatMode(ValueAnimator.REVERSE); + va.setRepeatCount(ValueAnimator.INFINITE); + va.setDuration(DURATION); + va.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float r = ((Float) animation.getAnimatedValue()).floatValue(); + float x = r * deltaX; + float y = r * deltaY; + child.setTranslationX(x); + child.setTranslationY(y); + } + }); + va.addListener(new AnimatorListenerAdapter() { + public void onAnimationRepeat(Animator animation) { + repeatCount++; + // We make sure to end only after a full period + if (cancelOnCycleComplete && repeatCount % 2 == 0) { + va.cancel(); + } + } + }); + va.setStartDelay(Math.max(REORDER_ANIMATION_DURATION, delay)); + mShakeAnimators.put(child, this); + va.start(); + } + + + private void completeAnimation() { + cancelOnCycleComplete = true; + } + + // Returns the time required to complete the current oscillating animation + private int completionTime() { + if (repeatCount % 2 == 0) { + return (int) (va.getDuration() - va.getCurrentPlayTime() + DURATION); + } else { + return (int) (va.getDuration() - va.getCurrentPlayTime()); + } + } + } + + private void completeAndClearReorderHintAnimations() { + for (ReorderHintAnimation a: mShakeAnimators.values()) { + a.completeAnimation(); + } + mShakeAnimators.clear(); + } + + private int getMaxCompletionTime() { + int maxTime = 0; + for (ReorderHintAnimation a: mShakeAnimators.values()) { + maxTime = Math.max(maxTime, a.completionTime()); + } + return maxTime; + } + + 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) { + info.cellX = lp.cellX = lp.tmpCellX; + info.cellY = lp.cellY = lp.tmpCellY; + } + } + 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); + } + + 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>(); + boolean isSolution = false; + int dragViewX, dragViewY, dragViewSpanX, dragViewSpanY; + + int area() { + return dragViewSpanX * dragViewSpanY; + } + } + + private class CellAndSpan { + int x, y; + int spanX, spanY; + + public CellAndSpan(int x, int y, int spanX, int spanY) { + this.x = x; + this.y = y; + this.spanX = spanX; + this.spanY = spanY; + } + } + /** * Find a vacant area that will fit the given bounds nearest the requested * cell location. Uses Euclidean distance to score multiple vacant areas. @@ -1237,6 +2346,27 @@ public class CellLayout extends ViewGroup { } /** + * 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. * @@ -1272,7 +2402,7 @@ public class CellLayout extends ViewGroup { * @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); + return findCellForSpanThatIntersectsIgnoring(cellXY, spanX, spanY, -1, -1, null, mOccupied); } /** @@ -1286,7 +2416,8 @@ public class CellLayout extends ViewGroup { * @return */ boolean findCellForSpanIgnoring(int[] cellXY, int spanX, int spanY, View ignoreView) { - return findCellForSpanThatIntersectsIgnoring(cellXY, spanX, spanY, -1, -1, ignoreView); + return findCellForSpanThatIntersectsIgnoring(cellXY, spanX, spanY, -1, -1, + ignoreView, mOccupied); } /** @@ -1304,16 +2435,16 @@ public class CellLayout extends ViewGroup { boolean findCellForSpanThatIntersects(int[] cellXY, int spanX, int spanY, int intersectX, int intersectY) { return findCellForSpanThatIntersectsIgnoring( - cellXY, spanX, spanY, intersectX, intersectY, null); + 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) { + int intersectX, int intersectY, View ignoreView, boolean occupied[][]) { // mark space take by ignoreView as available (method checks if ignoreView is null) - markCellsAsUnoccupiedForView(ignoreView); + markCellsAsUnoccupiedForView(ignoreView, occupied); boolean foundCell = false; while (true) { @@ -1339,7 +2470,7 @@ public class CellLayout extends ViewGroup { for (int x = startX; x < endX; x++) { for (int i = 0; i < spanX; i++) { for (int j = 0; j < spanY; j++) { - if (mOccupied[x + i][y + j]) { + if (occupied[x + i][y + j]) { // small optimization: we can skip to after the column we just found // an occupied cell x += i; @@ -1367,7 +2498,7 @@ public class CellLayout extends ViewGroup { } // re-mark space taken by ignoreView as occupied - markCellsAsOccupiedForView(ignoreView); + markCellsAsOccupiedForView(ignoreView, occupied); return foundCell; } @@ -1403,11 +2534,10 @@ public class CellLayout extends ViewGroup { } // Invalidate the drag data - mDragCell[0] = -1; - mDragCell[1] = -1; + mDragCell[0] = mDragCell[1] = -1; mDragOutlineAnims[mDragOutlineCurrent].animateOut(); mDragOutlineCurrent = (mDragOutlineCurrent + 1) % mDragOutlineAnims.length; - + revertTempState(); setIsDragOverlapping(false); } @@ -1435,7 +2565,7 @@ public class CellLayout extends ViewGroup { * @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, RectF resultRect) { + 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; @@ -1610,28 +2740,35 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { } } - public void onMove(View view, int newCellX, int newCellY) { - LayoutParams lp = (LayoutParams) view.getLayoutParams(); + public void onMove(View view, int newCellX, int newCellY, int newSpanX, int newSpanY) { markCellsAsUnoccupiedForView(view); - markCellsForView(newCellX, newCellY, lp.cellHSpan, lp.cellVSpan, true); + markCellsForView(newCellX, newCellY, newSpanX, newSpanY, mOccupied, true); } public void markCellsAsOccupiedForView(View view) { - if (view == null || view.getParent() != mChildren) return; + 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, true); + markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, occupied, true); } public void markCellsAsUnoccupiedForView(View view) { - if (view == null || view.getParent() != mChildren) return; + 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, false); + markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, occupied, false); } - private void markCellsForView(int cellX, int cellY, int spanX, int spanY, boolean value) { + 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++) { - mOccupied[x][y] = value; + occupied[x][y] = value; } } } @@ -1694,6 +2831,21 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { 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 @@ -1711,6 +2863,12 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { */ 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; @@ -1752,15 +2910,15 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { if (isLockedToGrid) { final int myCellHSpan = cellHSpan; final int myCellVSpan = cellVSpan; - final int myCellX = cellX; - final int myCellY = cellY; + final int myCellX = useTmpCoords ? tmpCellX : cellX; + final int myCellY = useTmpCoords ? tmpCellY : cellY; width = myCellHSpan * cellWidth + ((myCellHSpan - 1) * widthGap) - leftMargin - rightMargin; height = myCellVSpan * cellHeight + ((myCellVSpan - 1) * heightGap) - topMargin - bottomMargin; - x = myCellX * (cellWidth + widthGap) + leftMargin; - y = myCellY * (cellHeight + heightGap) + topMargin; + x = (int) (myCellX * (cellWidth + widthGap) + leftMargin); + y = (int) (myCellY * (cellHeight + heightGap) + topMargin); } } diff --git a/src/com/android/launcher2/CheckLongPressHelper.java b/src/com/android/launcher2/CheckLongPressHelper.java new file mode 100644 index 000000000..5c3752ad6 --- /dev/null +++ b/src/com/android/launcher2/CheckLongPressHelper.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2012 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.launcher2; + +import android.view.View; + +public class CheckLongPressHelper { + private View mView; + private boolean mHasPerformedLongPress; + private CheckForLongPress mPendingCheckForLongPress; + + class CheckForLongPress implements Runnable { + public void run() { + if ((mView.getParent() != null) && mView.hasWindowFocus() + && !mHasPerformedLongPress) { + if (mView.performLongClick()) { + mView.setPressed(false); + mHasPerformedLongPress = true; + } + } + } + } + + public CheckLongPressHelper(View v) { + mView = v; + } + + public void postCheckForLongPress() { + mHasPerformedLongPress = false; + + if (mPendingCheckForLongPress == null) { + mPendingCheckForLongPress = new CheckForLongPress(); + } + mView.postDelayed(mPendingCheckForLongPress, LauncherApplication.getLongPressTimeout()); + } + + public void cancelLongPress() { + mHasPerformedLongPress = false; + if (mPendingCheckForLongPress != null) { + mView.removeCallbacks(mPendingCheckForLongPress); + mPendingCheckForLongPress = null; + } + } + + public boolean hasPerformedLongPress() { + return mHasPerformedLongPress; + } +} diff --git a/src/com/android/launcher2/DeleteDropTarget.java b/src/com/android/launcher2/DeleteDropTarget.java index f19970823..7e4225be4 100644 --- a/src/com/android/launcher2/DeleteDropTarget.java +++ b/src/com/android/launcher2/DeleteDropTarget.java @@ -16,26 +16,34 @@ package com.android.launcher2; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; +import android.graphics.PointF; import android.graphics.Rect; import android.graphics.drawable.TransitionDrawable; import android.util.AttributeSet; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.animation.AnimationUtils; import android.view.animation.DecelerateInterpolator; +import android.view.animation.LinearInterpolator; import com.android.launcher.R; public class DeleteDropTarget extends ButtonDropTarget { + private static int DELETE_ANIMATION_DURATION = 285; + private static int MODE_FLING_DELETE_TO_TRASH = 0; + private static int MODE_FLING_DELETE_ALONG_VECTOR = 1; + + private final int mFlingDeleteMode = MODE_FLING_DELETE_ALONG_VECTOR; - private static int DELETE_ANIMATION_DURATION = 250; private ColorStateList mOriginalTextColor; - private int mHoverColor = 0xFFFF0000; private TransitionDrawable mUninstallDrawable; private TransitionDrawable mRemoveDrawable; private TransitionDrawable mCurrentDrawable; @@ -58,8 +66,6 @@ public class DeleteDropTarget extends ButtonDropTarget { // Get the hover color Resources r = getResources(); mHoverColor = r.getColor(R.color.delete_target_hover_tint); - mHoverPaint.setColorFilter(new PorterDuffColorFilter( - mHoverColor, PorterDuff.Mode.SRC_ATOP)); mUninstallDrawable = (TransitionDrawable) r.getDrawable(R.drawable.uninstall_target_selector); mRemoveDrawable = (TransitionDrawable) r.getDrawable(R.drawable.remove_target_selector); @@ -99,6 +105,15 @@ public class DeleteDropTarget extends ButtonDropTarget { return (d.dragSource instanceof Workspace) && (d.dragInfo instanceof FolderInfo); } + private void setHoverColor() { + mCurrentDrawable.startTransition(mTransitionDuration); + setTextColor(mHoverColor); + } + private void resetHoverColor() { + mCurrentDrawable.resetTransition(); + setTextColor(mOriginalTextColor); + } + @Override public boolean acceptDrop(DragObject d) { // We can remove everything including App shortcuts, folders, widgets, etc. @@ -134,8 +149,7 @@ public class DeleteDropTarget extends ButtonDropTarget { mCurrentDrawable = (TransitionDrawable) getCompoundDrawables()[0]; mActive = isVisible; - mCurrentDrawable.resetTransition(); - setTextColor(mOriginalTextColor); + resetHoverColor(); ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE); if (getText().length() > 0) { setText(isUninstall ? R.string.delete_target_uninstall_label @@ -152,35 +166,27 @@ public class DeleteDropTarget extends ButtonDropTarget { public void onDragEnter(DragObject d) { super.onDragEnter(d); - mCurrentDrawable.startTransition(mTransitionDuration); - setTextColor(mHoverColor); + setHoverColor(); } public void onDragExit(DragObject d) { super.onDragExit(d); if (!d.dragComplete) { - mCurrentDrawable.resetTransition(); - setTextColor(mOriginalTextColor); + resetHoverColor(); + } else { + // Restore the hover color if we are deleting + d.dragView.setColor(mHoverColor); } } private void animateToTrashAndCompleteDrop(final DragObject d) { DragLayer dragLayer = mLauncher.getDragLayer(); Rect from = new Rect(); - Rect to = new Rect(); dragLayer.getViewRectRelativeToSelf(d.dragView, from); - dragLayer.getViewRectRelativeToSelf(this, to); - - int width = mCurrentDrawable.getIntrinsicWidth(); - int height = mCurrentDrawable.getIntrinsicHeight(); - to.set(to.left + getPaddingLeft(), to.top + getPaddingTop(), - to.left + getPaddingLeft() + width, to.bottom); - - // Center the destination rect about the trash icon - int xOffset = (int) -(d.dragView.getMeasuredWidth() - width) / 2; - int yOffset = (int) -(d.dragView.getMeasuredHeight() - height) / 2; - to.offset(xOffset, yOffset); + Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), + mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight()); + float scale = (float) to.width() / from.width(); mSearchDropTargetBar.deferOnDragEnd(); Runnable onAnimationEndRunnable = new Runnable() { @@ -191,9 +197,10 @@ public class DeleteDropTarget extends ButtonDropTarget { completeDrop(d); } }; - dragLayer.animateView(d.dragView, from, to, 0.1f, 0.1f, + dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f, DELETE_ANIMATION_DURATION, new DecelerateInterpolator(2), - new DecelerateInterpolator(1.5f), onAnimationEndRunnable, false); + new LinearInterpolator(), onAnimationEndRunnable, + DragLayer.ANIMATION_END_DISAPPEAR, null); } private void completeDrop(DragObject d) { @@ -231,4 +238,187 @@ public class DeleteDropTarget extends ButtonDropTarget { public void onDrop(DragObject d) { animateToTrashAndCompleteDrop(d); } + + /** + * Creates an animation from the current drag view to the delete trash icon. + */ + private AnimatorUpdateListener createFlingToTrashAnimatorListener(final DragLayer dragLayer, + DragObject d, PointF vel, ViewConfiguration config) { + final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), + mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight()); + final Rect from = new Rect(); + dragLayer.getViewRectRelativeToSelf(d.dragView, from); + + // Calculate how far along the velocity vector we should put the intermediate point on + // the bezier curve + float velocity = Math.abs(vel.length()); + float vp = Math.min(1f, velocity / (config.getScaledMaximumFlingVelocity() / 2f)); + int offsetY = (int) (-from.top * vp); + int offsetX = (int) (offsetY / (vel.y / vel.x)); + final float y2 = from.top + offsetY; // intermediate t/l + final float x2 = from.left + offsetX; + final float x1 = from.left; // drag view t/l + final float y1 = from.top; + final float x3 = to.left; // delete target t/l + final float y3 = to.top; + + final TimeInterpolator scaleAlphaInterpolator = new TimeInterpolator() { + @Override + public float getInterpolation(float t) { + return t * t * t * t * t * t * t * t; + } + }; + return new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + final DragView dragView = (DragView) dragLayer.getAnimatedView(); + float t = ((Float) animation.getAnimatedValue()).floatValue(); + float tp = scaleAlphaInterpolator.getInterpolation(t); + float initialScale = dragView.getInitialScale(); + float finalAlpha = 0.5f; + float scale = dragView.getScaleX(); + float x1o = ((1f - scale) * dragView.getMeasuredWidth()) / 2f; + float y1o = ((1f - scale) * dragView.getMeasuredHeight()) / 2f; + float x = (1f - t) * (1f - t) * (x1 - x1o) + 2 * (1f - t) * t * (x2 - x1o) + + (t * t) * x3; + float y = (1f - t) * (1f - t) * (y1 - y1o) + 2 * (1f - t) * t * (y2 - x1o) + + (t * t) * y3; + + dragView.setTranslationX(x); + dragView.setTranslationY(y); + dragView.setScaleX(initialScale * (1f - tp)); + dragView.setScaleY(initialScale * (1f - tp)); + dragView.setAlpha(finalAlpha + (1f - finalAlpha) * (1f - tp)); + } + }; + } + + /** + * Creates an animation from the current drag view along its current velocity vector. + * For this animation, the alpha runs for a fixed duration and we update the position + * progressively. + */ + private static class FlingAlongVectorAnimatorUpdateListener implements AnimatorUpdateListener { + private static float FRICTION = 0.93f; + + private DragLayer mDragLayer; + private PointF mVelocity; + private Rect mFrom; + private long mPrevTime; + private boolean mHasOffsetForScale; + + private final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(1.5f); + + public FlingAlongVectorAnimatorUpdateListener(DragLayer dragLayer, PointF vel, Rect from, + long startTime) { + mDragLayer = dragLayer; + mVelocity = vel; + mFrom = from; + mPrevTime = startTime; + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + final DragView dragView = (DragView) mDragLayer.getAnimatedView(); + float t = ((Float) animation.getAnimatedValue()).floatValue(); + long curTime = AnimationUtils.currentAnimationTimeMillis(); + + if (!mHasOffsetForScale) { + mHasOffsetForScale = true; + float scale = dragView.getScaleX(); + float xOffset = ((scale - 1f) * dragView.getMeasuredWidth()) / 2f; + float yOffset = ((scale - 1f) * dragView.getMeasuredHeight()) / 2f; + + mFrom.left += xOffset; + mFrom.top += yOffset; + } + + mFrom.left += (mVelocity.x * (curTime - mPrevTime) / 1000f); + mFrom.top += (mVelocity.y * (curTime - mPrevTime) / 1000f); + + dragView.setTranslationX(mFrom.left); + dragView.setTranslationY(mFrom.top); + dragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t)); + + mVelocity.x *= FRICTION; + mVelocity.y *= FRICTION; + mPrevTime = curTime; + } + }; + private AnimatorUpdateListener createFlingAlongVectorAnimatorListener(final DragLayer dragLayer, + DragObject d, PointF vel, final long startTime, final int duration, + ViewConfiguration config) { + final Rect from = new Rect(); + dragLayer.getViewRectRelativeToSelf(d.dragView, from); + + return new FlingAlongVectorAnimatorUpdateListener(dragLayer, vel, from, startTime); + } + + public void onFlingToDelete(final DragObject d, int x, int y, PointF vel) { + final boolean isAllApps = d.dragSource instanceof AppsCustomizePagedView; + + // Don't highlight the icon as it's animating + d.dragView.setColor(0); + d.dragView.updateInitialScaleToCurrentScale(); + // Don't highlight the target if we are flinging from AllApps + if (isAllApps) { + resetHoverColor(); + } + + if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) { + // Defer animating out the drop target if we are animating to it + mSearchDropTargetBar.deferOnDragEnd(); + mSearchDropTargetBar.finishAnimations(); + } + + final ViewConfiguration config = ViewConfiguration.get(mLauncher); + final DragLayer dragLayer = mLauncher.getDragLayer(); + final int duration = DELETE_ANIMATION_DURATION; + final long startTime = AnimationUtils.currentAnimationTimeMillis(); + + // NOTE: Because it takes time for the first frame of animation to actually be + // called and we expect the animation to be a continuation of the fling, we have + // to account for the time that has elapsed since the fling finished. And since + // we don't have a startDelay, we will always get call to update when we call + // start() (which we want to ignore). + final TimeInterpolator tInterpolator = new TimeInterpolator() { + private int mCount = -1; + private float mOffset = 0f; + + @Override + public float getInterpolation(float t) { + if (mCount < 0) { + mCount++; + } else if (mCount == 0) { + mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() - + startTime) / duration); + mCount++; + } + return Math.min(1f, mOffset + t); + } + }; + AnimatorUpdateListener updateCb = null; + if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) { + updateCb = createFlingToTrashAnimatorListener(dragLayer, d, vel, config); + } else if (mFlingDeleteMode == MODE_FLING_DELETE_ALONG_VECTOR) { + updateCb = createFlingAlongVectorAnimatorListener(dragLayer, d, vel, startTime, + duration, config); + } + Runnable onAnimationEndRunnable = new Runnable() { + @Override + public void run() { + mSearchDropTargetBar.onDragEnd(); + + // If we are dragging from AllApps, then we allow AppsCustomizePagedView to clean up + // itself, otherwise, complete the drop to initiate the deletion process + if (!isAllApps) { + mLauncher.exitSpringLoadedDragMode(); + completeDrop(d); + } + mLauncher.getDragController().onDeferredEndFling(d); + } + }; + dragLayer.animateView(d.dragView, updateCb, duration, tInterpolator, onAnimationEndRunnable, + DragLayer.ANIMATION_END_DISAPPEAR, null); + } } diff --git a/src/com/android/launcher2/DragController.java b/src/com/android/launcher2/DragController.java index a120ac569..2a1d65adc 100644 --- a/src/com/android/launcher2/DragController.java +++ b/src/com/android/launcher2/DragController.java @@ -19,6 +19,7 @@ package com.android.launcher2; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Point; +import android.graphics.PointF; import android.graphics.Rect; import android.os.Handler; import android.os.IBinder; @@ -26,6 +27,7 @@ import android.os.Vibrator; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; +import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.inputmethod.InputMethodManager; @@ -47,8 +49,9 @@ public class DragController { /** Indicates the drag is a copy. */ public static int DRAG_ACTION_COPY = 1; - private static final int SCROLL_DELAY = 600; - private static final int VIBRATE_DURATION = 35; + private static final int SCROLL_DELAY = 500; + private static final int RESCROLL_DELAY = 750; + private static final int VIBRATE_DURATION = 15; private static final boolean PROFILE_DRAWING_DURING_DRAG = false; @@ -59,6 +62,9 @@ public class DragController { static final int SCROLL_LEFT = 0; static final int SCROLL_RIGHT = 1; + private static final float MAX_FLING_DEGREES = 35f; + private static final int FLING_TO_DELETE_THRESHOLD_Y_VELOCITY = -1400; + private Launcher mLauncher; private Handler mHandler; private final Vibrator mVibrator = new Vibrator(); @@ -85,8 +91,8 @@ public class DragController { /** Who can receive drop events */ private ArrayList<DropTarget> mDropTargets = new ArrayList<DropTarget>(); - private ArrayList<DragListener> mListeners = new ArrayList<DragListener>(); + private DropTarget mFlingToDeleteDropTarget; /** The window token used as the parent for the DragView. */ private IBinder mWindowToken; @@ -110,6 +116,9 @@ public class DragController { private int mTmpPoint[] = new int[2]; private Rect mDragLayerRect = new Rect(); + protected int mFlingToDeleteThresholdVelocity; + private VelocityTracker mVelocityTracker; + /** * Interface to receive notifications when a drag starts or stops */ @@ -140,6 +149,10 @@ public class DragController { mLauncher = launcher; mHandler = new Handler(); mScrollZone = launcher.getResources().getDimensionPixelSize(R.dimen.scroll_zone); + mVelocityTracker = VelocityTracker.obtain(); + + float density = launcher.getResources().getDisplayMetrics().density; + mFlingToDeleteThresholdVelocity = (int) (FLING_TO_DELETE_THRESHOLD_Y_VELOCITY * density); } public boolean dragging() { @@ -184,7 +197,7 @@ public class DragController { int dragLayerX = loc[0]; int dragLayerY = loc[1]; - startDrag(b, dragLayerX, dragLayerY, source, dragInfo, dragAction, null, dragRegion); + startDrag(b, dragLayerX, dragLayerY, source, dragInfo, dragAction, null, dragRegion, 1f); b.recycle(); if (dragAction == DRAG_ACTION_MOVE) { @@ -205,13 +218,16 @@ public class DragController { * Makes dragging feel more precise, e.g. you can clip out a transparent border */ public void startDrag(View v, Bitmap bmp, DragSource source, Object dragInfo, int dragAction, - Rect dragRegion) { + Rect dragRegion, float initialDragViewScale) { int[] loc = mCoordinatesTemp; mLauncher.getDragLayer().getLocationInDragLayer(v, loc); - int dragLayerX = loc[0]; - int dragLayerY = loc[1]; + int dragLayerX = loc[0] + v.getPaddingLeft() + + (int) ((initialDragViewScale * bmp.getWidth() - bmp.getWidth()) / 2); + int dragLayerY = loc[1] + v.getPaddingTop() + + (int) ((initialDragViewScale * bmp.getHeight() - bmp.getHeight()) / 2); - startDrag(bmp, dragLayerX, dragLayerY, source, dragInfo, dragAction, null, dragRegion); + startDrag(bmp, dragLayerX, dragLayerY, source, dragInfo, dragAction, null, dragRegion, + initialDragViewScale); if (dragAction == DRAG_ACTION_MOVE) { v.setVisibility(View.GONE); @@ -229,28 +245,12 @@ public class DragController { * @param dragInfo The data associated with the object that is being dragged * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or * {@link #DRAG_ACTION_COPY} - */ - public void startDrag(Bitmap b, int dragLayerX, int dragLayerY, - DragSource source, Object dragInfo, int dragAction) { - startDrag(b, dragLayerX, dragLayerY, source, dragInfo, dragAction, null, null); - } - - /** - * Starts a drag. - * - * @param b The bitmap to display as the drag image. It will be re-scaled to the - * enlarged size. - * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap. - * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap. - * @param source An object representing where the drag originated - * @param dragInfo The data associated with the object that is being dragged - * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or - * {@link #DRAG_ACTION_COPY} * @param dragRegion Coordinates within the bitmap b for the position of item being dragged. * Makes dragging feel more precise, e.g. you can clip out a transparent border */ public void startDrag(Bitmap b, int dragLayerX, int dragLayerY, - DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion) { + DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion, + float initialDragViewScale) { if (PROFILE_DRAWING_DURING_DRAG) { android.os.Debug.startMethodTracing("Launcher"); } @@ -285,7 +285,7 @@ public class DragController { mVibrator.vibrate(VIBRATE_DURATION); final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX, - registrationY, 0, 0, b.getWidth(), b.getHeight()); + registrationY, 0, 0, b.getWidth(), b.getHeight(), initialDragViewScale); if (dragOffset != null) { dragView.setDragVisualizeOffset(new Point(dragOffset)); @@ -363,9 +363,10 @@ public class DragController { if (mLastDropTarget != null) { mLastDropTarget.onDragExit(mDragObject); } + mDragObject.deferDragViewCleanupPostAnimation = false; mDragObject.cancelled = true; mDragObject.dragComplete = true; - mDragObject.dragSource.onDropCompleted(null, mDragObject, false); + mDragObject.dragSource.onDropCompleted(null, mDragObject, false, false); } endDrag(); } @@ -376,7 +377,11 @@ public class DragController { if (rawDragInfo instanceof ShortcutInfo) { ShortcutInfo dragInfo = (ShortcutInfo) rawDragInfo; for (ApplicationInfo info : apps) { - if (dragInfo.intent.getComponent().equals(info.intent.getComponent())) { + // Added null checks to prevent NPE we've seen in the wild + if (dragInfo != null && + dragInfo.intent != null && + info.intent != null && + dragInfo.intent.getComponent().equals(info.intent.getComponent())) { cancelDrag(); return; } @@ -388,16 +393,43 @@ public class DragController { private void endDrag() { if (mDragging) { mDragging = false; - for (DragListener listener : mListeners) { - listener.onDragEnd(); - } + clearScrollRunnable(); + boolean isDeferred = false; if (mDragObject.dragView != null) { - mDragObject.dragView.remove(); + isDeferred = mDragObject.deferDragViewCleanupPostAnimation; + if (!isDeferred) { + mDragObject.dragView.remove(); + } mDragObject.dragView = null; } + + // Only end the drag if we are not deferred + if (!isDeferred) { + for (DragListener listener : mListeners) { + listener.onDragEnd(); + } + } + } + + releaseVelocityTracker(); + } + + /** + * This only gets called as a result of drag view cleanup being deferred in endDrag(); + */ + void onDeferredEndDrag(DragView dragView) { + dragView.remove(); + + // If we skipped calling onDragEnd() before, do it now + for (DragListener listener : mListeners) { + listener.onDragEnd(); } } + void onDeferredEndFling(DropTarget.DragObject d) { + d.dragSource.onFlingToDeleteCompleted(); + } + /** * Clamps the position to the drag layer bounds. */ @@ -416,8 +448,11 @@ public class DragController { Log.d(Launcher.TAG, "DragController.onInterceptTouchEvent " + ev + " mDragging=" + mDragging); } - final int action = ev.getAction(); + // Update the velocity tracker + acquireVelocityTrackerAndAddMovement(ev); + + final int action = ev.getAction(); final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY()); final int dragLayerX = dragLayerPos[0]; final int dragLayerY = dragLayerPos[1]; @@ -433,7 +468,12 @@ public class DragController { break; case MotionEvent.ACTION_UP: if (mDragging) { - drop(dragLayerX, dragLayerY); + PointF vec = isFlingingToDelete(mDragObject.dragSource); + if (vec != null) { + dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec); + } else { + drop(dragLayerX, dragLayerY); + } } endDrag(); break; @@ -456,6 +496,15 @@ public class DragController { return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction); } + private void clearScrollRunnable() { + mHandler.removeCallbacks(mScrollRunnable); + if (mScrollState == SCROLL_WAITING_IN_ZONE) { + mScrollState = SCROLL_OUTSIDE_ZONE; + mScrollRunnable.setDirection(SCROLL_RIGHT); + mDragScroller.onExitScrollArea(); + } + } + private void handleMoveEvent(int x, int y) { mDragObject.dragView.move(x, y); @@ -491,30 +540,32 @@ public class DragController { Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2)); mLastTouch[0] = x; mLastTouch[1] = y; + final int delay = mDistanceSinceScroll < slop ? RESCROLL_DELAY : SCROLL_DELAY; if (x < mScrollZone) { - if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) { + if (mScrollState == SCROLL_OUTSIDE_ZONE) { mScrollState = SCROLL_WAITING_IN_ZONE; if (mDragScroller.onEnterScrollArea(x, y, SCROLL_LEFT)) { mScrollRunnable.setDirection(SCROLL_LEFT); - mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); + mHandler.postDelayed(mScrollRunnable, delay); } } } else if (x > mScrollView.getWidth() - mScrollZone) { - if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) { + if (mScrollState == SCROLL_OUTSIDE_ZONE) { mScrollState = SCROLL_WAITING_IN_ZONE; if (mDragScroller.onEnterScrollArea(x, y, SCROLL_RIGHT)) { mScrollRunnable.setDirection(SCROLL_RIGHT); - mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); + mHandler.postDelayed(mScrollRunnable, delay); } } } else { - if (mScrollState == SCROLL_WAITING_IN_ZONE) { - mScrollState = SCROLL_OUTSIDE_ZONE; - mScrollRunnable.setDirection(SCROLL_RIGHT); - mHandler.removeCallbacks(mScrollRunnable); - mDragScroller.onExitScrollArea(); - } + clearScrollRunnable(); + } + } + + public void forceMoveEvent() { + if (mDragging) { + handleMoveEvent(mDragObject.x, mDragObject.y); } } @@ -526,6 +577,9 @@ public class DragController { return false; } + // Update the velocity tracker + acquireVelocityTrackerAndAddMovement(ev); + final int action = ev.getAction(); final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY()); final int dragLayerX = dragLayerPos[0]; @@ -550,14 +604,20 @@ public class DragController { case MotionEvent.ACTION_UP: // Ensure that we've processed a move event at the current pointer location. handleMoveEvent(dragLayerX, dragLayerY); - mHandler.removeCallbacks(mScrollRunnable); + if (mDragging) { - drop(dragLayerX, dragLayerY); + PointF vec = isFlingingToDelete(mDragObject.dragSource); + if (vec != null) { + dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec); + } else { + drop(dragLayerX, dragLayerY); + } } endDrag(); break; case MotionEvent.ACTION_CANCEL: + mHandler.removeCallbacks(mScrollRunnable); cancelDrag(); break; } @@ -565,6 +625,60 @@ public class DragController { return true; } + /** + * Determines whether the user flung the current item to delete it. + * + * @return the vector at which the item was flung, or null if no fling was detected. + */ + private PointF isFlingingToDelete(DragSource source) { + if (mFlingToDeleteDropTarget == null) return null; + if (!source.supportsFlingToDelete()) return null; + + ViewConfiguration config = ViewConfiguration.get(mLauncher); + mVelocityTracker.computeCurrentVelocity(1000, config.getScaledMaximumFlingVelocity()); + + if (mVelocityTracker.getYVelocity() < mFlingToDeleteThresholdVelocity) { + // Do a quick dot product test to ensure that we are flinging upwards + PointF vel = new PointF(mVelocityTracker.getXVelocity(), + mVelocityTracker.getYVelocity()); + PointF upVec = new PointF(0f, -1f); + float theta = (float) Math.acos(((vel.x * upVec.x) + (vel.y * upVec.y)) / + (vel.length() * upVec.length())); + if (theta <= Math.toRadians(MAX_FLING_DEGREES)) { + return vel; + } + } + return null; + } + + private void dropOnFlingToDeleteTarget(float x, float y, PointF vel) { + final int[] coordinates = mCoordinatesTemp; + + mDragObject.x = coordinates[0]; + mDragObject.y = coordinates[1]; + + // Clean up dragging on the target if it's not the current fling delete target otherwise, + // start dragging to it. + if (mLastDropTarget != null && mFlingToDeleteDropTarget != mLastDropTarget) { + mLastDropTarget.onDragExit(mDragObject); + } + + // Drop onto the fling-to-delete target + boolean accepted = false; + mFlingToDeleteDropTarget.onDragEnter(mDragObject); + // We must set dragComplete to true _only_ after we "enter" the fling-to-delete target for + // "drop" + mDragObject.dragComplete = true; + mFlingToDeleteDropTarget.onDragExit(mDragObject); + if (mFlingToDeleteDropTarget.acceptDrop(mDragObject)) { + mFlingToDeleteDropTarget.onFlingToDelete(mDragObject, mDragObject.x, mDragObject.y, + vel); + accepted = true; + } + mDragObject.dragSource.onDropCompleted((View) mFlingToDeleteDropTarget, mDragObject, true, + accepted); + } + private void drop(float x, float y) { final int[] coordinates = mCoordinatesTemp; final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates); @@ -580,7 +694,7 @@ public class DragController { accepted = true; } } - mDragObject.dragSource.onDropCompleted((View) dropTarget, mDragObject, accepted); + mDragObject.dragSource.onDropCompleted((View) dropTarget, mDragObject, false, accepted); } private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) { @@ -655,6 +769,27 @@ public class DragController { } /** + * Sets the current fling-to-delete drop target. + */ + public void setFlingToDeleteDropTarget(DropTarget target) { + mFlingToDeleteDropTarget = target; + } + + private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + } + + private void releaseVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + /** * Set which view scrolls for touch events near the edge of the screen. */ public void setScrollView(View v) { @@ -681,6 +816,11 @@ public class DragController { mScrollState = SCROLL_OUTSIDE_ZONE; mDistanceSinceScroll = 0; mDragScroller.onExitScrollArea(); + + if (isDragging()) { + // Force an update so that we can requeue the scroller if necessary + forceMoveEvent(); + } } } diff --git a/src/com/android/launcher2/DragLayer.java b/src/com/android/launcher2/DragLayer.java index 915477119..94528574d 100644 --- a/src/com/android/launcher2/DragLayer.java +++ b/src/com/android/launcher2/DragLayer.java @@ -18,15 +18,15 @@ package com.android.launcher2; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PointF; import android.graphics.Rect; -import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.MotionEvent; @@ -34,6 +34,7 @@ import android.view.View; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import android.view.animation.AnimationUtils; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.widget.FrameLayout; @@ -62,15 +63,17 @@ public class DragLayer extends FrameLayout { private ValueAnimator mDropAnim = null; private ValueAnimator mFadeOutAnim = null; private TimeInterpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f); - private View mDropView = null; + private DragView mDropView = null; + private int mAnchorViewInitialScrollX = 0; + private View mAnchorView = null; - private int[] mDropViewPos = new int[2]; - private float mDropViewScale; - private float mDropViewAlpha; private boolean mHoverPointClosesFolder = false; private Rect mHitRect = new Rect(); private int mWorkspaceIndex = -1; private int mQsbIndex = -1; + public static final int ANIMATION_END_DISAPPEAR = 0; + public static final int ANIMATION_END_FADE_OUT = 1; + public static final int ANIMATION_END_REMAIN_VISIBLE = 2; /** * Used to create a new DragLayer from XML. @@ -412,26 +415,29 @@ public class DragLayer extends FrameLayout { animateViewIntoPosition(dragView, child, null); } - public void animateViewIntoPosition(DragView dragView, final int[] pos, float scale, - Runnable onFinishRunnable) { + public void animateViewIntoPosition(DragView dragView, final int[] pos, float alpha, + float scaleX, float scaleY, int animationEndStyle, Runnable onFinishRunnable, + int duration) { Rect r = new Rect(); getViewRectRelativeToSelf(dragView, r); final int fromX = r.left; final int fromY = r.top; - animateViewIntoPosition(dragView, fromX, fromY, pos[0], pos[1], scale, - onFinishRunnable, true, -1); + animateViewIntoPosition(dragView, fromX, fromY, pos[0], pos[1], alpha, 1, 1, scaleX, scaleY, + onFinishRunnable, animationEndStyle, duration, null); } public void animateViewIntoPosition(DragView dragView, final View child, final Runnable onFinishAnimationRunnable) { - animateViewIntoPosition(dragView, child, -1, onFinishAnimationRunnable); + animateViewIntoPosition(dragView, child, -1, onFinishAnimationRunnable, null); } public void animateViewIntoPosition(DragView dragView, final View child, int duration, - final Runnable onFinishAnimationRunnable) { - ((CellLayoutChildren) child.getParent()).measureChild(child); + final Runnable onFinishAnimationRunnable, View anchorView) { + ShortcutAndWidgetContainer parentChildren = (ShortcutAndWidgetContainer) child.getParent(); + CellLayout parent = (CellLayout) (CellLayout) parentChildren.getParent(); CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); + parentChildren.measureChild(child); Rect r = new Rect(); getViewRectRelativeToSelf(dragView, r); @@ -439,23 +445,28 @@ public class DragLayer extends FrameLayout { int coord[] = new int[2]; coord[0] = lp.x; coord[1] = lp.y; + // Since the child hasn't necessarily been laid out, we force the lp to be updated with // the correct coordinates (above) and use these to determine the final location float scale = getDescendantCoordRelativeToSelf((View) child.getParent(), coord); int toX = coord[0]; int toY = coord[1]; if (child instanceof TextView) { + float childrenScale = parent.getChildrenScale(); TextView tv = (TextView) child; - Drawable d = tv.getCompoundDrawables()[1]; - // Center in the y coordinate about the target's drawable - toY += Math.round(scale * tv.getPaddingTop()); - toY -= (dragView.getHeight() - (int) Math.round(scale * d.getIntrinsicHeight())) / 2; - // Center in the x coordinate about the target's drawable + // The child may be scaled (always about the center of the view) so to account for it, + // we have to offset the position by the scaled size. Once we do that, we can center + // the drag view about the scaled child view. + toY += Math.round(((1f - childrenScale) * child.getMeasuredHeight()) / 2 + + scale * childrenScale * tv.getPaddingTop()); + toY -= dragView.getMeasuredHeight() * (1 - scale * childrenScale) / 2; toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2; + + scale *= childrenScale; } else if (child instanceof FolderIcon) { // Account for holographic blur padding on the drag view - toY -= HolographicOutlineHelper.MAX_OUTER_BLUR_RADIUS / 2; + toY -= Workspace.DRAG_BITMAP_PADDING / 2; // Center in the x coordinate about the target's drawable toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2; } else { @@ -467,34 +478,27 @@ public class DragLayer extends FrameLayout { final int fromX = r.left; final int fromY = r.top; child.setVisibility(INVISIBLE); - child.setAlpha(0); Runnable onCompleteRunnable = new Runnable() { public void run() { child.setVisibility(VISIBLE); - ObjectAnimator oa = ObjectAnimator.ofFloat(child, "alpha", 0f, 1f); - oa.setDuration(60); - oa.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(android.animation.Animator animation) { - if (onFinishAnimationRunnable != null) { - onFinishAnimationRunnable.run(); - } - } - }); - oa.start(); + if (onFinishAnimationRunnable != null) { + onFinishAnimationRunnable.run(); + } } }; - animateViewIntoPosition(dragView, fromX, fromY, toX, toY, scale, - onCompleteRunnable, true, duration); + animateViewIntoPosition(dragView, fromX, fromY, toX, toY, 1, 1, 1, scale, scale, + onCompleteRunnable, ANIMATION_END_DISAPPEAR, duration, anchorView); } - private void animateViewIntoPosition(final View view, final int fromX, final int fromY, - final int toX, final int toY, float finalScale, Runnable onCompleteRunnable, - boolean fadeOut, int duration) { + public void animateViewIntoPosition(final DragView view, final int fromX, final int fromY, + final int toX, final int toY, float finalAlpha, float initScaleX, float initScaleY, + float finalScaleX, float finalScaleY, Runnable onCompleteRunnable, + int animationEndStyle, int duration, View anchorView) { Rect from = new Rect(fromX, fromY, fromX + view.getMeasuredWidth(), fromY + view.getMeasuredHeight()); Rect to = new Rect(toX, toY, toX + view.getMeasuredWidth(), toY + view.getMeasuredHeight()); - animateView(view, from, to, 1f, finalScale, duration, null, null, onCompleteRunnable, true); + animateView(view, from, to, finalAlpha, initScaleX, initScaleY, finalScaleX, finalScaleY, duration, + null, null, onCompleteRunnable, animationEndStyle, anchorView); } /** @@ -514,11 +518,16 @@ public class DragLayer extends FrameLayout { * @param onCompleteRunnable Optional runnable to run on animation completion. * @param fadeOut Whether or not to fade out the view once the animation completes. If true, * the runnable will execute after the view is faded out. + * @param anchorView If not null, this represents the view which the animated view stays + * anchored to in case scrolling is currently taking place. Note: currently this is + * only used for the X dimension for the case of the workspace. */ - public void animateView(final View view, final Rect from, final Rect to, final float finalAlpha, - final float finalScale, int duration, final Interpolator motionInterpolator, - final Interpolator alphaInterpolator, final Runnable onCompleteRunnable, - final boolean fadeOut) { + public void animateView(final DragView view, final Rect from, final Rect to, + final float finalAlpha, final float initScaleX, final float initScaleY, + final float finalScaleX, final float finalScaleY, int duration, + final Interpolator motionInterpolator, final Interpolator alphaInterpolator, + final Runnable onCompleteRunnable, final int animationEndStyle, View anchorView) { + // Calculate the duration of the animation based on the object's distance final float dist = (float) Math.sqrt(Math.pow(to.left - from.left, 2) + Math.pow(to.top - from.top, 2)); @@ -531,63 +540,116 @@ public class DragLayer extends FrameLayout { if (dist < maxDist) { duration *= mCubicEaseOutInterpolator.getInterpolation(dist / maxDist); } + duration = Math.max(duration, res.getInteger(R.integer.config_dropAnimMinDuration)); } - if (mDropAnim != null) { - mDropAnim.cancel(); - } - - if (mFadeOutAnim != null) { - mFadeOutAnim.cancel(); - } - - mDropView = view; - final float initialAlpha = view.getAlpha(); - mDropAnim = new ValueAnimator(); + // Fall back to cubic ease out interpolator for the animation if none is specified + TimeInterpolator interpolator = null; if (alphaInterpolator == null || motionInterpolator == null) { - mDropAnim.setInterpolator(mCubicEaseOutInterpolator); + interpolator = mCubicEaseOutInterpolator; } - mDropAnim.setDuration(duration); - mDropAnim.setFloatValues(0.0f, 1.0f); - mDropAnim.removeAllUpdateListeners(); - mDropAnim.addUpdateListener(new AnimatorUpdateListener() { + // Animate the view + final float initAlpha = view.getAlpha(); + final float dropViewScale = view.getScaleX(); + AnimatorUpdateListener updateCb = new AnimatorUpdateListener() { + @Override public void onAnimationUpdate(ValueAnimator animation) { final float percent = (Float) animation.getAnimatedValue(); - // Invalidate the old position - int width = view.getMeasuredWidth(); - int height = view.getMeasuredHeight(); - invalidate(mDropViewPos[0], mDropViewPos[1], - mDropViewPos[0] + width, mDropViewPos[1] + height); + final int width = view.getMeasuredWidth(); + final int height = view.getMeasuredHeight(); float alphaPercent = alphaInterpolator == null ? percent : alphaInterpolator.getInterpolation(percent); float motionPercent = motionInterpolator == null ? percent : motionInterpolator.getInterpolation(percent); - mDropViewPos[0] = from.left + (int) Math.round(((to.left - from.left) * motionPercent)); - mDropViewPos[1] = from.top + (int) Math.round(((to.top - from.top) * motionPercent)); - mDropViewScale = percent * finalScale + (1 - percent); - mDropViewAlpha = alphaPercent * finalAlpha + (1 - alphaPercent) * initialAlpha; - invalidate(mDropViewPos[0], mDropViewPos[1], - mDropViewPos[0] + width, mDropViewPos[1] + height); + float initialScaleX = initScaleX * dropViewScale; + float initialScaleY = initScaleY * dropViewScale; + float scaleX = finalScaleX * percent + initialScaleX * (1 - percent); + float scaleY = finalScaleY * percent + initialScaleY * (1 - percent); + float alpha = finalAlpha * alphaPercent + initAlpha * (1 - alphaPercent); + + float fromLeft = from.left + (initialScaleX - 1f) * width / 2; + float fromTop = from.top + (initialScaleY - 1f) * height / 2; + + int x = (int) (fromLeft + Math.round(((to.left - fromLeft) * motionPercent))); + int y = (int) (fromTop + Math.round(((to.top - fromTop) * motionPercent))); + + int xPos = x - mDropView.getScrollX() + (mAnchorView != null + ? (mAnchorViewInitialScrollX - mAnchorView.getScrollX()) : 0); + int yPos = y - mDropView.getScrollY(); + + mDropView.setTranslationX(xPos); + mDropView.setTranslationY(yPos); + mDropView.setScaleX(scaleX); + mDropView.setScaleY(scaleY); + mDropView.setAlpha(alpha); } - }); + }; + animateView(view, updateCb, duration, interpolator, onCompleteRunnable, animationEndStyle, + anchorView); + } + + public void animateView(final DragView view, AnimatorUpdateListener updateCb, int duration, + TimeInterpolator interpolator, final Runnable onCompleteRunnable, + final int animationEndStyle, View anchorView) { + // Clean up the previous animations + if (mDropAnim != null) mDropAnim.cancel(); + if (mFadeOutAnim != null) mFadeOutAnim.cancel(); + + // Show the drop view if it was previously hidden + mDropView = view; + mDropView.cancelAnimation(); + mDropView.resetLayoutParams(); + + // Set the anchor view if the page is scrolling + if (anchorView != null) { + mAnchorViewInitialScrollX = anchorView.getScrollX(); + } + mAnchorView = anchorView; + + // Create and start the animation + mDropAnim = new ValueAnimator(); + mDropAnim.setInterpolator(interpolator); + mDropAnim.setDuration(duration); + mDropAnim.setFloatValues(0f, 1f); + mDropAnim.addUpdateListener(updateCb); mDropAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { if (onCompleteRunnable != null) { onCompleteRunnable.run(); } - if (fadeOut) { + switch (animationEndStyle) { + case ANIMATION_END_DISAPPEAR: + clearAnimatedView(); + break; + case ANIMATION_END_FADE_OUT: fadeOutDragView(); - } else { - mDropView = null; + break; + case ANIMATION_END_REMAIN_VISIBLE: + break; } } }); mDropAnim.start(); } + public void clearAnimatedView() { + if (mDropAnim != null) { + mDropAnim.cancel(); + } + if (mDropView != null) { + mDragController.onDeferredEndDrag(mDropView); + } + mDropView = null; + invalidate(); + } + + public View getAnimatedView() { + return mDropView; + } + private void fadeOutDragView() { mFadeOutAnim = new ValueAnimator(); mFadeOutAnim.setDuration(150); @@ -596,16 +658,18 @@ public class DragLayer extends FrameLayout { mFadeOutAnim.addUpdateListener(new AnimatorUpdateListener() { public void onAnimationUpdate(ValueAnimator animation) { final float percent = (Float) animation.getAnimatedValue(); - mDropViewAlpha = 1 - percent; - int width = mDropView.getMeasuredWidth(); - int height = mDropView.getMeasuredHeight(); - invalidate(mDropViewPos[0], mDropViewPos[1], - mDropViewPos[0] + width, mDropViewPos[1] + height); + + float alpha = 1 - percent; + mDropView.setAlpha(alpha); } }); mFadeOutAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { + if (mDropView != null) { + mDragController.onDeferredEndDrag(mDropView); + } mDropView = null; + invalidate(); } }); mFadeOutAnim.start(); @@ -654,24 +718,4 @@ public class DragLayer extends FrameLayout { return i; } } - - @Override - protected void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); - if (mDropView != null) { - // We are animating an item that was just dropped on the home screen. - // Render its View in the current animation position. - canvas.save(Canvas.MATRIX_SAVE_FLAG); - final int xPos = mDropViewPos[0] - mDropView.getScrollX(); - final int yPos = mDropViewPos[1] - mDropView.getScrollY(); - int width = mDropView.getMeasuredWidth(); - int height = mDropView.getMeasuredHeight(); - canvas.translate(xPos, yPos); - canvas.translate((1 - mDropViewScale) * width / 2, (1 - mDropViewScale) * height / 2); - canvas.scale(mDropViewScale, mDropViewScale); - mDropView.setAlpha(mDropViewAlpha); - mDropView.draw(canvas); - canvas.restore(); - } - } } diff --git a/src/com/android/launcher2/DragSource.java b/src/com/android/launcher2/DragSource.java index 06f5ee1e6..54404770a 100644 --- a/src/com/android/launcher2/DragSource.java +++ b/src/com/android/launcher2/DragSource.java @@ -25,5 +25,21 @@ import com.android.launcher2.DropTarget.DragObject; * */ public interface DragSource { - void onDropCompleted(View target, DragObject d, boolean success); + /** + * @return whether items dragged from this source supports + */ + boolean supportsFlingToDelete(); + + /** + * A callback specifically made back to the source after an item from this source has been flung + * to be deleted on a DropTarget. In such a situation, this method will be called after + * onDropCompleted, and more importantly, after the fling animation has completed. + */ + void onFlingToDeleteCompleted(); + + /** + * A callback made back to the source after an item from this source has been dropped on a + * DropTarget. + */ + void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, boolean success); } diff --git a/src/com/android/launcher2/DragView.java b/src/com/android/launcher2/DragView.java index dd94175b6..5636f995d 100644 --- a/src/com/android/launcher2/DragView.java +++ b/src/com/android/launcher2/DragView.java @@ -25,6 +25,8 @@ import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Point; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.view.View; import android.view.animation.DecelerateInterpolator; @@ -32,7 +34,10 @@ import android.view.animation.DecelerateInterpolator; import com.android.launcher.R; public class DragView extends View { + private static float sDragAlpha = 1f; + private Bitmap mBitmap; + private Bitmap mCrossFadeBitmap; private Paint mPaint; private int mRegistrationX; private int mRegistrationY; @@ -41,12 +46,12 @@ public class DragView extends View { private Rect mDragRegion = null; private DragLayer mDragLayer = null; private boolean mHasDrawn = false; + private float mCrossFadeProgress = 0f; ValueAnimator mAnim; private float mOffsetX = 0.0f; private float mOffsetY = 0.0f; - - private DragLayer.LayoutParams mLayoutParams; + private float mInitialScale = 1f; /** * Construct the drag view. @@ -60,26 +65,20 @@ public class DragView extends View { * @param registrationY The y coordinate of the registration point. */ public DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY, - int left, int top, int width, int height) { + int left, int top, int width, int height, final float initialScale) { super(launcher); mDragLayer = launcher.getDragLayer(); + mInitialScale = initialScale; final Resources res = getResources(); - final int dragScale = res.getInteger(R.integer.config_dragViewExtraPixels); - - Matrix scale = new Matrix(); - final float scaleFactor = (width + dragScale) / width; - if (scaleFactor != 1.0f) { - scale.setScale(scaleFactor, scaleFactor); - } - - final int offsetX = res.getDimensionPixelSize(R.dimen.dragViewOffsetX); - final int offsetY = res.getDimensionPixelSize(R.dimen.dragViewOffsetY); + final float offsetX = res.getDimensionPixelSize(R.dimen.dragViewOffsetX); + final float offsetY = res.getDimensionPixelSize(R.dimen.dragViewOffsetY); + final float scaleDps = res.getDimensionPixelSize(R.dimen.dragViewScale); + final float scale = (width + scaleDps) / width; // Animate the view into the correct position mAnim = ValueAnimator.ofFloat(0.0f, 1.0f); - mAnim.setDuration(110); - mAnim.setInterpolator(new DecelerateInterpolator(2.5f)); + mAnim.setDuration(150); mAnim.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { @@ -90,19 +89,22 @@ public class DragView extends View { mOffsetX += deltaX; mOffsetY += deltaY; + setScaleX(initialScale + (value * (scale - initialScale))); + setScaleY(initialScale + (value * (scale - initialScale))); + if (sDragAlpha != 1f) { + setAlpha(sDragAlpha * value + (1f - value)); + } if (getParent() == null) { animation.cancel(); } else { - DragLayer.LayoutParams lp = mLayoutParams; - lp.x += deltaX; - lp.y += deltaY; - mDragLayer.requestLayout(); + setTranslationX(getTranslationX() + deltaX); + setTranslationY(getTranslationY() + deltaY); } } }); - mBitmap = Bitmap.createBitmap(bitmap, left, top, width, height, scale, true); + mBitmap = Bitmap.createBitmap(bitmap, left, top, width, height); setDragRegion(new Rect(0, 0, width, height)); // The point in our scaled bitmap that the touch events are located @@ -112,6 +114,7 @@ public class DragView extends View { // Force a measure, because Workspace uses getMeasuredHeight() before the layout pass int ms = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); measure(ms, ms); + mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); } public float getOffsetY() { @@ -150,6 +153,14 @@ public class DragView extends View { return mDragRegion; } + public float getInitialScale() { + return mInitialScale; + } + + public void updateInitialScaleToCurrentScale() { + mInitialScale = getScaleX(); + } + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(mBitmap.getWidth(), mBitmap.getHeight()); @@ -161,16 +172,54 @@ public class DragView extends View { // for debugging Paint p = new Paint(); p.setStyle(Paint.Style.FILL); - p.setColor(0xaaffffff); + p.setColor(0x66ffffff); canvas.drawRect(0, 0, getWidth(), getHeight(), p); } mHasDrawn = true; + boolean crossFade = mCrossFadeProgress > 0 && mCrossFadeBitmap != null; + if (crossFade) { + int alpha = crossFade ? (int) (255 * (1 - mCrossFadeProgress)) : 255; + mPaint.setAlpha(alpha); + } canvas.drawBitmap(mBitmap, 0.0f, 0.0f, mPaint); + if (crossFade) { + mPaint.setAlpha((int) (255 * mCrossFadeProgress)); + canvas.save(); + float sX = (mBitmap.getWidth() * 1.0f) / mCrossFadeBitmap.getWidth(); + float sY = (mBitmap.getHeight() * 1.0f) / mCrossFadeBitmap.getHeight(); + canvas.scale(sX, sY); + canvas.drawBitmap(mCrossFadeBitmap, 0.0f, 0.0f, mPaint); + canvas.restore(); + } + } + + public void setCrossFadeBitmap(Bitmap crossFadeBitmap) { + mCrossFadeBitmap = crossFadeBitmap; + } + + public void crossFade(int duration) { + ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); + va.setDuration(duration); + va.setInterpolator(new DecelerateInterpolator(1.5f)); + va.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mCrossFadeProgress = animation.getAnimatedFraction(); + } + }); + va.start(); } - public void setPaint(Paint paint) { - mPaint = paint; + public void setColor(int color) { + if (mPaint == null) { + mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + } + if (color != 0) { + mPaint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)); + } else { + mPaint.setColorFilter(null); + } invalidate(); } @@ -181,9 +230,6 @@ public class DragView extends View { @Override public void setAlpha(float alpha) { super.setAlpha(alpha); - if (mPaint == null) { - mPaint = new Paint(); - } mPaint.setAlpha((int) (255 * alpha)); invalidate(); } @@ -197,17 +243,32 @@ public class DragView extends View { */ public void show(int touchX, int touchY) { mDragLayer.addView(this); + + // Enable hw-layers on this view + setLayerType(View.LAYER_TYPE_HARDWARE, null); + + // Start the pick-up animation DragLayer.LayoutParams lp = new DragLayer.LayoutParams(0, 0); lp.width = mBitmap.getWidth(); lp.height = mBitmap.getHeight(); - lp.x = touchX - mRegistrationX; - lp.y = touchY - mRegistrationY; lp.customPosition = true; setLayoutParams(lp); - mLayoutParams = lp; + setTranslationX(touchX - mRegistrationX); + setTranslationY(touchY - mRegistrationY); mAnim.start(); } + public void cancelAnimation() { + if (mAnim != null && mAnim.isRunning()) { + mAnim.cancel(); + } + } + + public void resetLayoutParams() { + mOffsetX = mOffsetY = 0; + requestLayout(); + } + /** * Move the window containing this view. * @@ -215,26 +276,17 @@ public class DragView extends View { * @param touchY the y coordinate the user touched in DragLayer coordinates */ void move(int touchX, int touchY) { - DragLayer.LayoutParams lp = mLayoutParams; - lp.x = touchX - mRegistrationX + (int) mOffsetX; - lp.y = touchY - mRegistrationY + (int) mOffsetY; - mDragLayer.requestLayout(); + setTranslationX(touchX - mRegistrationX + (int) mOffsetX); + setTranslationY(touchY - mRegistrationY + (int) mOffsetY); } void remove() { - post(new Runnable() { - public void run() { - mDragLayer.removeView(DragView.this); - } - }); - } + if (getParent() != null) { + // Disable hw-layers on this view + setLayerType(View.LAYER_TYPE_NONE, null); - int[] getPosition(int[] result) { - DragLayer.LayoutParams lp = mLayoutParams; - if (result == null) result = new int[2]; - result[0] = lp.x; - result[1] = lp.y; - return result; + mDragLayer.removeView(DragView.this); + } } } diff --git a/src/com/android/launcher2/DrawableStateProxyView.java b/src/com/android/launcher2/DrawableStateProxyView.java new file mode 100644 index 000000000..498730fdb --- /dev/null +++ b/src/com/android/launcher2/DrawableStateProxyView.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2012 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.launcher2; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import com.android.launcher.R; + +public class DrawableStateProxyView extends LinearLayout { + + private View mView; + private int mViewId; + + public DrawableStateProxyView(Context context) { + this(context, null); + } + + public DrawableStateProxyView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DrawableStateProxyView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DrawableStateProxyView, + defStyle, 0); + mViewId = a.getResourceId(R.styleable.DrawableStateProxyView_sourceViewId, -1); + a.recycle(); + + setFocusable(false); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + + if (mView == null) { + View parent = (View) getParent(); + mView = parent.findViewById(mViewId); + } + mView.setPressed(isPressed()); + mView.setHovered(isHovered()); + } +} diff --git a/src/com/android/launcher2/DropTarget.java b/src/com/android/launcher2/DropTarget.java index 4172da243..fb714c60a 100644 --- a/src/com/android/launcher2/DropTarget.java +++ b/src/com/android/launcher2/DropTarget.java @@ -16,6 +16,7 @@ package com.android.launcher2; +import android.graphics.PointF; import android.graphics.Rect; /** @@ -55,6 +56,9 @@ public interface DropTarget { /** Indicates that the drag operation was cancelled */ public boolean cancelled = false; + /** Defers removing the DragView from the DragLayer until after the drop animation. */ + public boolean deferDragViewCleanupPostAnimation = true; + public DragObject() { } } @@ -89,6 +93,13 @@ public interface DropTarget { void onDragExit(DragObject dragObject); /** + * Handle an object being dropped as a result of flinging to delete and will be called in place + * of onDrop(). (This is only called on objects that are set as the DragController's + * fling-to-delete target. + */ + void onFlingToDelete(DragObject dragObject, int x, int y, PointF vec); + + /** * Allows a DropTarget to delegate drag and drop events to another object. * * Most subclasses will should just return null from this method. diff --git a/src/com/android/launcher2/FastBitmapDrawable.java b/src/com/android/launcher2/FastBitmapDrawable.java index 9fa62da4a..d317d3302 100644 --- a/src/com/android/launcher2/FastBitmapDrawable.java +++ b/src/com/android/launcher2/FastBitmapDrawable.java @@ -29,7 +29,7 @@ class FastBitmapDrawable extends Drawable { private int mAlpha; private int mWidth; private int mHeight; - private final Paint mPaint = new Paint(); + private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); FastBitmapDrawable(Bitmap b) { mAlpha = 255; diff --git a/src/com/android/launcher2/FocusHelper.java b/src/com/android/launcher2/FocusHelper.java index 7807b5dcf..ecc9d9c5d 100644 --- a/src/com/android/launcher2/FocusHelper.java +++ b/src/com/android/launcher2/FocusHelper.java @@ -534,7 +534,7 @@ public class FocusHelper { if (handleKeyEvent) { // Select the first bubble text view in the current page of the workspace final CellLayout layout = (CellLayout) workspace.getChildAt(pageIndex); - final CellLayoutChildren children = layout.getChildrenLayout(); + final ShortcutAndWidgetContainer children = layout.getShortcutsAndWidgets(); final View newIcon = getIconInDirection(layout, children, -1, 1); if (newIcon != null) { newIcon.requestFocus(); @@ -556,9 +556,10 @@ public class FocusHelper { /** * Private helper method to get the CellLayoutChildren given a CellLayout index. */ - private static CellLayoutChildren getCellLayoutChildrenForIndex(ViewGroup container, int i) { + private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex( + ViewGroup container, int i) { ViewGroup parent = (ViewGroup) container.getChildAt(i); - return (CellLayoutChildren) parent.getChildAt(0); + return (ShortcutAndWidgetContainer) parent.getChildAt(0); } /** @@ -664,7 +665,7 @@ public class FocusHelper { * Handles key events in a Workspace containing. */ static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) { - CellLayoutChildren parent = (CellLayoutChildren) v.getParent(); + ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); final CellLayout layout = (CellLayout) parent.getParent(); final Workspace workspace = (Workspace) layout.getParent(); final ViewGroup launcher = (ViewGroup) workspace.getParent(); @@ -819,7 +820,7 @@ public class FocusHelper { * Handles key events for items in a Folder. */ static boolean handleFolderKeyEvent(View v, int keyCode, KeyEvent e) { - CellLayoutChildren parent = (CellLayoutChildren) v.getParent(); + ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); final CellLayout layout = (CellLayout) parent.getParent(); final Folder folder = (Folder) layout.getParent(); View title = folder.mFolderName; diff --git a/src/com/android/launcher2/Folder.java b/src/com/android/launcher2/Folder.java index fdde4d593..c502fb703 100644 --- a/src/com/android/launcher2/Folder.java +++ b/src/com/android/launcher2/Folder.java @@ -20,10 +20,9 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; -import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; import android.content.Context; import android.content.res.Resources; +import android.graphics.PointF; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.text.InputType; @@ -39,14 +38,13 @@ import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.DecelerateInterpolator; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.LinearLayout; import android.widget.TextView; import com.android.launcher.R; +import com.android.launcher2.DropTarget.DragObject; import com.android.launcher2.FolderInfo.FolderListener; import java.util.ArrayList; @@ -74,18 +72,13 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList private final LayoutInflater mInflater; private final IconCache mIconCache; private int mState = STATE_NONE; - private static final int FULL_GROW = 0; - private static final int PARTIAL_GROW = 1; private static final int REORDER_ANIMATION_DURATION = 230; private static final int ON_EXIT_CLOSE_DELAY = 800; - private int mMode = PARTIAL_GROW; private boolean mRearrangeOnClose = false; private FolderIcon mFolderIcon; private int mMaxCountX; private int mMaxCountY; private int mMaxNumItems; - private Rect mNewSize = new Rect(); - private Rect mIconRect = new Rect(); private ArrayList<View> mItemsInReadingOrder = new ArrayList<View>(); private Drawable mIconDrawable; boolean mItemsInvalidated = false; @@ -156,7 +149,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList super.onFinishInflate(); mContent = (CellLayout) findViewById(R.id.folder_content); mContent.setGridSize(0, 0); - mContent.getChildrenLayout().setMotionEventSplittingEnabled(false); + mContent.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false); mFolderName = (FolderEditText) findViewById(R.id.folder_name); mFolderName.setFolder(this); mFolderName.setOnFocusChangeListener(this); @@ -365,21 +358,9 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList */ private void positionAndSizeAsIcon() { if (!(getParent() instanceof DragLayer)) return; - - DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); - - if (mMode == PARTIAL_GROW) { - setScaleX(0.8f); - setScaleY(0.8f); - setAlpha(0f); - } else { - mLauncher.getDragLayer().getDescendantRectRelativeToSelf(mFolderIcon, mIconRect); - lp.width = mIconRect.width(); - lp.height = mIconRect.height(); - lp.x = mIconRect.left; - lp.y = mIconRect.top; - mContent.setAlpha(0); - } + setScaleX(0.8f); + setScaleY(0.8f); + setAlpha(0f); mState = STATE_SMALL; } @@ -387,34 +368,11 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList positionAndSizeAsIcon(); if (!(getParent() instanceof DragLayer)) return; - - ObjectAnimator oa; - DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); - centerAboutIcon(); - if (mMode == PARTIAL_GROW) { - PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1); - PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f); - PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f); - oa = ObjectAnimator.ofPropertyValuesHolder(this, alpha, scaleX, scaleY); - } else { - PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", mNewSize.width()); - PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", mNewSize.height()); - PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", mNewSize.left); - PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", mNewSize.top); - oa = ObjectAnimator.ofPropertyValuesHolder(lp, width, height, x, y); - oa.addUpdateListener(new AnimatorUpdateListener() { - public void onAnimationUpdate(ValueAnimator animation) { - requestLayout(); - } - }); - - PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1.0f); - ObjectAnimator alphaOa = ObjectAnimator.ofPropertyValuesHolder(mContent, alpha); - alphaOa.setDuration(mExpandDuration); - alphaOa.setInterpolator(new AccelerateInterpolator(2.0f)); - alphaOa.start(); - } + PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1); + PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f); + PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f); + ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(this, alpha, scaleX, scaleY); oa.addListener(new AnimatorListenerAdapter() { @Override @@ -457,33 +415,10 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList public void animateClosed() { if (!(getParent() instanceof DragLayer)) return; - - ObjectAnimator oa; - if (mMode == PARTIAL_GROW) { - PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0); - PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 0.9f); - PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 0.9f); - oa = ObjectAnimator.ofPropertyValuesHolder(this, alpha, scaleX, scaleY); - } else { - DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); - - PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", mIconRect.width()); - PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", mIconRect.height()); - PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", mIconRect.left); - PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", mIconRect.top); - oa = ObjectAnimator.ofPropertyValuesHolder(lp, width, height, x, y); - oa.addUpdateListener(new AnimatorUpdateListener() { - public void onAnimationUpdate(ValueAnimator animation) { - requestLayout(); - } - }); - - PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0f); - ObjectAnimator alphaOa = ObjectAnimator.ofPropertyValuesHolder(mContent, alpha); - alphaOa.setDuration(mExpandDuration); - alphaOa.setInterpolator(new DecelerateInterpolator(2.0f)); - alphaOa.start(); - } + PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0); + PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 0.9f); + PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 0.9f); + ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(this, alpha, scaleX, scaleY); oa.addListener(new AnimatorListenerAdapter() { @Override @@ -594,7 +529,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList for (int x = startX; x <= endX; x++) { View v = mContent.getChildAt(x,y); if (mContent.animateChildToPosition(v, empty[0], empty[1], - REORDER_ANIMATION_DURATION, delay)) { + REORDER_ANIMATION_DURATION, delay, true, true)) { empty[0] = x; empty[1] = y; delay += delayAmount; @@ -611,7 +546,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList for (int x = startX; x >= endX; x--) { View v = mContent.getChildAt(x,y); if (mContent.animateChildToPosition(v, empty[0], empty[1], - REORDER_ANIMATION_DURATION, delay)) { + REORDER_ANIMATION_DURATION, delay, true, true)) { empty[0] = x; empty[1] = y; delay += delayAmount; @@ -685,7 +620,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mReorderAlarm.cancelAlarm(); } - public void onDropCompleted(View target, DragObject d, boolean success) { + public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, + boolean success) { if (success) { if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon) { replaceFolderWithFinalItem(); @@ -719,6 +655,20 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList updateItemLocationsInDatabase(); } + @Override + public boolean supportsFlingToDelete() { + return true; + } + + public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { + // Do nothing + } + + @Override + public void onFlingToDeleteCompleted() { + // Do nothing + } + private void updateItemLocationsInDatabase() { ArrayList<View> list = getItemsInReadingOrder(); for (int i = 0; i < list.size(); i++) { @@ -791,11 +741,17 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList int centeredLeft = centerX - width / 2; int centeredTop = centerY - height / 2; + int currentPage = mLauncher.getWorkspace().getCurrentPage(); + // In case the workspace is scrolling, we need to use the final scroll to compute + // the folders bounds. + mLauncher.getWorkspace().setFinalScrollForPageChange(currentPage); // We first fetch the currently visible CellLayoutChildren - CellLayout currentPage = mLauncher.getWorkspace().getCurrentDropLayout(); - CellLayoutChildren boundingLayout = currentPage.getChildrenLayout(); + CellLayout currentLayout = (CellLayout) mLauncher.getWorkspace().getChildAt(currentPage); + ShortcutAndWidgetContainer boundingLayout = currentLayout.getShortcutsAndWidgets(); Rect bounds = new Rect(); parent.getDescendantRectRelativeToSelf(boundingLayout, bounds); + // We reset the workspaces scroll + mLauncher.getWorkspace().resetFinalScrollForPageChange(currentPage); // We need to bound the folder to the currently visible CellLayoutChildren int left = Math.min(Math.max(bounds.left, centeredLeft), @@ -821,14 +777,10 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mFolderIcon.setPivotX(folderIconPivotX); mFolderIcon.setPivotY(folderIconPivotY); - if (mMode == PARTIAL_GROW) { - lp.width = width; - lp.height = height; - lp.x = left; - lp.y = top; - } else { - mNewSize.set(left, top, left + width, top + height); - } + lp.width = width; + lp.height = height; + lp.x = left; + lp.y = top; } private void setupContentForNumItems(int count) { @@ -886,11 +838,11 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } public int getItemCount() { - return mContent.getChildrenLayout().getChildCount(); + return mContent.getShortcutsAndWidgets().getChildCount(); } public View getItemAt(int index) { - return mContent.getChildrenLayout().getChildAt(index); + return mContent.getShortcutsAndWidgets().getChildAt(index); } private void onCloseComplete() { diff --git a/src/com/android/launcher2/FolderIcon.java b/src/com/android/launcher2/FolderIcon.java index 3c0829d4a..c005edfcd 100644 --- a/src/com/android/launcher2/FolderIcon.java +++ b/src/com/android/launcher2/FolderIcon.java @@ -30,6 +30,7 @@ import android.graphics.drawable.Drawable; import android.os.Parcelable; import android.util.AttributeSet; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.AccelerateInterpolator; @@ -53,6 +54,8 @@ public class FolderIcon extends LinearLayout implements FolderListener { FolderInfo mInfo; private static boolean sStaticValuesDirty = true; + private CheckLongPressHelper mLongPressHelper; + // The number of icons to display in the private static final int NUM_ITEMS_IN_PREVIEW = 3; private static final int CONSUMPTION_ANIMATION_DURATION = 100; @@ -95,10 +98,16 @@ public class FolderIcon extends LinearLayout implements FolderListener { public FolderIcon(Context context, AttributeSet attrs) { super(context, attrs); + init(); } public FolderIcon(Context context) { super(context); + init(); + } + + private void init() { + mLongPressHelper = new CheckLongPressHelper(this); } public boolean isDropEnabled() { @@ -295,14 +304,14 @@ public class FolderIcon extends LinearLayout implements FolderListener { } public void performCreateAnimation(final ShortcutInfo destInfo, final View destView, - final ShortcutInfo srcInfo, final View srcView, Rect dstRect, + final ShortcutInfo srcInfo, final DragView srcView, Rect dstRect, float scaleRelativeToDragLayer, Runnable postAnimationRunnable) { Drawable animateDrawable = ((TextView) destView).getCompoundDrawables()[1]; computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(), destView.getMeasuredWidth()); // This will animate the dragView (srcView) into the new folder - onDrop(srcInfo, srcView, dstRect, scaleRelativeToDragLayer, 1, postAnimationRunnable); + onDrop(srcInfo, srcView, dstRect, scaleRelativeToDragLayer, 1, postAnimationRunnable, null); // This will animate the first item from it's position as an icon into its // position as the first item in the preview @@ -320,8 +329,9 @@ public class FolderIcon extends LinearLayout implements FolderListener { mFolderRingAnimator.animateToNaturalState(); } - private void onDrop(final ShortcutInfo item, View animateView, Rect finalRect, - float scaleRelativeToDragLayer, int index, Runnable postAnimationRunnable) { + private void onDrop(final ShortcutInfo item, DragView animateView, Rect finalRect, + float scaleRelativeToDragLayer, int index, Runnable postAnimationRunnable, + DragObject d) { item.cellX = -1; item.cellY = -1; @@ -359,10 +369,11 @@ public class FolderIcon extends LinearLayout implements FolderListener { float finalAlpha = index < NUM_ITEMS_IN_PREVIEW ? 0.5f : 0f; + float finalScale = scale * scaleRelativeToDragLayer; dragLayer.animateView(animateView, from, to, finalAlpha, - scale * scaleRelativeToDragLayer, DROP_IN_ANIMATION_DURATION, + 1, 1, finalScale, finalScale, DROP_IN_ANIMATION_DURATION, new DecelerateInterpolator(2), new AccelerateInterpolator(2), - postAnimationRunnable, false); + postAnimationRunnable, DragLayer.ANIMATION_END_DISAPPEAR, null); postDelayed(new Runnable() { public void run() { addItem(item); @@ -382,7 +393,7 @@ public class FolderIcon extends LinearLayout implements FolderListener { item = (ShortcutInfo) d.dragInfo; } mFolder.notifyDrop(); - onDrop(item, d.dragView, null, 1.0f, mInfo.contents.size(), d.postAnimationRunnable); + onDrop(item, d.dragView, null, 1.0f, mInfo.contents.size(), d.postAnimationRunnable, d); } public DropTarget getDropTargetDelegate(DragObject d) { @@ -589,4 +600,29 @@ public class FolderIcon extends LinearLayout implements FolderListener { setContentDescription(String.format(mContext.getString(R.string.folder_name_format), title)); } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // Call the superclass onTouchEvent first, because sometimes it changes the state to + // isPressed() on an ACTION_UP + boolean result = super.onTouchEvent(event); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mLongPressHelper.postCheckForLongPress(); + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + mLongPressHelper.cancelLongPress(); + break; + } + return result; + } + + @Override + public void cancelLongPress() { + super.cancelLongPress(); + + mLongPressHelper.cancelLongPress(); + } } diff --git a/src/com/android/launcher2/HandleView.java b/src/com/android/launcher2/HandleView.java index 13d07e2dd..d77138b0a 100644 --- a/src/com/android/launcher2/HandleView.java +++ b/src/com/android/launcher2/HandleView.java @@ -17,13 +17,12 @@ package com.android.launcher2; -import android.widget.ImageView; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.MotionEvent; -import android.view.KeyEvent; import android.view.View; +import android.widget.ImageView; import com.android.launcher.R; diff --git a/src/com/android/launcher2/HolographicPagedViewIcon.java b/src/com/android/launcher2/HolographicPagedViewIcon.java deleted file mode 100644 index dda233e37..000000000 --- a/src/com/android/launcher2/HolographicPagedViewIcon.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2010 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.launcher2; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.widget.TextView; - - - -/** - * An icon on a PagedView, specifically for items in the launcher's paged view (with compound - * drawables on the top). - */ -public class HolographicPagedViewIcon extends TextView { - PagedViewIcon mOriginalIcon; - Paint mPaint; - - public HolographicPagedViewIcon(Context context, PagedViewIcon original) { - super(context); - mOriginalIcon = original; - mPaint = new Paint(); - } - - @Override - protected void onDraw(Canvas canvas) { - Bitmap overlay = mOriginalIcon.getHolographicOutline(); - if (overlay != null) { - final int offset = getScrollX(); - final int compoundPaddingLeft = getCompoundPaddingLeft(); - final int compoundPaddingRight = getCompoundPaddingRight(); - int hspace = getWidth() - compoundPaddingRight - compoundPaddingLeft; - canvas.drawBitmap(overlay, - offset + compoundPaddingLeft + (hspace - overlay.getWidth()) / 2, - mOriginalIcon.getPaddingTop(), - mPaint); - } - } -} diff --git a/src/com/android/launcher2/Hotseat.java b/src/com/android/launcher2/Hotseat.java index f7fa38007..9e525bd5a 100644 --- a/src/com/android/launcher2/Hotseat.java +++ b/src/com/android/launcher2/Hotseat.java @@ -29,13 +29,13 @@ import com.android.launcher.R; public class Hotseat extends FrameLayout { private static final String TAG = "Hotseat"; - private static final int sAllAppsButtonRank = 2; // In the middle of the dock private Launcher mLauncher; private CellLayout mContent; private int mCellCountX; private int mCellCountY; + private int mAllAppsButtonRank; private boolean mIsLandscape; public Hotseat(Context context) { @@ -53,6 +53,7 @@ public class Hotseat extends FrameLayout { R.styleable.Hotseat, defStyle, 0); mCellCountX = a.getInt(R.styleable.Hotseat_cellCountX, -1); mCellCountY = a.getInt(R.styleable.Hotseat_cellCountY, -1); + mAllAppsButtonRank = context.getResources().getInteger(R.integer.hotseat_all_apps_index); mIsLandscape = context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; } @@ -77,8 +78,8 @@ public class Hotseat extends FrameLayout { int getCellYFromOrder(int rank) { return mIsLandscape ? (mContent.getCountY() - (rank + 1)) : 0; } - public static boolean isAllAppsButtonRank(int rank) { - return rank == sAllAppsButtonRank; + public boolean isAllAppsButtonRank(int rank) { + return rank == mAllAppsButtonRank; } @Override @@ -88,6 +89,7 @@ public class Hotseat extends FrameLayout { if (mCellCountY < 0) mCellCountY = LauncherModel.getCellCountY(); mContent = (CellLayout) findViewById(R.id.layout); mContent.setGridSize(mCellCountX, mCellCountY); + mContent.setIsHotseat(true); resetLayout(); } @@ -126,9 +128,10 @@ public class Hotseat extends FrameLayout { // Note: We do this to ensure that the hotseat is always laid out in the orientation of // the hotseat in order regardless of which orientation they were added - int x = getCellXFromOrder(sAllAppsButtonRank); - int y = getCellYFromOrder(sAllAppsButtonRank); - mContent.addViewToCellLayout(allAppsButton, -1, 0, new CellLayout.LayoutParams(x,y,1,1), - true); + int x = getCellXFromOrder(mAllAppsButtonRank); + int y = getCellYFromOrder(mAllAppsButtonRank); + CellLayout.LayoutParams lp = new CellLayout.LayoutParams(x,y,1,1); + lp.canReorder = false; + mContent.addViewToCellLayout(allAppsButton, -1, 0, lp, true, true); } } diff --git a/src/com/android/launcher2/IconCache.java b/src/com/android/launcher2/IconCache.java index 2430a6b33..1e8379d3c 100644 --- a/src/com/android/launcher2/IconCache.java +++ b/src/com/android/launcher2/IconCache.java @@ -16,7 +16,9 @@ package com.android.launcher2; +import android.app.ActivityManager; import android.content.ComponentName; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -24,7 +26,6 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.Drawable; -import android.util.DisplayMetrics; import java.util.HashMap; @@ -49,23 +50,13 @@ public class IconCache { private int mIconDpi; public IconCache(LauncherApplication context) { + ActivityManager activityManager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + mContext = context; mPackageManager = context.getPackageManager(); - int density = context.getResources().getDisplayMetrics().densityDpi; - if (LauncherApplication.isScreenLarge()) { - if (density == DisplayMetrics.DENSITY_LOW) { - mIconDpi = DisplayMetrics.DENSITY_MEDIUM; - } else if (density == DisplayMetrics.DENSITY_MEDIUM) { - mIconDpi = DisplayMetrics.DENSITY_HIGH; - } else if (density == DisplayMetrics.DENSITY_HIGH) { - mIconDpi = DisplayMetrics.DENSITY_XHIGH; - } else if (density == DisplayMetrics.DENSITY_XHIGH) { - // We'll need to use a denser icon, or some sort of a mipmap - mIconDpi = DisplayMetrics.DENSITY_XHIGH; - } - } else { - mIconDpi = context.getResources().getDisplayMetrics().densityDpi; - } + mIconDpi = activityManager.getLauncherLargeIconDensity(); + // need to set mIconDpi before getting default icon mDefaultIcon = makeDefaultIcon(); } diff --git a/src/com/android/launcher2/InfoDropTarget.java b/src/com/android/launcher2/InfoDropTarget.java index dba845b04..2e0b5c827 100644 --- a/src/com/android/launcher2/InfoDropTarget.java +++ b/src/com/android/launcher2/InfoDropTarget.java @@ -21,8 +21,6 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.TransitionDrawable; import android.util.AttributeSet; import android.view.View; @@ -34,7 +32,6 @@ public class InfoDropTarget extends ButtonDropTarget { private ColorStateList mOriginalTextColor; private TransitionDrawable mDrawable; - private int mHoverColor = 0xFF0000FF; public InfoDropTarget(Context context, AttributeSet attrs) { this(context, attrs, 0); @@ -53,8 +50,6 @@ public class InfoDropTarget extends ButtonDropTarget { // Get the hover color Resources r = getResources(); mHoverColor = r.getColor(R.color.info_target_hover_tint); - mHoverPaint.setColorFilter(new PorterDuffColorFilter( - mHoverColor, PorterDuff.Mode.SRC_ATOP)); mDrawable = (TransitionDrawable) getCompoundDrawables()[0]; mDrawable.setCrossFadeEnabled(true); @@ -85,6 +80,9 @@ public class InfoDropTarget extends ButtonDropTarget { if (componentName != null) { mLauncher.startApplicationDetailsActivity(componentName); } + + // There is no post-drop animation, so clean up the DragView now + d.deferDragViewCleanupPostAnimation = false; return false; } diff --git a/src/com/android/launcher2/InstallShortcutReceiver.java b/src/com/android/launcher2/InstallShortcutReceiver.java index ed8f29942..4c0974fd3 100644 --- a/src/com/android/launcher2/InstallShortcutReceiver.java +++ b/src/com/android/launcher2/InstallShortcutReceiver.java @@ -16,18 +16,32 @@ package com.android.launcher2; -import java.util.ArrayList; - import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; import android.widget.Toast; import com.android.launcher.R; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + public class InstallShortcutReceiver extends BroadcastReceiver { public static final String ACTION_INSTALL_SHORTCUT = "com.android.launcher.action.INSTALL_SHORTCUT"; + public static final String NEW_APPS_PAGE_KEY = "apps.new.page"; + public static final String NEW_APPS_LIST_KEY = "apps.new.list"; + + public static final int NEW_SHORTCUT_BOUNCE_DURATION = 450; + public static final int NEW_SHORTCUT_STAGGER_DELAY = 75; + + private static final int INSTALL_SHORTCUT_SUCCESSFUL = 0; + private static final int INSTALL_SHORTCUT_IS_DUPLICATE = -1; + private static final int INSTALL_SHORTCUT_NO_SPACE = -2; // A mime-type representing shortcut data public static final String SHORTCUT_MIMETYPE = @@ -39,22 +53,58 @@ public class InstallShortcutReceiver extends BroadcastReceiver { if (!ACTION_INSTALL_SHORTCUT.equals(data.getAction())) { return; } + String spKey = LauncherApplication.getSharedPreferencesKey(); + SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE); + + final int screen = Launcher.getScreen(); + final Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); + if (intent == null) { + return; + } + // This name is only used for comparisons and notifications, so fall back to activity name + // if not supplied + String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); + if (name == null) { + try { + PackageManager pm = context.getPackageManager(); + ActivityInfo info = pm.getActivityInfo(intent.getComponent(), 0); + name = info.loadLabel(pm).toString(); + } catch (PackageManager.NameNotFoundException nnfe) { + return; + } + } - int screen = Launcher.getScreen(); + final ArrayList<ItemInfo> items = LauncherModel.getItemsInLocalCoordinates(context); + final boolean exists = LauncherModel.shortcutExists(context, name, intent); + final int[] result = {INSTALL_SHORTCUT_SUCCESSFUL}; + + // Try adding the target to the workspace screens incrementally, starting at the current + // screen and alternating between +1, -1, +2, -2, etc. (using ~ ceil(i/2f)*(-1)^(i-1)) + boolean found = false; + for (int i = 0; i < (2 * Launcher.SCREEN_COUNT) + 1 && !found; ++i) { + int si = screen + (int) ((i / 2f) + 0.5f) * ((i % 2 == 1) ? 1 : -1); + if (0 <= si && si < Launcher.SCREEN_COUNT) { + found = installShortcut(context, data, items, name, intent, si, exists, sp, result); + } + } - if (!installShortcut(context, data, screen)) { - // The target screen is full, let's try the other screens - for (int i = 0; i < Launcher.SCREEN_COUNT; i++) { - if (i != screen && installShortcut(context, data, i)) break; + // We only report error messages (duplicate shortcut or out of space) as the add-animation + // will provide feedback otherwise + if (!found) { + if (result[0] == INSTALL_SHORTCUT_NO_SPACE) { + Toast.makeText(context, context.getString(R.string.out_of_space), + Toast.LENGTH_SHORT).show(); + } else if (result[0] == INSTALL_SHORTCUT_IS_DUPLICATE) { + Toast.makeText(context, context.getString(R.string.shortcut_duplicate, name), + Toast.LENGTH_SHORT).show(); } } } - private boolean installShortcut(Context context, Intent data, int screen) { - String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); - - if (findEmptyCell(context, mCoordinates, screen)) { - Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); + private boolean installShortcut(Context context, Intent data, ArrayList<ItemInfo> items, + String name, Intent intent, int screen, boolean shortcutExists, + SharedPreferences sharedPrefs, int[] result) { + if (findEmptyCell(context, items, mCoordinates, screen)) { if (intent != null) { if (intent.getAction() == null) { intent.setAction(Intent.ACTION_VIEW); @@ -63,38 +113,47 @@ public class InstallShortcutReceiver extends BroadcastReceiver { // By default, we allow for duplicate entries (located in // different places) boolean duplicate = data.getBooleanExtra(Launcher.EXTRA_SHORTCUT_DUPLICATE, true); - if (duplicate || !LauncherModel.shortcutExists(context, name, intent)) { + if (duplicate || !shortcutExists) { + // If the new app is going to fall into the same page as before, then just + // continue adding to the current page + int newAppsScreen = sharedPrefs.getInt(NEW_APPS_PAGE_KEY, screen); + Set<String> newApps = new HashSet<String>(); + if (newAppsScreen == screen) { + newApps = sharedPrefs.getStringSet(NEW_APPS_LIST_KEY, newApps); + } + newApps.add(intent.toUri(0).toString()); + sharedPrefs.edit() + .putInt(NEW_APPS_PAGE_KEY, screen) + .putStringSet(NEW_APPS_LIST_KEY, newApps) + .commit(); + + // Update the Launcher db LauncherApplication app = (LauncherApplication) context.getApplicationContext(); ShortcutInfo info = app.getModel().addShortcut(context, data, - LauncherSettings.Favorites.CONTAINER_DESKTOP, screen, mCoordinates[0], - mCoordinates[1], true); - if (info != null) { - Toast.makeText(context, context.getString(R.string.shortcut_installed, name), - Toast.LENGTH_SHORT).show(); - } else { + LauncherSettings.Favorites.CONTAINER_DESKTOP, screen, + mCoordinates[0], mCoordinates[1], true); + if (info == null) { return false; } } else { - Toast.makeText(context, context.getString(R.string.shortcut_duplicate, name), - Toast.LENGTH_SHORT).show(); + result[0] = INSTALL_SHORTCUT_IS_DUPLICATE; } return true; } } else { - Toast.makeText(context, context.getString(R.string.out_of_space), - Toast.LENGTH_SHORT).show(); + result[0] = INSTALL_SHORTCUT_NO_SPACE; } return false; } - private static boolean findEmptyCell(Context context, int[] xy, int screen) { + private static boolean findEmptyCell(Context context, ArrayList<ItemInfo> items, int[] xy, + int screen) { final int xCount = LauncherModel.getCellCountX(); final int yCount = LauncherModel.getCellCountY(); boolean[][] occupied = new boolean[xCount][yCount]; - ArrayList<ItemInfo> items = LauncherModel.getItemsInLocalCoordinates(context); ItemInfo item = null; int cellX, cellY, spanX, spanY; for (int i = 0; i < items.size(); ++i) { diff --git a/src/com/android/launcher2/InstallWidgetReceiver.java b/src/com/android/launcher2/InstallWidgetReceiver.java index 6b3763ce0..a1e9b1187 100644 --- a/src/com/android/launcher2/InstallWidgetReceiver.java +++ b/src/com/android/launcher2/InstallWidgetReceiver.java @@ -189,7 +189,7 @@ public class InstallWidgetReceiver { final PendingAddWidgetInfo createInfo = new PendingAddWidgetInfo(widgetInfo, mMimeType, mClipData); mLauncher.addAppWidgetFromDrop(createInfo, LauncherSettings.Favorites.CONTAINER_DESKTOP, - mTargetLayoutScreen, null, mTargetLayoutPos); + mTargetLayoutScreen, null, null, mTargetLayoutPos); } } } diff --git a/src/com/android/launcher2/ItemInfo.java b/src/com/android/launcher2/ItemInfo.java index 8d4662495..11a6c0d00 100644 --- a/src/com/android/launcher2/ItemInfo.java +++ b/src/com/android/launcher2/ItemInfo.java @@ -77,6 +77,15 @@ class ItemInfo { int spanY = 1; /** + * Indicates the minimum X cell span. + */ + int minSpanX = 1; + + /** + * Indicates the minimum Y cell span. + */ + int minSpanY = 1; + /** * Indicates whether the item is a gesture. */ boolean isGesture = false; diff --git a/src/com/android/launcher2/Launcher.java b/src/com/android/launcher2/Launcher.java index 8e074b53f..b3ce96884 100644 --- a/src/com/android/launcher2/Launcher.java +++ b/src/com/android/launcher2/Launcher.java @@ -23,12 +23,12 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; import android.app.Activity; import android.app.ActivityManager; import android.app.AlertDialog; import android.app.Dialog; import android.app.SearchManager; -import android.app.StatusBarManager; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; @@ -54,8 +54,8 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; -import android.os.Build; import android.os.Bundle; +import android.os.Debug; import android.os.Environment; import android.os.Handler; import android.os.Message; @@ -80,10 +80,12 @@ import android.view.View; import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.view.ViewTreeObserver; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.AccelerateInterpolator; +import android.view.animation.BounceInterpolator; import android.view.animation.DecelerateInterpolator; import android.view.inputmethod.InputMethodManager; import android.widget.Advanceable; @@ -103,7 +105,12 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; /** * Default launcher application. @@ -139,6 +146,7 @@ public final class Launcher extends Activity static final int DIALOG_RENAME_FOLDER = 2; private static final String PREFERENCES = "launcher.preferences"; + static final String FORCE_ENABLE_ROTATION_PROPERTY = "launcher.force_enable_rotation"; // Type: int private static final String RUNTIME_STATE_CURRENT_SCREEN = "launcher.current_screen"; @@ -246,7 +254,16 @@ public final class Launcher extends Activity private static Drawable.ConstantState[] sAppMarketIcon = new Drawable.ConstantState[2]; static final ArrayList<String> sDumpLogs = new ArrayList<String>(); + PendingAddWidgetInfo mWidgetBeingConfigured = null; + // We only want to get the SharedPreferences once since it does an FS stat each time we get + // it from the context. + private SharedPreferences mSharedPrefs; + + // Holds the page that we need to animate to, and the icon views that we need to animate up + // when we scroll to that page on resume. + private int mNewShortcutAnimatePage = -1; + private ArrayList<View> mNewShortcutAnimateViews = new ArrayList<View>(); private BubbleTextView mWaitingForResume; @@ -274,6 +291,8 @@ public final class Launcher extends Activity protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); LauncherApplication app = ((LauncherApplication)getApplication()); + mSharedPrefs = getSharedPreferences(LauncherApplication.getSharedPreferencesKey(), + Context.MODE_PRIVATE); mModel = app.setLauncher(this); mIconCache = app.getIconCache(); mDragController = new DragController(this); @@ -310,7 +329,7 @@ public final class Launcher extends Activity } if (!mRestoring) { - mModel.startLoader(this, true); + mModel.startLoader(true); } if (!mModel.isAllAppsLoaded()) { @@ -348,8 +367,11 @@ public final class Launcher extends Activity } mSearchDropTargetBar.onSearchPackagesChanged(searchVisible, voiceVisible); + final String forceEnableRotation = + SystemProperties.get(FORCE_ENABLE_ROTATION_PROPERTY, "false"); + // On large interfaces, we want the screen to auto-rotate based on the current orientation - if (LauncherApplication.isScreenLarge() || Build.TYPE.contentEquals("eng")) { + if (LauncherApplication.isScreenLarge() || "true".equalsIgnoreCase(forceEnableRotation)) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); } } @@ -495,24 +517,35 @@ public final class Launcher extends Activity break; case REQUEST_CREATE_APPWIDGET: int appWidgetId = args.intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); - completeAddAppWidget(appWidgetId, args.container, args.screen); + completeAddAppWidget(appWidgetId, args.container, args.screen, null, null); result = true; break; case REQUEST_PICK_WALLPAPER: // We just wanted the activity result here so we can clear mWaitingForResult break; } - // In any situation where we have a multi-step drop, we should reset the add info only after - // we complete the drop + // Before adding this resetAddInfo(), after a shortcut was added to a workspace screen, + // if you turned the screen off and then back while in All Apps, Launcher would not + // return to the workspace. Clearing mAddInfo.container here fixes this issue resetAddInfo(); return result; } @Override - protected void onActivityResult(final int requestCode, int resultCode, final Intent data) { + protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) { boolean delayExitSpringLoadedMode = false; + boolean isWidgetDrop = (requestCode == REQUEST_PICK_APPWIDGET || + requestCode == REQUEST_CREATE_APPWIDGET); mWaitingForResult = false; + // We have special handling for widgets + if (isWidgetDrop) { + int appWidgetId = data != null ? + data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) : -1; + completeTwoStageWidgetDrop(resultCode, appWidgetId); + return; + } + // The pattern used here is that a user PICKs a specific application, // which, depending on the target, might need to CREATE the actual target. @@ -526,71 +559,75 @@ public final class Launcher extends Activity args.screen = mPendingAddInfo.screen; args.cellX = mPendingAddInfo.cellX; args.cellY = mPendingAddInfo.cellY; - - // If the loader is still running, defer the add until it is done. if (isWorkspaceLocked()) { sPendingAddList.add(args); } else { delayExitSpringLoadedMode = completeAdd(args); } - } else if ((requestCode == REQUEST_PICK_APPWIDGET || - requestCode == REQUEST_CREATE_APPWIDGET) && resultCode == RESULT_CANCELED) { - if (data != null) { - // Clean up the appWidgetId if we canceled - int appWidgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); - if (appWidgetId != -1) { - mAppWidgetHost.deleteAppWidgetId(appWidgetId); - } - } } - + mDragLayer.clearAnimatedView(); // Exit spring loaded mode if necessary after cancelling the configuration of a widget - exitSpringLoadedDragModeDelayed((resultCode != RESULT_CANCELED), delayExitSpringLoadedMode); + exitSpringLoadedDragModeDelayed((resultCode != RESULT_CANCELED), delayExitSpringLoadedMode, + null); + } + + private void completeTwoStageWidgetDrop(final int resultCode, final int appWidgetId) { + CellLayout cellLayout = (CellLayout) mWorkspace.getChildAt(mWidgetBeingConfigured.screen); + Runnable onCompleteRunnable = null; + int animationType = 0; + + if (resultCode == RESULT_OK) { + animationType = Workspace.COMPLETE_TWO_STAGE_WIDGET_DROP_ANIMATION; + final AppWidgetHostView layout = mAppWidgetHost.createView(this, appWidgetId, + mWidgetBeingConfigured.info); + mWidgetBeingConfigured.boundWidget = layout; + onCompleteRunnable = new Runnable() { + @Override + public void run() { + completeAddAppWidget(appWidgetId, mPendingAddInfo.container, + mPendingAddInfo.screen, layout, null); + exitSpringLoadedDragModeDelayed((resultCode != RESULT_CANCELED), false, + null); + } + }; + } else if (resultCode == RESULT_CANCELED) { + animationType = Workspace.CANCEL_TWO_STAGE_WIDGET_DROP_ANIMATION; + onCompleteRunnable = new Runnable() { + @Override + public void run() { + exitSpringLoadedDragModeDelayed((resultCode != RESULT_CANCELED), false, + null); + } + }; + } + mWorkspace.animateWidgetDrop(mWidgetBeingConfigured, cellLayout, + (DragView) mDragLayer.getAnimatedView(), onCompleteRunnable, + animationType, mWidgetBeingConfigured.boundWidget, true); + mWidgetBeingConfigured = null; } @Override protected void onResume() { super.onResume(); + mPaused = false; if (mRestoring || mOnResumeNeedsLoad) { mWorkspaceLoading = true; - mModel.startLoader(this, true); + mModel.startLoader(true); mRestoring = false; mOnResumeNeedsLoad = false; } + + // Reset the pressed state of icons that were locked in the press state while activities + // were launching if (mWaitingForResume != null) { + // Resets the previous workspace icon press state mWaitingForResume.setStayPressed(false); } - // When we resume Launcher, a different Activity might be responsible for the app - // market intent, so refresh the icon - updateAppMarketIcon(); - mAppsCustomizeTabHost.onResume(); - if (!mWorkspaceLoading) { - final ViewTreeObserver observer = mWorkspace.getViewTreeObserver(); - final Workspace workspace = mWorkspace; - // We want to let Launcher draw itself at least once before we force it to build - // layers on all the workspace pages, so that transitioning to Launcher from other - // apps is nice and speedy. Usually the first call to preDraw doesn't correspond to - // a true draw so we wait until the second preDraw call to be safe - observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { - boolean mFirstTime = true; - public boolean onPreDraw() { - if (mFirstTime) { - mFirstTime = false; - } else { - // We delay the layer building a bit in order to give - // other message processing a time to run. In particular - // this avoids a delay in hiding the IME if it was - // currently shown, because doing that may involve - // some communication back with the app. - workspace.postDelayed(mBuildLayersRunnable, 500); - observer.removeOnPreDrawListener(this); - } - return true; - } - }); + if (mAppsCustomizeContent != null) { + // Resets the previous all apps icon press state + mAppsCustomizeContent.resetDrawableState(); } - clearTypedText(); } @Override @@ -705,7 +742,7 @@ public final class Launcher extends Activity showAllApps(false); } - final int currentScreen = savedState.getInt(RUNTIME_STATE_CURRENT_SCREEN, -1); + int currentScreen = savedState.getInt(RUNTIME_STATE_CURRENT_SCREEN, -1); if (currentScreen > -1) { mWorkspace.setCurrentPage(currentScreen); } @@ -848,7 +885,7 @@ public final class Launcher extends Activity cellXY[0] = cellX; cellXY[1] = cellY; } else if (!layout.findCellForSpan(cellXY, 1, 1)) { - showOutOfSpaceMessage(); + showOutOfSpaceMessage(isHotseatLayout(layout)); return; } @@ -892,13 +929,14 @@ public final class Launcher extends Activity foundCellSpan = true; // If appropriate, either create a folder or add to an existing folder - if (mWorkspace.createUserFolderIfNecessary(view, container, layout, cellXY, + if (mWorkspace.createUserFolderIfNecessary(view, container, layout, cellXY, 0, true, null,null)) { return; } DragObject dragObject = new DragObject(); dragObject.dragInfo = info; - if (mWorkspace.addToExistingFolderIfNecessary(view, layout, cellXY, dragObject, true)) { + if (mWorkspace.addToExistingFolderIfNecessary(view, layout, cellXY, 0, dragObject, + true)) { return; } } else if (touchXY != null) { @@ -910,7 +948,7 @@ public final class Launcher extends Activity } if (!foundCellSpan) { - showOutOfSpaceMessage(); + showOutOfSpaceMessage(isHotseatLayout(layout)); return; } @@ -939,7 +977,7 @@ public final class Launcher extends Activity return getSpanForWidget(info.provider, info.minWidth, info.minHeight, spanXY); } - int[] getMinResizeSpanForWidget(AppWidgetProviderInfo info, int[] spanXY) { + int[] getMinSpanForWidget(AppWidgetProviderInfo info, int[] spanXY) { return getSpanForWidget(info.provider, info.minResizeWidth, info.minResizeHeight, spanXY); } @@ -947,18 +985,27 @@ public final class Launcher extends Activity return getSpanForWidget(info.componentName, info.minWidth, info.minHeight, spanXY); } + int[] getMinSpanForWidget(PendingAddWidgetInfo info, int[] spanXY) { + return getSpanForWidget(info.componentName, info.minResizeWidth, + info.minResizeHeight, spanXY); + } + /** * Add a widget to the workspace. * * @param appWidgetId The app widget id * @param cellInfo The position on screen where to create the widget. */ - private void completeAddAppWidget(final int appWidgetId, long container, int screen) { - AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId); + private void completeAddAppWidget(final int appWidgetId, long container, int screen, + AppWidgetHostView hostView, AppWidgetProviderInfo appWidgetInfo) { + if (appWidgetInfo == null) { + appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId); + } // Calculate the grid spans needed to fit this widget CellLayout layout = getCellLayout(container, screen); + int[] minSpanXY = getMinSpanForWidget(appWidgetInfo, null); int[] spanXY = getSpanForWidget(appWidgetInfo, null); // Try finding open space on Launcher screen @@ -966,18 +1013,24 @@ public final class Launcher extends Activity // if we are placing widgets on a "spring-loaded" screen int[] cellXY = mTmpAddItemCellCoordinates; int[] touchXY = mPendingAddInfo.dropPos; + int[] finalSpan = new int[2]; boolean foundCellSpan = false; if (mPendingAddInfo.cellX >= 0 && mPendingAddInfo.cellY >= 0) { cellXY[0] = mPendingAddInfo.cellX; cellXY[1] = mPendingAddInfo.cellY; + spanXY[0] = mPendingAddInfo.spanX; + spanXY[1] = mPendingAddInfo.spanY; foundCellSpan = true; } else if (touchXY != null) { // when dragging and dropping, just find the closest free spot int[] result = layout.findNearestVacantArea( - touchXY[0], touchXY[1], spanXY[0], spanXY[1], cellXY); + touchXY[0], touchXY[1], minSpanXY[0], minSpanXY[1], spanXY[0], + spanXY[1], cellXY, finalSpan); + spanXY[0] = finalSpan[0]; + spanXY[1] = finalSpan[1]; foundCellSpan = (result != null); } else { - foundCellSpan = layout.findCellForSpan(cellXY, spanXY[0], spanXY[1]); + foundCellSpan = layout.findCellForSpan(cellXY, minSpanXY[0], minSpanXY[1]); } if (!foundCellSpan) { @@ -990,7 +1043,7 @@ public final class Launcher extends Activity } }.start(); } - showOutOfSpaceMessage(); + showOutOfSpaceMessage(isHotseatLayout(layout)); return; } @@ -998,22 +1051,30 @@ public final class Launcher extends Activity LauncherAppWidgetInfo launcherInfo = new LauncherAppWidgetInfo(appWidgetId); launcherInfo.spanX = spanXY[0]; launcherInfo.spanY = spanXY[1]; + launcherInfo.minSpanX = mPendingAddInfo.minSpanX; + launcherInfo.minSpanY = mPendingAddInfo.minSpanY; LauncherModel.addItemToDatabase(this, launcherInfo, container, screen, cellXY[0], cellXY[1], false); if (!mRestoring) { - // Perform actual inflation because we're live - launcherInfo.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); + if (hostView == null) { + // Perform actual inflation because we're live + launcherInfo.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); + launcherInfo.hostView.setAppWidget(appWidgetId, appWidgetInfo); + } else { + // The AppWidgetHostView has already been inflated and instantiated + launcherInfo.hostView = hostView; + } - launcherInfo.hostView.setAppWidget(appWidgetId, appWidgetInfo); launcherInfo.hostView.setTag(launcherInfo); - + launcherInfo.hostView.setVisibility(View.VISIBLE); mWorkspace.addInScreen(launcherInfo.hostView, container, screen, cellXY[0], cellXY[1], launcherInfo.spanX, launcherInfo.spanY, isWorkspaceLocked()); addWidgetToAutoAdvanceIfNeeded(launcherInfo.hostView, appWidgetInfo); } + resetAddInfo(); } private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @@ -1068,6 +1129,35 @@ public final class Launcher extends Activity public void onWindowVisibilityChanged(int visibility) { mVisible = visibility == View.VISIBLE; updateRunning(); + // The following code used to be in onResume, but it turns out onResume is called when + // you're in All Apps and click home to go to the workspace. onWindowVisibilityChanged + // is a more appropriate event to handle + if (mVisible) { + mAppsCustomizeTabHost.onWindowVisible(); + if (!mWorkspaceLoading) { + final ViewTreeObserver observer = mWorkspace.getViewTreeObserver(); + // We want to let Launcher draw itself at least once before we force it to build + // layers on all the workspace pages, so that transitioning to Launcher from other + // apps is nice and speedy. Usually the first call to preDraw doesn't correspond to + // a true draw so we wait until the second preDraw call to be safe + observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + public boolean onPreDraw() { + // We delay the layer building a bit in order to give + // other message processing a time to run. In particular + // this avoids a delay in hiding the IME if it was + // currently shown, because doing that may involve + // some communication back with the app. + mWorkspace.postDelayed(mBuildLayersRunnable, 500); + observer.removeOnPreDrawListener(this); + return true; + } + }); + } + // When Launcher comes back to foreground, a different Activity might be responsible for + // the app market intent, so refresh the icon + updateAppMarketIcon(); + clearTypedText(); + } } private void sendAdvanceMessage(long delay) { @@ -1139,8 +1229,9 @@ public final class Launcher extends Activity launcherInfo.hostView = null; } - void showOutOfSpaceMessage() { - Toast.makeText(this, getString(R.string.out_of_space), Toast.LENGTH_SHORT).show(); + void showOutOfSpaceMessage(boolean isHotseatLayout) { + int strId = (isHotseatLayout ? R.string.hotseat_out_of_space : R.string.out_of_space); + Toast.makeText(this, getString(strId), Toast.LENGTH_SHORT).show(); } public LauncherAppWidgetHost getAppWidgetHost() { @@ -1408,6 +1499,7 @@ public final class Launcher extends Activity mPendingAddInfo.screen = -1; mPendingAddInfo.cellX = mPendingAddInfo.cellY = -1; mPendingAddInfo.spanX = mPendingAddInfo.spanY = -1; + mPendingAddInfo.minSpanX = mPendingAddInfo.minSpanY = -1; mPendingAddInfo.dropPos = null; } @@ -1419,9 +1511,9 @@ public final class Launcher extends Activity addAppWidgetImpl(appWidgetId, null); } - void addAppWidgetImpl(int appWidgetId, PendingAddWidgetInfo info) { - AppWidgetProviderInfo appWidget = mAppWidgetManager.getAppWidgetInfo(appWidgetId); - + void addAppWidgetImpl(final int appWidgetId, final PendingAddWidgetInfo info) { + final AppWidgetProviderInfo appWidget = info.info; + Runnable configurationActivity = null; if (appWidget.configure != null) { // Launch over to configure widget, if needed Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE); @@ -1429,9 +1521,8 @@ public final class Launcher extends Activity intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); if (info != null) { if (info.mimeType != null && !info.mimeType.isEmpty()) { - intent.putExtra( - InstallWidgetReceiver.EXTRA_APPWIDGET_CONFIGURATION_DATA_MIME_TYPE, - info.mimeType); + intent.putExtra(InstallWidgetReceiver. + EXTRA_APPWIDGET_CONFIGURATION_DATA_MIME_TYPE, info.mimeType); final String mimeType = info.mimeType; final ClipData clipData = (ClipData) info.configurationData; @@ -1442,8 +1533,8 @@ public final class Launcher extends Activity final CharSequence stringData = item.getText(); final Uri uriData = item.getUri(); final Intent intentData = item.getIntent(); - final String key = - InstallWidgetReceiver.EXTRA_APPWIDGET_CONFIGURATION_DATA; + final String key = InstallWidgetReceiver. + EXTRA_APPWIDGET_CONFIGURATION_DATA; if (uriData != null) { intent.putExtra(key, uriData); } else if (intentData != null) { @@ -1456,14 +1547,13 @@ public final class Launcher extends Activity } } } - startActivityForResultSafely(intent, REQUEST_CREATE_APPWIDGET); + mWidgetBeingConfigured = info; } else { // Otherwise just add it - completeAddAppWidget(appWidgetId, info.container, info.screen); - + completeAddAppWidget(appWidgetId, info.container, info.screen, info.boundWidget, appWidget); // Exit spring loaded mode if necessary after adding the widget - exitSpringLoadedDragModeDelayed(true, false); + exitSpringLoadedDragModeDelayed(true, false, null); } } @@ -1501,18 +1591,31 @@ public final class Launcher extends Activity * @param position The location on the screen where it was dropped, optional */ void addAppWidgetFromDrop(PendingAddWidgetInfo info, long container, int screen, - int[] cell, int[] loc) { + int[] cell, int[] span, int[] loc) { resetAddInfo(); mPendingAddInfo.container = info.container = container; mPendingAddInfo.screen = info.screen = screen; mPendingAddInfo.dropPos = loc; + mPendingAddInfo.minSpanX = info.minSpanX; + mPendingAddInfo.minSpanY = info.minSpanY; + if (cell != null) { mPendingAddInfo.cellX = cell[0]; mPendingAddInfo.cellY = cell[1]; } + if (span != null) { + mPendingAddInfo.spanX = span[0]; + mPendingAddInfo.spanY = span[1]; + } - int appWidgetId = getAppWidgetHost().allocateAppWidgetId(); - AppWidgetManager.getInstance(this).bindAppWidgetId(appWidgetId, info.componentName); + AppWidgetHostView hostView = info.boundWidget; + int appWidgetId; + if (hostView != null) { + appWidgetId = hostView.getAppWidgetId(); + } else { + appWidgetId = getAppWidgetHost().allocateAppWidgetId(); + AppWidgetManager.getInstance(this).bindAppWidgetId(appWidgetId, info.componentName); + } addAppWidgetImpl(appWidgetId, info); } @@ -1662,7 +1765,7 @@ public final class Launcher extends Activity return; } - if (mWorkspace.isSwitchingState()) { + if (!mWorkspace.isFinishedSwitchingState()) { return; } @@ -1743,6 +1846,8 @@ public final class Launcher extends Activity public void onClickAppMarketButton(View v) { if (mAppMarketIntent != null) { startActivitySafely(mAppMarketIntent, "app market"); + } else { + Log.e(TAG, "Invalid app market intent."); } } @@ -1931,6 +2036,7 @@ public final class Launcher extends Activity } public boolean onLongClick(View v) { + if (mState != State.WORKSPACE) { return false; } @@ -2098,7 +2204,7 @@ public final class Launcher extends Activity if (mWorkspaceLoading) { lockAllApps(); - mModel.startLoader(Launcher.this, false); + mModel.startLoader(false); } else { final FolderIcon folderIcon = (FolderIcon) mWorkspace.getViewForTag(mFolderInfo); @@ -2110,7 +2216,7 @@ public final class Launcher extends Activity } else { lockAllApps(); mWorkspaceLoading = true; - mModel.startLoader(Launcher.this, false); + mModel.startLoader(false); } } } @@ -2129,6 +2235,10 @@ public final class Launcher extends Activity return (mState == State.APPS_CUSTOMIZE); } + public boolean isAllAppsButtonRank(int rank) { + return mHotseat.isAllAppsButtonRank(rank); + } + // AllAppsView.Watcher public void zoomed(float zoom) { if (zoom == 1.0f) { @@ -2156,6 +2266,30 @@ public final class Launcher extends Activity } } + private void dispatchOnLauncherTransitionStart(View v, boolean animated, boolean toWorkspace) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionStart(this, animated, toWorkspace); + } + + // Update the workspace transition step as well + dispatchOnLauncherTransitionStep(v, 0f); + } + + private void dispatchOnLauncherTransitionStep(View v, float t) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionStep(this, t); + } + } + + private void dispatchOnLauncherTransitionEnd(View v, boolean animated, boolean toWorkspace) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionEnd(this, animated, toWorkspace); + } + + // Update the workspace transition step as well + dispatchOnLauncherTransitionStep(v, 1f); + } + /** * Things to test when changing the following seven functions. * - Home from workspace @@ -2201,7 +2335,7 @@ public final class Launcher extends Activity * Assumes that the view to show is anchored at either the very top or very bottom * of the screen. */ - private void showAppsCustomizeHelper(boolean animated, final boolean springLoaded) { + private void showAppsCustomizeHelper(final boolean animated, final boolean springLoaded) { if (mStateAnimation != null) { mStateAnimation.cancel(); mStateAnimation = null; @@ -2212,6 +2346,7 @@ public final class Launcher extends Activity final int duration = res.getInteger(R.integer.config_appsCustomizeZoomInTime); final int fadeDuration = res.getInteger(R.integer.config_appsCustomizeFadeInTime); final float scale = (float) res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor); + final View fromView = mWorkspace; final View toView = mAppsCustomizeTabHost; final int startDelay = res.getInteger(R.integer.config_workspaceAppsCustomizeAnimationStagger); @@ -2219,32 +2354,40 @@ public final class Launcher extends Activity setPivotsForZoom(toView, scale); // Shrink workspaces away if going to AppsCustomize from workspace - mWorkspace.changeState(Workspace.State.SMALL, animated); + Animator workspaceAnim = + mWorkspace.getChangeStateAnimation(Workspace.State.SMALL, animated); if (animated) { - final ValueAnimator scaleAnim = ValueAnimator.ofFloat(0f, 1f).setDuration(duration); - scaleAnim.setInterpolator(new Workspace.ZoomOutInterpolator()); - scaleAnim.addUpdateListener(new LauncherAnimatorUpdateListener() { - public void onAnimationUpdate(float a, float b) { - toView.setScaleX(a * scale + b * 1f); - toView.setScaleY(a * scale + b * 1f); - } - }); + toView.setScaleX(scale); + toView.setScaleY(scale); + final LauncherViewPropertyAnimator scaleAnim = new LauncherViewPropertyAnimator(toView); + scaleAnim. + scaleX(1f).scaleY(1f). + setDuration(duration). + setInterpolator(new Workspace.ZoomOutInterpolator()); toView.setVisibility(View.VISIBLE); toView.setAlpha(0f); - ValueAnimator alphaAnim = ValueAnimator.ofFloat(0f, 1f).setDuration(fadeDuration); + final ObjectAnimator alphaAnim = ObjectAnimator + .ofFloat(toView, "alpha", 0f, 1f) + .setDuration(fadeDuration); alphaAnim.setInterpolator(new DecelerateInterpolator(1.5f)); - alphaAnim.addUpdateListener(new LauncherAnimatorUpdateListener() { - public void onAnimationUpdate(float a, float b) { - // don't need to invalidate because we do so above - toView.setAlpha(a * 0f + b * 1f); + alphaAnim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float t = (Float) animation.getAnimatedValue(); + dispatchOnLauncherTransitionStep(fromView, t); + dispatchOnLauncherTransitionStep(toView, t); } }); - alphaAnim.setStartDelay(startDelay); - alphaAnim.start(); - scaleAnim.addListener(new AnimatorListenerAdapter() { + // toView should appear right at the end of the workspace shrink + // animation + mStateAnimation = new AnimatorSet(); + mStateAnimation.play(scaleAnim).after(startDelay); + mStateAnimation.play(alphaAnim).after(startDelay); + + mStateAnimation.addListener(new AnimatorListenerAdapter() { boolean animationCancelled = false; @Override @@ -2258,15 +2401,8 @@ public final class Launcher extends Activity } @Override public void onAnimationEnd(Animator animation) { - // If we don't set the final scale values here, if this animation is cancelled - // it will have the wrong scale value and subsequent cameraPan animations will - // not fix that - toView.setScaleX(1.0f); - toView.setScaleY(1.0f); - if (toView instanceof LauncherTransitionable) { - ((LauncherTransitionable) toView).onLauncherTransitionEnd(instance, - scaleAnim, false); - } + dispatchOnLauncherTransitionEnd(fromView, animated, false); + dispatchOnLauncherTransitionEnd(toView, animated, false); if (!springLoaded && !LauncherApplication.isScreenLarge()) { // Hide the workspace scrollbar @@ -2284,19 +2420,48 @@ public final class Launcher extends Activity } }); - // toView should appear right at the end of the workspace shrink animation - mStateAnimation = new AnimatorSet(); - mStateAnimation.play(scaleAnim).after(startDelay); + if (workspaceAnim != null) { + mStateAnimation.play(workspaceAnim); + } boolean delayAnim = false; - if (toView instanceof LauncherTransitionable) { - LauncherTransitionable lt = (LauncherTransitionable) toView; - delayAnim = lt.onLauncherTransitionStart(instance, mStateAnimation, false); + final ViewTreeObserver observer; + + dispatchOnLauncherTransitionStart(fromView, animated, false); + dispatchOnLauncherTransitionStart(toView, animated, false); + + // If any of the objects being animated haven't been measured/laid out + // yet, delay the animation until we get a layout pass + if ((((LauncherTransitionable) toView).getContent().getMeasuredWidth() == 0) || + (mWorkspace.getMeasuredWidth() == 0) || + (toView.getMeasuredWidth() == 0)) { + observer = mWorkspace.getViewTreeObserver(); + delayAnim = true; + } else { + observer = null; } - // if the anim is delayed, the LauncherTransitionable is responsible for starting it - if (!delayAnim) { - // TODO: q-- what if this anim is cancelled before being started? or started after - // being cancelled? + + if (delayAnim) { + final AnimatorSet stateAnimation = mStateAnimation; + final OnGlobalLayoutListener delayedStart = new OnGlobalLayoutListener() { + public void onGlobalLayout() { + mWorkspace.post(new Runnable() { + public void run() { + // Check that mStateAnimation hasn't changed while + // we waited for a layout pass + if (mStateAnimation == stateAnimation) { + // Need to update pivots for zoom if layout changed + setPivotsForZoom(toView, scale); + mStateAnimation.start(); + } + } + }); + observer.removeGlobalOnLayoutListener(this); + } + }; + observer.addOnGlobalLayoutListener(delayedStart); + } else { + setPivotsForZoom(toView, scale); mStateAnimation.start(); } } else { @@ -2306,16 +2471,16 @@ public final class Launcher extends Activity toView.setScaleY(1.0f); toView.setVisibility(View.VISIBLE); toView.bringToFront(); - if (toView instanceof LauncherTransitionable) { - ((LauncherTransitionable) toView).onLauncherTransitionStart(instance, null, false); - ((LauncherTransitionable) toView).onLauncherTransitionEnd(instance, null, false); - - if (!springLoaded && !LauncherApplication.isScreenLarge()) { - // Hide the workspace scrollbar - mWorkspace.hideScrollingIndicator(true); - hideDockDivider(); - } + + if (!springLoaded && !LauncherApplication.isScreenLarge()) { + // Hide the workspace scrollbar + mWorkspace.hideScrollingIndicator(true); + hideDockDivider(); } + dispatchOnLauncherTransitionStart(fromView, animated, false); + dispatchOnLauncherTransitionEnd(fromView, animated, false); + dispatchOnLauncherTransitionStart(toView, animated, false); + dispatchOnLauncherTransitionEnd(toView, animated, false); updateWallpaperVisibility(false); } } @@ -2325,18 +2490,32 @@ public final class Launcher extends Activity * This is the opposite of showAppsCustomizeHelper. * @param animated If true, the transition will be animated. */ - private void hideAppsCustomizeHelper(boolean animated, final boolean springLoaded) { + private void hideAppsCustomizeHelper(State toState, final boolean animated, + final boolean springLoaded, final Runnable onCompleteRunnable) { + if (mStateAnimation != null) { mStateAnimation.cancel(); mStateAnimation = null; } Resources res = getResources(); - final Launcher instance = this; final int duration = res.getInteger(R.integer.config_appsCustomizeZoomOutTime); + final int fadeOutDuration = + res.getInteger(R.integer.config_appsCustomizeFadeOutTime); final float scaleFactor = (float) res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor); final View fromView = mAppsCustomizeTabHost; + final View toView = mWorkspace; + Animator workspaceAnim = null; + + if (toState == State.WORKSPACE) { + int stagger = res.getInteger(R.integer.config_appsCustomizeWorkspaceAnimationStagger); + workspaceAnim = mWorkspace.getChangeStateAnimation( + Workspace.State.NORMAL, animated, stagger); + } else if (toState == State.APPS_CUSTOMIZE_SPRING_LOADED) { + workspaceAnim = mWorkspace.getChangeStateAnimation( + Workspace.State.SPRING_LOADED, animated); + } setPivotsForZoom(fromView, scaleFactor); updateWallpaperVisibility(true); @@ -2345,48 +2524,56 @@ public final class Launcher extends Activity final float oldScaleX = fromView.getScaleX(); final float oldScaleY = fromView.getScaleY(); - ValueAnimator scaleAnim = ValueAnimator.ofFloat(0f, 1f).setDuration(duration); - scaleAnim.setInterpolator(new Workspace.ZoomInInterpolator()); - scaleAnim.addUpdateListener(new LauncherAnimatorUpdateListener() { - public void onAnimationUpdate(float a, float b) { - fromView.setScaleX(a * oldScaleX + b * scaleFactor); - fromView.setScaleY(a * oldScaleY + b * scaleFactor); - } - }); - final ValueAnimator alphaAnim = ValueAnimator.ofFloat(0f, 1f); - alphaAnim.setDuration(res.getInteger(R.integer.config_appsCustomizeFadeOutTime)); + final LauncherViewPropertyAnimator scaleAnim = + new LauncherViewPropertyAnimator(fromView); + scaleAnim. + scaleX(scaleFactor).scaleY(scaleFactor). + setDuration(duration). + setInterpolator(new Workspace.ZoomInInterpolator()); + + final ObjectAnimator alphaAnim = ObjectAnimator + .ofFloat(fromView, "alpha", 1f, 0f) + .setDuration(fadeOutDuration); alphaAnim.setInterpolator(new AccelerateDecelerateInterpolator()); - alphaAnim.addUpdateListener(new LauncherAnimatorUpdateListener() { - public void onAnimationUpdate(float a, float b) { - fromView.setAlpha(a * 1f + b * 0f); + alphaAnim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float t = 1f - (Float) animation.getAnimatedValue(); + dispatchOnLauncherTransitionStep(fromView, t); + dispatchOnLauncherTransitionStep(toView, t); } }); - if (fromView instanceof LauncherTransitionable) { - ((LauncherTransitionable) fromView).onLauncherTransitionStart(instance, alphaAnim, - true); - } - alphaAnim.addListener(new AnimatorListenerAdapter() { + + mStateAnimation = new AnimatorSet(); + + dispatchOnLauncherTransitionStart(fromView, animated, true); + dispatchOnLauncherTransitionStart(toView, animated, true); + + mStateAnimation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { updateWallpaperVisibility(true); fromView.setVisibility(View.GONE); - if (fromView instanceof LauncherTransitionable) { - ((LauncherTransitionable) fromView).onLauncherTransitionEnd(instance, - alphaAnim, true); - } + dispatchOnLauncherTransitionEnd(fromView, animated, true); + dispatchOnLauncherTransitionEnd(toView, animated, true); mWorkspace.hideScrollingIndicator(false); + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } } }); - mStateAnimation = new AnimatorSet(); mStateAnimation.playTogether(scaleAnim, alphaAnim); + if (workspaceAnim != null) { + mStateAnimation.play(workspaceAnim); + } mStateAnimation.start(); } else { fromView.setVisibility(View.GONE); - if (fromView instanceof LauncherTransitionable) { - ((LauncherTransitionable) fromView).onLauncherTransitionStart(instance, null, true); - ((LauncherTransitionable) fromView).onLauncherTransitionEnd(instance, null, true); - } + dispatchOnLauncherTransitionStart(fromView, animated, true); + dispatchOnLauncherTransitionEnd(fromView, animated, true); + dispatchOnLauncherTransitionStart(toView, animated, true); + dispatchOnLauncherTransitionEnd(toView, animated, true); mWorkspace.hideScrollingIndicator(false); } } @@ -2394,19 +2581,19 @@ public final class Launcher extends Activity @Override public void onTrimMemory(int level) { super.onTrimMemory(level); - if (level == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { + if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { mAppsCustomizeTabHost.onTrimMemory(); } } void showWorkspace(boolean animated) { - Resources res = getResources(); - int stagger = res.getInteger(R.integer.config_appsCustomizeWorkspaceAnimationStagger); + showWorkspace(animated, null); + } - mWorkspace.changeState(Workspace.State.NORMAL, animated, stagger); + void showWorkspace(boolean animated, Runnable onCompleteRunnable) { if (mState != State.WORKSPACE) { mWorkspace.setVisibility(View.VISIBLE); - hideAppsCustomizeHelper(animated, false); + hideAppsCustomizeHelper(State.WORKSPACE, animated, false, onCompleteRunnable); // Show the search bar and hotseat mSearchDropTargetBar.showSearchBar(animated); @@ -2455,14 +2642,14 @@ public final class Launcher extends Activity void enterSpringLoadedDragMode() { if (mState == State.APPS_CUSTOMIZE) { - mWorkspace.changeState(Workspace.State.SPRING_LOADED); - hideAppsCustomizeHelper(true, true); + hideAppsCustomizeHelper(State.APPS_CUSTOMIZE_SPRING_LOADED, true, true, null); hideDockDivider(); mState = State.APPS_CUSTOMIZE_SPRING_LOADED; } } - void exitSpringLoadedDragModeDelayed(final boolean successfulDrop, boolean extendedDelay) { + void exitSpringLoadedDragModeDelayed(final boolean successfulDrop, boolean extendedDelay, + final Runnable onCompleteRunnable) { if (mState != State.APPS_CUSTOMIZE_SPRING_LOADED) return; mHandler.postDelayed(new Runnable() { @@ -2474,7 +2661,7 @@ public final class Launcher extends Activity // clean up our state transition functions mAppsCustomizeTabHost.setVisibility(View.GONE); mSearchDropTargetBar.showSearchBar(true); - showWorkspace(true); + showWorkspace(true, onCompleteRunnable); } else { exitSpringLoadedDragMode(); } @@ -2539,8 +2726,10 @@ public final class Launcher extends Activity void showHotseat(boolean animated) { if (!LauncherApplication.isScreenLarge()) { if (animated) { - int duration = mSearchDropTargetBar.getTransitionInDuration(); - mHotseat.animate().alpha(1f).setDuration(duration); + if (mHotseat.getAlpha() != 1f) { + int duration = mSearchDropTargetBar.getTransitionInDuration(); + mHotseat.animate().alpha(1f).setDuration(duration); + } } else { mHotseat.setAlpha(1f); } @@ -2553,8 +2742,10 @@ public final class Launcher extends Activity void hideHotseat(boolean animated) { if (!LauncherApplication.isScreenLarge()) { if (animated) { - int duration = mSearchDropTargetBar.getTransitionOutDuration(); - mHotseat.animate().alpha(0f).setDuration(duration); + if (mHotseat.getAlpha() != 0f) { + int duration = mSearchDropTargetBar.getTransitionOutDuration(); + mHotseat.animate().alpha(0f).setDuration(duration); + } } else { mHotseat.setAlpha(0f); } @@ -2567,7 +2758,7 @@ public final class Launcher extends Activity */ void addExternalItemToScreen(ItemInfo itemInfo, final CellLayout layout) { if (!mWorkspace.addExternalItemToScreen(itemInfo, layout)) { - showOutOfSpaceMessage(); + showOutOfSpaceMessage(isHotseatLayout(layout)); } } @@ -2905,7 +3096,6 @@ public final class Launcher extends Activity } } - /** * Refreshes the shortcuts shown on the workspace. * @@ -2914,6 +3104,8 @@ public final class Launcher extends Activity public void startBinding() { final Workspace workspace = mWorkspace; + mNewShortcutAnimatePage = -1; + mNewShortcutAnimateViews.clear(); mWorkspace.clearDropTargets(); int count = workspace.getChildCount(); for (int i = 0; i < count; i++) { @@ -2935,8 +3127,12 @@ public final class Launcher extends Activity public void bindItems(ArrayList<ItemInfo> shortcuts, int start, int end) { setLoadOnResume(); - final Workspace workspace = mWorkspace; - for (int i=start; i<end; i++) { + // Get the list of added shortcuts and intersect them with the set of shortcuts here + Set<String> newApps = new HashSet<String>(); + newApps = mSharedPrefs.getStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY, newApps); + + Workspace workspace = mWorkspace; + for (int i = start; i < end; i++) { final ItemInfo item = shortcuts.get(i); // Short circuit if we are loading dock items for a configuration which has no dock @@ -2948,9 +3144,23 @@ public final class Launcher extends Activity switch (item.itemType) { case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: - View shortcut = createShortcut((ShortcutInfo)item); + ShortcutInfo info = (ShortcutInfo) item; + String uri = info.intent.toUri(0).toString(); + View shortcut = createShortcut(info); workspace.addInScreen(shortcut, item.container, item.screen, item.cellX, item.cellY, 1, 1, false); + if (newApps.contains(uri)) { + newApps.remove(uri); + + // Prepare the view to be animated up + shortcut.setAlpha(0f); + shortcut.setScaleX(0f); + shortcut.setScaleY(0f); + mNewShortcutAnimatePage = item.screen; + if (!mNewShortcutAnimateViews.contains(shortcut)) { + mNewShortcutAnimateViews.add(shortcut); + } + } break; case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: FolderIcon newFolder = FolderIcon.fromXml(R.layout.folder_icon, this, @@ -2961,6 +3171,7 @@ public final class Launcher extends Activity break; } } + workspace.requestLayout(); } @@ -3031,8 +3242,6 @@ public final class Launcher extends Activity mSavedInstanceState = null; } - mWorkspaceLoading = false; - // If we received the result of any pending adds while the loader was running (e.g. the // widget configuration forced an orientation change), process them now. for (int i = 0; i < sPendingAddList.size(); i++) { @@ -3044,7 +3253,72 @@ public final class Launcher extends Activity // package changes in bindSearchablesChanged() updateAppMarketIcon(); - mWorkspace.post(mBuildLayersRunnable); + // Animate up any icons as necessary + if (mVisible || mWorkspaceLoading) { + Runnable newAppsRunnable = new Runnable() { + @Override + public void run() { + runNewAppsAnimation(); + } + }; + if (mNewShortcutAnimatePage > -1 && + mNewShortcutAnimatePage != mWorkspace.getCurrentPage()) { + mWorkspace.snapToPage(mNewShortcutAnimatePage, newAppsRunnable); + } else { + newAppsRunnable.run(); + } + } + + mWorkspaceLoading = false; + } + + /** + * Runs a new animation that scales up icons that were added while Launcher was in the + * background. + */ + private void runNewAppsAnimation() { + AnimatorSet anim = new AnimatorSet(); + Collection<Animator> bounceAnims = new ArrayList<Animator>(); + Collections.sort(mNewShortcutAnimateViews, new Comparator<View>() { + @Override + public int compare(View a, View b) { + CellLayout.LayoutParams alp = (CellLayout.LayoutParams) a.getLayoutParams(); + CellLayout.LayoutParams blp = (CellLayout.LayoutParams) b.getLayoutParams(); + int cellCountX = LauncherModel.getCellCountX(); + return (alp.cellY * cellCountX + alp.cellX) - (blp.cellY * cellCountX + blp.cellX); + } + }); + for (int i = 0; i < mNewShortcutAnimateViews.size(); ++i) { + View v = mNewShortcutAnimateViews.get(i); + ValueAnimator bounceAnim = ObjectAnimator.ofPropertyValuesHolder(v, + PropertyValuesHolder.ofFloat("alpha", 1f), + PropertyValuesHolder.ofFloat("scaleX", 1f), + PropertyValuesHolder.ofFloat("scaleY", 1f)); + bounceAnim.setDuration(InstallShortcutReceiver.NEW_SHORTCUT_BOUNCE_DURATION); + bounceAnim.setStartDelay(i * InstallShortcutReceiver.NEW_SHORTCUT_STAGGER_DELAY); + bounceAnim.setInterpolator(new SmoothPagedView.OvershootInterpolator()); + bounceAnims.add(bounceAnim); + } + anim.playTogether(bounceAnims); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mWorkspace.postDelayed(mBuildLayersRunnable, 500); + } + }); + anim.start(); + + // Clean up + mNewShortcutAnimatePage = -1; + mNewShortcutAnimateViews.clear(); + new Thread("clearNewAppsThread") { + public void run() { + mSharedPrefs.edit() + .putInt(InstallShortcutReceiver.NEW_APPS_PAGE_KEY, -1) + .putStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY, null) + .commit(); + } + }.start(); } @Override @@ -3187,7 +3461,6 @@ public final class Launcher extends Activity } /* Cling related */ - private static final String PREFS_KEY = "com.android.launcher2.prefs"; private boolean isClingsEnabled() { // disable clings when running in a test harness if(ActivityManager.isRunningInTestHarness()) return false; @@ -3223,11 +3496,14 @@ public final class Launcher extends Activity public void onAnimationEnd(Animator animation) { cling.setVisibility(View.GONE); cling.cleanup(); - SharedPreferences prefs = - getSharedPreferences("com.android.launcher2.prefs", Context.MODE_PRIVATE); - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(flag, true); - editor.commit(); + // We should update the shared preferences on a background thread + new Thread("dismissClingThread") { + public void run() { + SharedPreferences.Editor editor = mSharedPrefs.edit(); + editor.putBoolean(flag, true); + editor.commit(); + } + }.start(); }; }); anim.start(); @@ -3247,9 +3523,8 @@ public final class Launcher extends Activity } public void showFirstRunWorkspaceCling() { // Enable the clings only if they have not been dismissed before - SharedPreferences prefs = - getSharedPreferences(PREFS_KEY, Context.MODE_PRIVATE); - if (isClingsEnabled() && !prefs.getBoolean(Cling.WORKSPACE_CLING_DISMISSED_KEY, false)) { + if (isClingsEnabled() && + !mSharedPrefs.getBoolean(Cling.WORKSPACE_CLING_DISMISSED_KEY, false)) { initCling(R.id.workspace_cling, null, false, 0); } else { removeCling(R.id.workspace_cling); @@ -3257,9 +3532,8 @@ public final class Launcher extends Activity } public void showFirstRunAllAppsCling(int[] position) { // Enable the clings only if they have not been dismissed before - SharedPreferences prefs = - getSharedPreferences(PREFS_KEY, Context.MODE_PRIVATE); - if (isClingsEnabled() && !prefs.getBoolean(Cling.ALLAPPS_CLING_DISMISSED_KEY, false)) { + if (isClingsEnabled() && + !mSharedPrefs.getBoolean(Cling.ALLAPPS_CLING_DISMISSED_KEY, false)) { initCling(R.id.all_apps_cling, position, true, 0); } else { removeCling(R.id.all_apps_cling); @@ -3267,15 +3541,13 @@ public final class Launcher extends Activity } public Cling showFirstRunFoldersCling() { // Enable the clings only if they have not been dismissed before - SharedPreferences prefs = - getSharedPreferences(PREFS_KEY, Context.MODE_PRIVATE); - Cling cling = null; - if (isClingsEnabled() && !prefs.getBoolean(Cling.FOLDER_CLING_DISMISSED_KEY, false)) { - cling = initCling(R.id.folder_cling, null, true, 0); + if (isClingsEnabled() && + !mSharedPrefs.getBoolean(Cling.FOLDER_CLING_DISMISSED_KEY, false)) { + return initCling(R.id.folder_cling, null, true, 0); } else { removeCling(R.id.folder_cling); + return null; } - return cling; } public boolean isFolderClingVisible() { Cling cling = (Cling) findViewById(R.id.folder_cling); @@ -3328,7 +3600,8 @@ public final class Launcher extends Activity } interface LauncherTransitionable { - // return true if the callee will take care of start the animation by itself - boolean onLauncherTransitionStart(Launcher l, Animator animation, boolean toWorkspace); - void onLauncherTransitionEnd(Launcher l, Animator animation, boolean toWorkspace); + View getContent(); + void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace); + void onLauncherTransitionStep(Launcher l, float t); + void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace); } diff --git a/src/com/android/launcher2/LauncherAppWidgetHostView.java b/src/com/android/launcher2/LauncherAppWidgetHostView.java index 0c3bdcaf8..d73dd3008 100644 --- a/src/com/android/launcher2/LauncherAppWidgetHostView.java +++ b/src/com/android/launcher2/LauncherAppWidgetHostView.java @@ -30,12 +30,12 @@ import com.android.launcher.R; * {@inheritDoc} */ public class LauncherAppWidgetHostView extends AppWidgetHostView { - private boolean mHasPerformedLongPress; - private CheckForLongPress mPendingCheckForLongPress; + private CheckLongPressHelper mLongPressHelper; private LayoutInflater mInflater; public LauncherAppWidgetHostView(Context context) { super(context); + mLongPressHelper = new CheckLongPressHelper(this); mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } @@ -46,8 +46,8 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView { public boolean onInterceptTouchEvent(MotionEvent ev) { // Consume any touch events for ourselves after longpress is triggered - if (mHasPerformedLongPress) { - mHasPerformedLongPress = false; + if (mLongPressHelper.hasPerformedLongPress()) { + mLongPressHelper.cancelLongPress(); return true; } @@ -55,16 +55,13 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView { // users can always pick up this widget switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: { - postCheckForLongClick(); + mLongPressHelper.postCheckForLongPress(); break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: - mHasPerformedLongPress = false; - if (mPendingCheckForLongPress != null) { - removeCallbacks(mPendingCheckForLongPress); - } + mLongPressHelper.cancelLongPress(); break; } @@ -72,42 +69,11 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView { return false; } - class CheckForLongPress implements Runnable { - private int mOriginalWindowAttachCount; - - public void run() { - if ((mParent != null) && hasWindowFocus() - && mOriginalWindowAttachCount == getWindowAttachCount() - && !mHasPerformedLongPress) { - if (performLongClick()) { - mHasPerformedLongPress = true; - } - } - } - - public void rememberWindowAttachCount() { - mOriginalWindowAttachCount = getWindowAttachCount(); - } - } - - private void postCheckForLongClick() { - mHasPerformedLongPress = false; - - if (mPendingCheckForLongPress == null) { - mPendingCheckForLongPress = new CheckForLongPress(); - } - mPendingCheckForLongPress.rememberWindowAttachCount(); - postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout()); - } - @Override public void cancelLongPress() { super.cancelLongPress(); - mHasPerformedLongPress = false; - if (mPendingCheckForLongPress != null) { - removeCallbacks(mPendingCheckForLongPress); - } + mLongPressHelper.cancelLongPress(); } @Override diff --git a/src/com/android/launcher2/LauncherApplication.java b/src/com/android/launcher2/LauncherApplication.java index 9936ca682..ef1eb5fcd 100644 --- a/src/com/android/launcher2/LauncherApplication.java +++ b/src/com/android/launcher2/LauncherApplication.java @@ -25,6 +25,9 @@ import android.content.IntentFilter; import android.content.res.Configuration; import android.database.ContentObserver; import android.os.Handler; +import android.view.MotionEvent; + +import com.android.launcher.R; import java.lang.ref.WeakReference; @@ -33,6 +36,8 @@ public class LauncherApplication extends Application { public IconCache mIconCache; private static boolean sIsScreenLarge; private static float sScreenDensity; + private static int sLongPressTimeout = 300; + private static final String sSharedPreferencesKey = "com.android.launcher2.prefs"; WeakReference<LauncherProvider> mLauncherProvider; @Override @@ -40,10 +45,7 @@ public class LauncherApplication extends Application { super.onCreate(); // set sIsScreenXLarge and sScreenDensity *before* creating icon cache - final int screenSize = getResources().getConfiguration().screenLayout & - Configuration.SCREENLAYOUT_SIZE_MASK; - sIsScreenLarge = screenSize == Configuration.SCREENLAYOUT_SIZE_LARGE || - screenSize == Configuration.SCREENLAYOUT_SIZE_XLARGE; + sIsScreenLarge = getResources().getBoolean(R.bool.is_large_screen); sScreenDensity = getResources().getDisplayMetrics().density; mIconCache = new IconCache(this); @@ -93,7 +95,10 @@ public class LauncherApplication extends Application { private final ContentObserver mFavoritesObserver = new ContentObserver(new Handler()) { @Override public void onChange(boolean selfChange) { - mModel.startLoader(LauncherApplication.this, false); + // If the database has ever changed, then we really need to force a reload of the + // workspace on the next load + mModel.resetLoadedState(false, true); + mModel.startLoaderFromBackground(); } }; @@ -118,6 +123,10 @@ public class LauncherApplication extends Application { return mLauncherProvider.get(); } + public static String getSharedPreferencesKey() { + return sSharedPreferencesKey; + } + public static boolean isScreenLarge() { return sIsScreenLarge; } @@ -130,4 +139,8 @@ public class LauncherApplication extends Application { public static float getScreenDensity() { return sScreenDensity; } + + public static int getLongPressTimeout() { + return sLongPressTimeout; + } } diff --git a/src/com/android/launcher2/LauncherModel.java b/src/com/android/launcher2/LauncherModel.java index 7da55db6d..32c77c7c1 100644 --- a/src/com/android/launcher2/LauncherModel.java +++ b/src/com/android/launcher2/LauncherModel.java @@ -138,6 +138,7 @@ public class LauncherModel extends BroadcastReceiver { public void bindAppsRemoved(ArrayList<ApplicationInfo> apps, boolean permanent); public void bindPackagesUpdated(); public boolean isAllAppsVisible(); + public boolean isAllAppsButtonRank(int rank); public void bindSearchablesChanged(); } @@ -648,17 +649,22 @@ public class LauncherModel extends BroadcastReceiver { } private void forceReload() { + resetLoadedState(true, true); + + // Do this here because if the launcher activity is running it will be restarted. + // If it's not running startLoaderFromBackground will merely tell it that it needs + // to reload. + startLoaderFromBackground(); + } + + public void resetLoadedState(boolean resetAllAppsLoaded, boolean resetWorkspaceLoaded) { synchronized (mLock) { // Stop any existing loaders first, so they don't set mAllAppsLoaded or // mWorkspaceLoaded to true later stopLoaderLocked(); - mAllAppsLoaded = false; - mWorkspaceLoaded = false; + if (resetAllAppsLoaded) mAllAppsLoaded = false; + if (resetWorkspaceLoaded) mWorkspaceLoaded = false; } - // Do this here because if the launcher activity is running it will be restarted. - // If it's not running startLoaderFromBackground will merely tell it that it needs - // to reload. - startLoaderFromBackground(); } /** @@ -679,7 +685,7 @@ public class LauncherModel extends BroadcastReceiver { } } if (runLoader) { - startLoader(mApp, false); + startLoader(false); } } @@ -697,7 +703,7 @@ public class LauncherModel extends BroadcastReceiver { return isLaunching; } - public void startLoader(Context context, boolean isLaunching) { + public void startLoader(boolean isLaunching) { synchronized (mLock) { if (DEBUG_LOADERS) { Log.d(TAG, "startLoader isLaunching=" + isLaunching); @@ -708,7 +714,7 @@ public class LauncherModel extends BroadcastReceiver { // If there is already one running, tell it to stop. // also, don't downgrade isLaunching if we're already running isLaunching = isLaunching || stopLoaderLocked(); - mLoaderTask = new LoaderTask(context, isLaunching); + mLoaderTask = new LoaderTask(mApp, isLaunching); sWorkerThread.setPriority(Thread.NORM_PRIORITY); sWorker.post(mLoaderTask); } @@ -920,7 +926,7 @@ public class LauncherModel extends BroadcastReceiver { int containerIndex = item.screen; if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { // Return early if we detect that an item is under the hotseat button - if (Hotseat.isAllAppsButtonRank(item.screen)) { + if (mCallbacks == null || mCallbacks.get().isAllAppsButtonRank(item.screen)) { return false; } @@ -1348,6 +1354,7 @@ public class LauncherModel extends BroadcastReceiver { } // shallow copy + @SuppressWarnings("unchecked") final ArrayList<ApplicationInfo> list = (ArrayList<ApplicationInfo>) mAllAppsList.data.clone(); mHandler.post(new Runnable() { @@ -1651,7 +1658,26 @@ public class LauncherModel extends BroadcastReceiver { // but don't worry about that. All we're doing with usingFallbackIcon is // to avoid saving lots of copies of that in the database, and most apps // have icons anyway. - final ResolveInfo resolveInfo = manager.resolveActivity(intent, 0); + + // Attempt to use queryIntentActivities to get the ResolveInfo (with IntentFilter info) and + // if that fails, or is ambiguious, fallback to the standard way of getting the resolve info + // via resolveActivity(). + ResolveInfo resolveInfo = null; + ComponentName oldComponent = intent.getComponent(); + Intent newIntent = new Intent(intent.getAction(), null); + newIntent.addCategory(Intent.CATEGORY_LAUNCHER); + newIntent.setPackage(oldComponent.getPackageName()); + List<ResolveInfo> infos = manager.queryIntentActivities(newIntent, 0); + for (ResolveInfo i : infos) { + ComponentName cn = new ComponentName(i.activityInfo.packageName, + i.activityInfo.name); + if (cn.equals(oldComponent)) { + resolveInfo = i; + } + } + if (resolveInfo == null) { + resolveInfo = manager.resolveActivity(intent, 0); + } if (resolveInfo != null) { icon = mIconCache.getIcon(componentName, resolveInfo, labelCache); } diff --git a/src/com/android/launcher2/LauncherProvider.java b/src/com/android/launcher2/LauncherProvider.java index a01cc479b..de9a9b2de 100644 --- a/src/com/android/launcher2/LauncherProvider.java +++ b/src/com/android/launcher2/LauncherProvider.java @@ -706,6 +706,8 @@ public class LauncherProvider extends ContentProvider { ContentValues values = new ContentValues(); PackageManager packageManager = mContext.getPackageManager(); + int allAppsButtonRank = + mContext.getResources().getInteger(R.integer.hotseat_all_apps_index); int i = 0; try { XmlResourceParser parser = mContext.getResources().getXml(workspaceResourceId); @@ -739,8 +741,8 @@ public class LauncherProvider extends ContentProvider { // If we are adding to the hotseat, the screen is used as the position in the // hotseat. This screen can't be at position 0 because AllApps is in the // zeroth position. - if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT && - Hotseat.isAllAppsButtonRank(Integer.valueOf(screen))) { + if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT + && Integer.valueOf(screen) == allAppsButtonRank) { throw new RuntimeException("Invalid screen position for hotseat item"); } diff --git a/src/com/android/launcher2/LauncherViewPropertyAnimator.java b/src/com/android/launcher2/LauncherViewPropertyAnimator.java new file mode 100644 index 000000000..88b4cb4b8 --- /dev/null +++ b/src/com/android/launcher2/LauncherViewPropertyAnimator.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2012 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.launcher2; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.TimeInterpolator; +import android.view.ViewPropertyAnimator; +import android.view.View; + +import java.util.ArrayList; +import java.util.EnumSet; + +public class LauncherViewPropertyAnimator extends Animator implements AnimatorListener { + enum Properties { + TRANSLATION_X, + TRANSLATION_Y, + SCALE_X, + SCALE_Y, + ROTATION_Y, + ALPHA, + START_DELAY, + DURATION, + INTERPOLATOR + } + EnumSet<Properties> mPropertiesToSet = EnumSet.noneOf(Properties.class); + ViewPropertyAnimator mViewPropertyAnimator; + View mTarget; + + float mTranslationX; + float mTranslationY; + float mScaleX; + float mScaleY; + float mRotationY; + float mAlpha; + long mStartDelay; + long mDuration; + TimeInterpolator mInterpolator; + ArrayList<Animator.AnimatorListener> mListeners; + boolean mRunning = false; + + public LauncherViewPropertyAnimator(View target) { + mTarget = target; + mListeners = new ArrayList<Animator.AnimatorListener>(); + } + + @Override + public void addListener(Animator.AnimatorListener listener) { + mListeners.add(listener); + } + + @Override + public void cancel() { + if (mViewPropertyAnimator != null) { + mViewPropertyAnimator.cancel(); + } + } + + @Override + public Animator clone() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void end() { + throw new RuntimeException("Not implemented"); + } + + @Override + public long getDuration() { + return mDuration; + } + + @Override + public ArrayList<Animator.AnimatorListener> getListeners() { + return mListeners; + } + + @Override + public long getStartDelay() { + return mStartDelay; + } + + @Override + public void onAnimationCancel(Animator animation) { + for (int i = 0; i < mListeners.size(); i++) { + Animator.AnimatorListener listener = mListeners.get(i); + listener.onAnimationCancel(this); + } + mRunning = false; + } + + @Override + public void onAnimationEnd(Animator animation) { + for (int i = 0; i < mListeners.size(); i++) { + Animator.AnimatorListener listener = mListeners.get(i); + listener.onAnimationEnd(this); + } + mRunning = false; + } + + @Override + public void onAnimationRepeat(Animator animation) { + for (int i = 0; i < mListeners.size(); i++) { + Animator.AnimatorListener listener = mListeners.get(i); + listener.onAnimationRepeat(this); + } + } + + @Override + public void onAnimationStart(Animator animation) { + for (int i = 0; i < mListeners.size(); i++) { + Animator.AnimatorListener listener = mListeners.get(i); + listener.onAnimationStart(this); + } + mRunning = true; + } + + @Override + public boolean isRunning() { + return mRunning; + } + + @Override + public boolean isStarted() { + return mViewPropertyAnimator != null; + } + + @Override + public void removeAllListeners() { + mListeners.clear(); + } + + @Override + public void removeListener(Animator.AnimatorListener listener) { + mListeners.remove(listener); + } + + @Override + public Animator setDuration(long duration) { + mPropertiesToSet.add(Properties.DURATION); + mDuration = duration; + return this; + } + + @Override + public void setInterpolator(TimeInterpolator value) { + mPropertiesToSet.add(Properties.INTERPOLATOR); + mInterpolator = value; + } + + @Override + public void setStartDelay(long startDelay) { + mPropertiesToSet.add(Properties.START_DELAY); + mStartDelay = startDelay; + } + + @Override + public void setTarget(Object target) { + throw new RuntimeException("Not implemented"); + } + + @Override + public void setupEndValues() { + + } + + @Override + public void setupStartValues() { + } + + @Override + public void start() { + mViewPropertyAnimator = mTarget.animate(); + if (mPropertiesToSet.contains(Properties.TRANSLATION_X)) { + mViewPropertyAnimator.translationX(mTranslationX); + } + if (mPropertiesToSet.contains(Properties.TRANSLATION_Y)) { + mViewPropertyAnimator.translationY(mTranslationY); + } + if (mPropertiesToSet.contains(Properties.SCALE_X)) { + mViewPropertyAnimator.scaleX(mScaleX); + } + if (mPropertiesToSet.contains(Properties.ROTATION_Y)) { + mViewPropertyAnimator.rotationY(mRotationY); + } + if (mPropertiesToSet.contains(Properties.SCALE_Y)) { + mViewPropertyAnimator.scaleY(mScaleY); + } + if (mPropertiesToSet.contains(Properties.ALPHA)) { + mViewPropertyAnimator.alpha(mAlpha); + } + if (mPropertiesToSet.contains(Properties.START_DELAY)) { + mViewPropertyAnimator.setStartDelay(mStartDelay); + } + if (mPropertiesToSet.contains(Properties.DURATION)) { + mViewPropertyAnimator.setDuration(mDuration); + } + if (mPropertiesToSet.contains(Properties.INTERPOLATOR)) { + mViewPropertyAnimator.setInterpolator(mInterpolator); + } + mViewPropertyAnimator.setListener(this); + mViewPropertyAnimator.start(); + } + + public LauncherViewPropertyAnimator translationX(float value) { + mPropertiesToSet.add(Properties.TRANSLATION_X); + mTranslationX = value; + return this; + } + + public LauncherViewPropertyAnimator translationY(float value) { + mPropertiesToSet.add(Properties.TRANSLATION_Y); + mTranslationY = value; + return this; + } + + public LauncherViewPropertyAnimator scaleX(float value) { + mPropertiesToSet.add(Properties.SCALE_X); + mScaleX = value; + return this; + } + + public LauncherViewPropertyAnimator scaleY(float value) { + mPropertiesToSet.add(Properties.SCALE_Y); + mScaleY = value; + return this; + } + + public LauncherViewPropertyAnimator rotationY(float value) { + mPropertiesToSet.add(Properties.ROTATION_Y); + mRotationY = value; + return this; + } + + public LauncherViewPropertyAnimator alpha(float value) { + mPropertiesToSet.add(Properties.ALPHA); + mAlpha = value; + return this; + } +} diff --git a/src/com/android/launcher2/PagedView.java b/src/com/android/launcher2/PagedView.java index 3f5652e1d..8a2a5a0ac 100644 --- a/src/com/android/launcher2/PagedView.java +++ b/src/com/android/launcher2/PagedView.java @@ -62,19 +62,26 @@ public abstract class PagedView extends ViewGroup { // the min drag distance for a fling to register, to prevent random page shifts private static final int MIN_LENGTH_FOR_FLING = 25; - private static final int PAGE_SNAP_ANIMATION_DURATION = 550; + protected static final int PAGE_SNAP_ANIMATION_DURATION = 550; + protected static final int SLOW_PAGE_SNAP_ANIMATION_DURATION = 950; protected static final float NANOTIME_DIV = 1000000000.0f; private static final float OVERSCROLL_ACCELERATE_FACTOR = 2; private static final float OVERSCROLL_DAMP_FACTOR = 0.14f; - private static final int MINIMUM_SNAP_VELOCITY = 2200; - private static final int MIN_FLING_VELOCITY = 250; + private static final float RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f; // The page is moved more than halfway, automatically move to the next page on touch up. private static final float SIGNIFICANT_MOVE_THRESHOLD = 0.4f; - // the velocity at which a fling gesture will cause us to snap to the next page - protected int mSnapVelocity = 500; + // The following constants need to be scaled based on density. The scaled versions will be + // assigned to the corresponding member variables below. + private static final int FLING_THRESHOLD_VELOCITY = 500; + private static final int MIN_SNAP_VELOCITY = 1500; + private static final int MIN_FLING_VELOCITY = 250; + + protected int mFlingThresholdVelocity; + protected int mMinFlingVelocity; + protected int mMinSnapVelocity; protected float mDensity; protected float mSmoothingTime; @@ -145,15 +152,6 @@ public abstract class PagedView extends ViewGroup { protected ArrayList<Boolean> mDirtyPageContent; - // choice modes - protected static final int CHOICE_MODE_NONE = 0; - protected static final int CHOICE_MODE_SINGLE = 1; - // Multiple selection mode is not supported by all Launcher actions atm - protected static final int CHOICE_MODE_MULTIPLE = 2; - - protected int mChoiceMode; - private ActionMode mActionMode; - // If true, syncPages and syncPageItems will be called to refresh pages protected boolean mContentIsRefreshable = true; @@ -175,10 +173,12 @@ public abstract class PagedView extends ViewGroup { // Scrolling indicator private ValueAnimator mScrollIndicatorAnimator; - private ImageView mScrollIndicator; + private View mScrollIndicator; private int mScrollIndicatorPaddingLeft; private int mScrollIndicatorPaddingRight; private boolean mHasScrollIndicator = true; + private boolean mShouldShowScrollIndicator = false; + private boolean mShouldShowScrollIndicatorImmediately = false; protected static final int sScrollIndicatorFadeInDuration = 150; protected static final int sScrollIndicatorFadeOutDuration = 650; protected static final int sScrollIndicatorFlashDuration = 650; @@ -200,7 +200,6 @@ public abstract class PagedView extends ViewGroup { public PagedView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mChoiceMode = CHOICE_MODE_NONE; TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PagedView, defStyle, 0); @@ -242,6 +241,10 @@ public abstract class PagedView extends ViewGroup { mPagingTouchSlop = configuration.getScaledPagingTouchSlop(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); mDensity = getResources().getDisplayMetrics().density; + + mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity); + mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * mDensity); + mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * mDensity); } public void setPageSwitchListener(PageSwitchListener pageSwitchListener) { @@ -292,6 +295,7 @@ public abstract class PagedView extends ViewGroup { int newX = getChildOffset(mCurrentPage) - getRelativeChildOffset(mCurrentPage); scrollTo(newX, 0); mScroller.setFinalX(newX); + mScroller.forceFinished(true); } /** @@ -340,12 +344,10 @@ public abstract class PagedView extends ViewGroup { // a method that subclasses can override to add behavior protected void onPageBeginMoving() { - showScrollingIndicator(false); } // a method that subclasses can override to add behavior protected void onPageEndMoving() { - hideScrollingIndicator(false); } /** @@ -579,7 +581,14 @@ public abstract class PagedView extends ViewGroup { // Calculate the variable page spacing if necessary if (mPageSpacing < 0) { - setPageSpacing(((right - left) - getChildAt(0).getMeasuredWidth()) / 2); + // The gap between pages in the PagedView should be equal to the gap from the page + // to the edge of the screen (so it is not visible in the current screen). To + // account for unequal padding on each side of the paged view, we take the maximum + // of the left/right gap and use that as the gap between each page. + int offset = getRelativeChildOffset(0); + int spacing = Math.max(offset, (right - left) - offset - + getChildAt(0).getMeasuredWidth()); + setPageSpacing(spacing); } } @@ -602,9 +611,7 @@ public abstract class PagedView extends ViewGroup { if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) { setHorizontalScrollBarEnabled(false); - int newX = getChildOffset(mCurrentPage) - getRelativeChildOffset(mCurrentPage); - scrollTo(newX, 0); - mScroller.setFinalX(newX); + updateCurrentPageScroll(); setHorizontalScrollBarEnabled(true); mFirstLayout = false; } @@ -624,8 +631,7 @@ public abstract class PagedView extends ViewGroup { if (child != null) { float scrollProgress = getScrollProgress(screenCenter, child, i); float alpha = 1 - Math.abs(scrollProgress); - child.setFastAlpha(alpha); - child.fastInvalidate(); + child.setAlpha(alpha); } } invalidate(); @@ -715,20 +721,23 @@ public abstract class PagedView extends ViewGroup { protected void getVisiblePages(int[] range) { final int pageCount = getChildCount(); + if (pageCount > 0) { - final int pageWidth = getScaledMeasuredWidth(getPageAt(0)); final int screenWidth = getMeasuredWidth(); - int x = getScaledRelativeChildOffset(0) + pageWidth; int leftScreen = 0; int rightScreen = 0; - while (x <= mScrollX && leftScreen < pageCount - 1) { + View currPage = getPageAt(leftScreen); + while (leftScreen < pageCount - 1 && + currPage.getX() + currPage.getWidth() - currPage.getPaddingRight() < mScrollX) { leftScreen++; - x += getScaledMeasuredWidth(getPageAt(leftScreen)) + mPageSpacing; + currPage = getPageAt(leftScreen); } rightScreen = leftScreen; - while (x < mScrollX + screenWidth && rightScreen < pageCount - 1) { + currPage = getPageAt(rightScreen + 1); + while (rightScreen < pageCount - 1 && + currPage.getX() - currPage.getPaddingLeft() < mScrollX + screenWidth) { rightScreen++; - x += getScaledMeasuredWidth(getPageAt(rightScreen)) + mPageSpacing; + currPage = getPageAt(rightScreen + 1); } range[0] = leftScreen; range[1] = rightScreen; @@ -764,8 +773,22 @@ public abstract class PagedView extends ViewGroup { canvas.clipRect(mScrollX, mScrollY, mScrollX + mRight - mLeft, mScrollY + mBottom - mTop); - for (int i = rightScreen; i >= leftScreen; i--) { - drawChild(canvas, getPageAt(i), drawingTime); + // On certain graphics drivers, if you draw to a off-screen buffer that's not + // used, it can lead to poor performance. We were running into this when + // setChildrenLayersEnabled was called on a CellLayout; that triggered a re-draw + // of that CellLayout's hardware layer, even if that CellLayout wasn't visible. + // As a fix, below we set pages that aren't going to be rendered are to be + // View.INVISIBLE, preventing re-drawing of their hardware layer + for (int i = getChildCount() - 1; i >= 0; i--) { + final View v = getPageAt(i); + + if (leftScreen <= i && i <= rightScreen && + v.getAlpha() > ViewConfiguration.ALPHA_THRESHOLD) { + v.setVisibility(VISIBLE); + drawChild(canvas, v, drawingTime); + } else { + v.setVisibility(INVISIBLE); + } } canvas.restore(); } @@ -986,19 +1009,6 @@ public abstract class PagedView extends ViewGroup { return mTouchState != TOUCH_STATE_REST; } - protected void animateClickFeedback(View v, final Runnable r) { - // animate the view slightly to show click feedback running some logic after it is "pressed" - ObjectAnimator anim = (ObjectAnimator) AnimatorInflater. - loadAnimator(mContext, R.anim.paged_view_click_feedback); - anim.setTarget(v); - anim.addListener(new AnimatorListenerAdapter() { - public void onAnimationRepeat(Animator animation) { - r.run(); - } - }); - anim.start(); - } - protected void determineScrollingStart(MotionEvent ev) { determineScrollingStart(ev, 1.0f); } @@ -1208,12 +1218,11 @@ public abstract class PagedView extends ViewGroup { final int pageWidth = getScaledMeasuredWidth(getPageAt(mCurrentPage)); boolean isSignificantMove = Math.abs(deltaX) > pageWidth * SIGNIFICANT_MOVE_THRESHOLD; - final int snapVelocity = mSnapVelocity; mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder - x); boolean isFling = mTotalMotionX > MIN_LENGTH_FOR_FLING && - Math.abs(velocityX) > snapVelocity; + Math.abs(velocityX) > mFlingThresholdVelocity; // In the case that the page is moved far to one direction and then is flung // in the opposite direction, we use a threshold to determine whether we should @@ -1434,7 +1443,7 @@ public abstract class PagedView extends ViewGroup { int delta = newX - mUnboundedScrollX; int duration = 0; - if (Math.abs(velocity) < MIN_FLING_VELOCITY) { + if (Math.abs(velocity) < mMinFlingVelocity) { // If the velocity is low enough, then treat this more as an automatic page advance // as opposed to an apparent physical response to flinging snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); @@ -1450,7 +1459,7 @@ public abstract class PagedView extends ViewGroup { distanceInfluenceForSnapDuration(distanceRatio); velocity = Math.abs(velocity); - velocity = Math.max(MINIMUM_SNAP_VELOCITY, velocity); + velocity = Math.max(mMinSnapVelocity, velocity); // we want the page's snap velocity to approximately match the velocity at which the // user flings, so we scale the duration by a value near to the derivative of the scroll @@ -1591,22 +1600,26 @@ public abstract class PagedView extends ViewGroup { int upperPageBound = getAssociatedUpperPageBound(page); if (DEBUG) Log.d(TAG, "loadAssociatedPages: " + lowerPageBound + "/" + upperPageBound); + // First, clear any pages that should no longer be loaded + for (int i = 0; i < count; ++i) { + Page layout = (Page) getPageAt(i); + if ((i < lowerPageBound) || (i > upperPageBound)) { + if (layout.getPageChildCount() > 0) { + layout.removeAllViewsOnPage(); + } + mDirtyPageContent.set(i, true); + } + } + // Next, load any new pages for (int i = 0; i < count; ++i) { if ((i != page) && immediateAndOnly) { continue; } - Page layout = (Page) getPageAt(i); - final int childCount = layout.getPageChildCount(); if (lowerPageBound <= i && i <= upperPageBound) { if (mDirtyPageContent.get(i)) { syncPageItems(i, (i == page) && immediateAndOnly); mDirtyPageContent.set(i, false); } - } else { - if (childCount > 0) { - layout.removeAllViewsOnPage(); - } - mDirtyPageContent.set(i, true); } } } @@ -1621,72 +1634,6 @@ public abstract class PagedView extends ViewGroup { return Math.min(page + 1, count - 1); } - protected void startChoiceMode(int mode, ActionMode.Callback callback) { - if (isChoiceMode(CHOICE_MODE_NONE)) { - mChoiceMode = mode; - mActionMode = startActionMode(callback); - } - } - - public void endChoiceMode() { - if (!isChoiceMode(CHOICE_MODE_NONE)) { - mChoiceMode = CHOICE_MODE_NONE; - resetCheckedGrandchildren(); - if (mActionMode != null) mActionMode.finish(); - mActionMode = null; - } - } - - protected boolean isChoiceMode(int mode) { - return mChoiceMode == mode; - } - - protected ArrayList<Checkable> getCheckedGrandchildren() { - ArrayList<Checkable> checked = new ArrayList<Checkable>(); - final int childCount = getChildCount(); - for (int i = 0; i < childCount; ++i) { - Page layout = (Page) getPageAt(i); - final int grandChildCount = layout.getPageChildCount(); - for (int j = 0; j < grandChildCount; ++j) { - final View v = layout.getChildOnPageAt(j); - if (v instanceof Checkable && ((Checkable) v).isChecked()) { - checked.add((Checkable) v); - } - } - } - return checked; - } - - /** - * If in CHOICE_MODE_SINGLE and an item is checked, returns that item. - * Otherwise, returns null. - */ - protected Checkable getSingleCheckedGrandchild() { - if (mChoiceMode != CHOICE_MODE_MULTIPLE) { - final int childCount = getChildCount(); - for (int i = 0; i < childCount; ++i) { - Page layout = (Page) getPageAt(i); - final int grandChildCount = layout.getPageChildCount(); - for (int j = 0; j < grandChildCount; ++j) { - final View v = layout.getChildOnPageAt(j); - if (v instanceof Checkable && ((Checkable) v).isChecked()) { - return (Checkable) v; - } - } - } - } - return null; - } - - protected void resetCheckedGrandchildren() { - // loop through children, and set all of their children to _not_ be checked - final ArrayList<Checkable> checked = getCheckedGrandchildren(); - for (int i = 0; i < checked.size(); ++i) { - final Checkable c = checked.get(i); - c.setChecked(false); - } - } - /** * This method is called ONLY to synchronize the number of pages that the paged view has. * To actually fill the pages with information, implement syncPageItems() below. It is @@ -1743,12 +1690,12 @@ public abstract class PagedView extends ViewGroup { } } - protected ImageView getScrollingIndicator() { + protected View getScrollingIndicator() { // We use mHasScrollIndicator to prevent future lookups if there is no sibling indicator // found if (mHasScrollIndicator && mScrollIndicator == null) { ViewGroup parent = (ViewGroup) getParent(); - mScrollIndicator = (ImageView) (parent.findViewById(R.id.paged_view_indicator)); + mScrollIndicator = (View) (parent.findViewById(R.id.paged_view_indicator)); mHasScrollIndicator = mScrollIndicator != null; if (mHasScrollIndicator) { mScrollIndicator.setVisibility(View.VISIBLE); @@ -1774,9 +1721,12 @@ public abstract class PagedView extends ViewGroup { } protected void showScrollingIndicator(boolean immediately) { + mShouldShowScrollIndicator = true; + mShouldShowScrollIndicatorImmediately = true; if (getChildCount() <= 1) return; if (!isScrollingIndicatorEnabled()) return; + mShouldShowScrollIndicator = false; getScrollingIndicator(); if (mScrollIndicator != null) { // Fade the indicator in @@ -1848,6 +1798,9 @@ public abstract class PagedView extends ViewGroup { if (mScrollIndicator != null) { updateScrollingIndicatorPosition(); } + if (mShouldShowScrollIndicator) { + showScrollingIndicator(mShouldShowScrollIndicatorImmediately); + } } private void updateScrollingIndicatorPosition() { @@ -1874,7 +1827,6 @@ public abstract class PagedView extends ViewGroup { indicatorPos += indicatorCenterOffset; } mScrollIndicator.setTranslationX(indicatorPos); - mScrollIndicator.invalidate(); } public void showScrollIndicatorTrack() { diff --git a/src/com/android/launcher2/PagedViewGridLayout.java b/src/com/android/launcher2/PagedViewGridLayout.java index b1b621598..90bfe88ec 100644 --- a/src/com/android/launcher2/PagedViewGridLayout.java +++ b/src/com/android/launcher2/PagedViewGridLayout.java @@ -109,6 +109,7 @@ public class PagedViewGridLayout extends GridLayout implements Page { @Override public void removeAllViewsOnPage() { removeAllViews(); + mOnLayoutListener = null; destroyHardwareLayer(); } diff --git a/src/com/android/launcher2/PagedViewIcon.java b/src/com/android/launcher2/PagedViewIcon.java index af10f189c..4149ab618 100644 --- a/src/com/android/launcher2/PagedViewIcon.java +++ b/src/com/android/launcher2/PagedViewIcon.java @@ -16,43 +16,28 @@ package com.android.launcher2; -import android.animation.ObjectAnimator; import android.content.Context; -import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; import android.util.AttributeSet; -import android.widget.Checkable; import android.widget.TextView; -import com.android.launcher.R; - - /** * An icon on a PagedView, specifically for items in the launcher's paged view (with compound * drawables on the top). */ -public class PagedViewIcon extends TextView implements Checkable { - private static final String TAG = "PagedViewIcon"; - - // holographic outline - private final Paint mPaint = new Paint(); - private Bitmap mCheckedOutline; - private Bitmap mHolographicOutline; - private Bitmap mIcon; +public class PagedViewIcon extends TextView { + /** A simple callback interface to allow a PagedViewIcon to notify when it has been pressed */ + public static interface PressedCallback { + void iconPressed(PagedViewIcon icon); + } - private int mAlpha = 255; - private int mHolographicAlpha; + private static final String TAG = "PagedViewIcon"; + private static final float PRESS_ALPHA = 0.4f; - private boolean mIsChecked; - private ObjectAnimator mCheckedAlphaAnimator; - private float mCheckedAlpha = 1.0f; - private int mCheckedFadeInDuration; - private int mCheckedFadeOutDuration; + private PagedViewIcon.PressedCallback mPressedCallback; + private boolean mLockDrawableState = false; - HolographicPagedViewIcon mHolographicOutlineView; - private HolographicOutlineHelper mHolographicOutlineHelper; + private Bitmap mIcon; public PagedViewIcon(Context context) { this(context, null); @@ -64,131 +49,43 @@ public class PagedViewIcon extends TextView implements Checkable { public PagedViewIcon(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - - // Set up fade in/out constants - final Resources r = context.getResources(); - final int alpha = r.getInteger(R.integer.config_dragAppsCustomizeIconFadeAlpha); - if (alpha > 0) { - mCheckedAlpha = r.getInteger(R.integer.config_dragAppsCustomizeIconFadeAlpha) / 256.0f; - mCheckedFadeInDuration = - r.getInteger(R.integer.config_dragAppsCustomizeIconFadeInDuration); - mCheckedFadeOutDuration = - r.getInteger(R.integer.config_dragAppsCustomizeIconFadeOutDuration); - } - - mHolographicOutlineView = new HolographicPagedViewIcon(context, this); - } - - protected HolographicPagedViewIcon getHolographicOutlineView() { - return mHolographicOutlineView; - } - - protected Bitmap getHolographicOutline() { - return mHolographicOutline; } public void applyFromApplicationInfo(ApplicationInfo info, boolean scaleUp, - HolographicOutlineHelper holoOutlineHelper) { - mHolographicOutlineHelper = holoOutlineHelper; + PagedViewIcon.PressedCallback cb) { mIcon = info.iconBitmap; + mPressedCallback = cb; setCompoundDrawablesWithIntrinsicBounds(null, new FastBitmapDrawable(mIcon), null, null); setText(info.title); setTag(info); } - public void setHolographicOutline(Bitmap holoOutline) { - mHolographicOutline = holoOutline; - getHolographicOutlineView().invalidate(); - } - - @Override - public void setAlpha(float alpha) { - final float viewAlpha = HolographicOutlineHelper.viewAlphaInterpolator(alpha); - final float holographicAlpha = HolographicOutlineHelper.highlightAlphaInterpolator(alpha); - int newViewAlpha = (int) (viewAlpha * 255); - int newHolographicAlpha = (int) (holographicAlpha * 255); - if ((mAlpha != newViewAlpha) || (mHolographicAlpha != newHolographicAlpha)) { - mAlpha = newViewAlpha; - mHolographicAlpha = newHolographicAlpha; - super.setAlpha(viewAlpha); - } - } - - public void invalidateCheckedImage() { - if (mCheckedOutline != null) { - mCheckedOutline.recycle(); - mCheckedOutline = null; - } - } - - @Override - protected void onDraw(Canvas canvas) { - if (mAlpha > 0) { - super.onDraw(canvas); - } - - Bitmap overlay = null; - - // draw any blended overlays - if (mCheckedOutline != null) { - mPaint.setAlpha(255); - overlay = mCheckedOutline; - } - - if (overlay != null) { - final int offset = getScrollX(); - final int compoundPaddingLeft = getCompoundPaddingLeft(); - final int compoundPaddingRight = getCompoundPaddingRight(); - int hspace = getWidth() - compoundPaddingRight - compoundPaddingLeft; - canvas.drawBitmap(overlay, - offset + compoundPaddingLeft + (hspace - overlay.getWidth()) / 2, - mPaddingTop, - mPaint); - } + public void lockDrawableState() { + mLockDrawableState = true; } - @Override - public boolean isChecked() { - return mIsChecked; + public void resetDrawableState() { + mLockDrawableState = false; + post(new Runnable() { + @Override + public void run() { + refreshDrawableState(); + } + }); } - void setChecked(boolean checked, boolean animate) { - if (mIsChecked != checked) { - mIsChecked = checked; - - float alpha; - int duration; - if (mIsChecked) { - alpha = mCheckedAlpha; - duration = mCheckedFadeInDuration; - } else { - alpha = 1.0f; - duration = mCheckedFadeOutDuration; - } + protected void drawableStateChanged() { + super.drawableStateChanged(); - // Initialize the animator - if (mCheckedAlphaAnimator != null) { - mCheckedAlphaAnimator.cancel(); + // We keep in the pressed state until resetDrawableState() is called to reset the press + // feedback + if (isPressed()) { + setAlpha(PRESS_ALPHA); + if (mPressedCallback != null) { + mPressedCallback.iconPressed(this); } - if (animate) { - mCheckedAlphaAnimator = ObjectAnimator.ofFloat(this, "alpha", getAlpha(), alpha); - mCheckedAlphaAnimator.setDuration(duration); - mCheckedAlphaAnimator.start(); - } else { - setAlpha(alpha); - } - - invalidate(); + } else if (!mLockDrawableState) { + setAlpha(1f); } } - - @Override - public void setChecked(boolean checked) { - setChecked(checked, true); - } - - @Override - public void toggle() { - setChecked(!mIsChecked); - } } diff --git a/src/com/android/launcher2/PagedViewWidget.java b/src/com/android/launcher2/PagedViewWidget.java index 8fcfa8fed..670717e49 100644 --- a/src/com/android/launcher2/PagedViewWidget.java +++ b/src/com/android/launcher2/PagedViewWidget.java @@ -16,20 +16,14 @@ package com.android.launcher2; -import android.animation.ObjectAnimator; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.RectF; import android.util.AttributeSet; -import android.view.KeyEvent; import android.view.MotionEvent; -import android.widget.Checkable; +import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; @@ -39,27 +33,15 @@ import com.android.launcher.R; /** * The linear layout used strictly for the widget/wallpaper tab of the customization tray */ -public class PagedViewWidget extends LinearLayout implements Checkable { +public class PagedViewWidget extends LinearLayout { static final String TAG = "PagedViewWidgetLayout"; private static boolean sDeletePreviewsWhenDetachedFromWindow = true; - private final Paint mPaint = new Paint(); - private Bitmap mHolographicOutline; - private HolographicOutlineHelper mHolographicOutlineHelper; - private ImageView mPreviewImageView; - private final RectF mTmpScaleRect = new RectF(); - private String mDimensionsFormatString; - - private int mAlpha = 255; - private int mHolographicAlpha; - - private boolean mIsChecked; - private ObjectAnimator mCheckedAlphaAnimator; - private float mCheckedAlpha = 1.0f; - private int mCheckedFadeInDuration; - private int mCheckedFadeOutDuration; + CheckForShortPress mPendingCheckForShortPress = null; + ShortPressListener mShortPressListener = null; + boolean mShortPressTriggered = false; public PagedViewWidget(Context context) { this(context, null); @@ -72,16 +54,7 @@ public class PagedViewWidget extends LinearLayout implements Checkable { public PagedViewWidget(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - // Set up fade in/out constants final Resources r = context.getResources(); - final int alpha = r.getInteger(R.integer.config_dragAppsCustomizeIconFadeAlpha); - if (alpha > 0) { - mCheckedAlpha = r.getInteger(R.integer.config_dragAppsCustomizeIconFadeAlpha) / 256.0f; - mCheckedFadeInDuration = - r.getInteger(R.integer.config_dragAppsCustomizeIconFadeInDuration); - mCheckedFadeOutDuration = - r.getInteger(R.integer.config_dragAppsCustomizeIconFadeOutDuration); - } mDimensionsFormatString = r.getString(R.string.widget_dims_format); setWillNotDraw(false); @@ -109,14 +82,12 @@ public class PagedViewWidget extends LinearLayout implements Checkable { } public void applyFromAppWidgetProviderInfo(AppWidgetProviderInfo info, - int maxWidth, int[] cellSpan, HolographicOutlineHelper holoOutlineHelper) { - mHolographicOutlineHelper = holoOutlineHelper; + int maxWidth, int[] cellSpan) { final ImageView image = (ImageView) findViewById(R.id.widget_preview); if (maxWidth > -1) { image.setMaxWidth(maxWidth); } image.setContentDescription(info.label); - mPreviewImageView = image; final TextView name = (TextView) findViewById(R.id.widget_name); name.setText(info.label); final TextView dims = (TextView) findViewById(R.id.widget_dims); @@ -125,13 +96,10 @@ public class PagedViewWidget extends LinearLayout implements Checkable { } } - public void applyFromResolveInfo(PackageManager pm, ResolveInfo info, - HolographicOutlineHelper holoOutlineHelper) { - mHolographicOutlineHelper = holoOutlineHelper; + public void applyFromResolveInfo(PackageManager pm, ResolveInfo info) { CharSequence label = info.loadLabel(pm); final ImageView image = (ImageView) findViewById(R.id.widget_preview); image.setContentDescription(label); - mPreviewImageView = image; final TextView name = (TextView) findViewById(R.id.widget_name); name.setText(label); final TextView dims = (TextView) findViewById(R.id.widget_dims); @@ -159,111 +127,74 @@ public class PagedViewWidget extends LinearLayout implements Checkable { } } - public void setHolographicOutline(Bitmap holoOutline) { - mHolographicOutline = holoOutline; - invalidate(); + void setShortPressListener(ShortPressListener listener) { + mShortPressListener = listener; } - @Override - public boolean onTouchEvent(MotionEvent event) { - // We eat up the touch events here, since the PagedView (which uses the same swiping - // touch code as Workspace previously) uses onInterceptTouchEvent() to determine when - // the user is scrolling between pages. This means that if the pages themselves don't - // handle touch events, it gets forwarded up to PagedView itself, and it's own - // onTouchEvent() handling will prevent further intercept touch events from being called - // (it's the same view in that case). This is not ideal, but to prevent more changes, - // we just always mark the touch event as handled. - return super.onTouchEvent(event) || true; + interface ShortPressListener { + void onShortPress(View v); + void cleanUpShortPress(View v); } - @Override - protected void onDraw(Canvas canvas) { - if (mAlpha > 0) { - super.onDraw(canvas); - } - - // draw any blended overlays - if (mHolographicOutline != null && mHolographicAlpha > 0) { - // Calculate how much to scale the holographic preview - mTmpScaleRect.set(0,0,1,1); - mPreviewImageView.getImageMatrix().mapRect(mTmpScaleRect); - - mPaint.setAlpha(mHolographicAlpha); - canvas.save(); - canvas.scale(mTmpScaleRect.right, mTmpScaleRect.bottom); - canvas.drawBitmap(mHolographicOutline, mPreviewImageView.getLeft(), - mPreviewImageView.getTop(), mPaint); - canvas.restore(); + class CheckForShortPress implements Runnable { + public void run() { + if (mShortPressListener != null) { + mShortPressListener.onShortPress(PagedViewWidget.this); + } + mShortPressTriggered = true; } } - @Override - protected boolean onSetAlpha(int alpha) { - return true; - } - - private void setChildrenAlpha(float alpha) { - final int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - getChildAt(i).setAlpha(alpha); + private void checkForShortPress() { + if (mPendingCheckForShortPress == null) { + mPendingCheckForShortPress = new CheckForShortPress(); } + postDelayed(mPendingCheckForShortPress, 120); } - @Override - public void setAlpha(float alpha) { - final float viewAlpha = mHolographicOutlineHelper.viewAlphaInterpolator(alpha); - final float holographicAlpha = mHolographicOutlineHelper.highlightAlphaInterpolator(alpha); - int newViewAlpha = (int) (viewAlpha * 255); - int newHolographicAlpha = (int) (holographicAlpha * 255); - if ((mAlpha != newViewAlpha) || (mHolographicAlpha != newHolographicAlpha)) { - mAlpha = newViewAlpha; - mHolographicAlpha = newHolographicAlpha; - setChildrenAlpha(viewAlpha); - super.setAlpha(viewAlpha); + + /** + * Remove the longpress detection timer. + */ + private void removeShortPressCallback() { + if (mPendingCheckForShortPress != null) { + removeCallbacks(mPendingCheckForShortPress); } } - void setChecked(boolean checked, boolean animate) { - if (mIsChecked != checked) { - mIsChecked = checked; - - float alpha; - int duration; - if (mIsChecked) { - alpha = mCheckedAlpha; - duration = mCheckedFadeInDuration; - } else { - alpha = 1.0f; - duration = mCheckedFadeOutDuration; - } - - // Initialize the animator - if (mCheckedAlphaAnimator != null) { - mCheckedAlphaAnimator.cancel(); - } - if (animate) { - mCheckedAlphaAnimator = ObjectAnimator.ofFloat(this, "alpha", getAlpha(), alpha); - mCheckedAlphaAnimator.setDuration(duration); - mCheckedAlphaAnimator.start(); - } else { - setAlpha(alpha); + private void cleanUpShortPress() { + removeShortPressCallback(); + if (mShortPressTriggered) { + if (mShortPressListener != null) { + mShortPressListener.cleanUpShortPress(PagedViewWidget.this); } - - invalidate(); + mShortPressTriggered = false; } } @Override - public void setChecked(boolean checked) { - setChecked(checked, true); - } - - @Override - public boolean isChecked() { - return mIsChecked; - } - - @Override - public void toggle() { - setChecked(!mIsChecked); + public boolean onTouchEvent(MotionEvent event) { + super.onTouchEvent(event); + + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + cleanUpShortPress(); + break; + case MotionEvent.ACTION_DOWN: + checkForShortPress(); + break; + case MotionEvent.ACTION_CANCEL: + cleanUpShortPress(); + break; + case MotionEvent.ACTION_MOVE: + break; + } + // We eat up the touch events here, since the PagedView (which uses the same swiping + // touch code as Workspace previously) uses onInterceptTouchEvent() to determine when + // the user is scrolling between pages. This means that if the pages themselves don't + // handle touch events, it gets forwarded up to PagedView itself, and it's own + // onTouchEvent() handling will prevent further intercept touch events from being called + // (it's the same view in that case). This is not ideal, but to prevent more changes, + // we just always mark the touch event as handled. + return true; } } diff --git a/src/com/android/launcher2/PagedViewWithDraggableItems.java b/src/com/android/launcher2/PagedViewWithDraggableItems.java index 287a06565..a0479707e 100644 --- a/src/com/android/launcher2/PagedViewWithDraggableItems.java +++ b/src/com/android/launcher2/PagedViewWithDraggableItems.java @@ -165,4 +165,12 @@ public abstract class PagedViewWithDraggableItems extends PagedView cancelDragging(); super.onDetachedFromWindow(); } + + /** Show the scrolling indicators when we move the page */ + protected void onPageBeginMoving() { + showScrollingIndicator(false); + } + protected void onPageEndMoving() { + hideScrollingIndicator(false); + } } diff --git a/src/com/android/launcher2/PendingAddItemInfo.java b/src/com/android/launcher2/PendingAddItemInfo.java index 9c52ecfa0..d36e21763 100644 --- a/src/com/android/launcher2/PendingAddItemInfo.java +++ b/src/com/android/launcher2/PendingAddItemInfo.java @@ -16,6 +16,7 @@ package com.android.launcher2; +import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.os.Parcelable; @@ -33,8 +34,12 @@ class PendingAddItemInfo extends ItemInfo { class PendingAddWidgetInfo extends PendingAddItemInfo { int minWidth; int minHeight; + int minResizeWidth; + int minResizeHeight; int previewImage; int icon; + AppWidgetProviderInfo info; + AppWidgetHostView boundWidget; // Any configuration data that we want to pass to a configuration activity when // starting up a widget @@ -43,9 +48,12 @@ class PendingAddWidgetInfo extends PendingAddItemInfo { public PendingAddWidgetInfo(AppWidgetProviderInfo i, String dataMimeType, Parcelable data) { itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; + this.info = i; componentName = i.provider; minWidth = i.minWidth; minHeight = i.minHeight; + minResizeWidth = i.minResizeWidth; + minResizeHeight = i.minResizeHeight; previewImage = i.previewImage; icon = i.icon; if (dataMimeType != null && data != null) { @@ -53,4 +61,20 @@ class PendingAddWidgetInfo extends PendingAddItemInfo { configurationData = data; } } -}
\ No newline at end of file + + // Copy constructor + public PendingAddWidgetInfo(PendingAddWidgetInfo copy) { + minWidth = copy.minWidth; + minHeight = copy.minHeight; + minResizeWidth = copy.minResizeWidth; + minResizeHeight = copy.minResizeHeight; + previewImage = copy.previewImage; + icon = copy.icon; + info = copy.info; + boundWidget = copy.boundWidget; + mimeType = copy.mimeType; + configurationData = copy.configurationData; + componentName = copy.componentName; + itemType = copy.itemType; + } +} diff --git a/src/com/android/launcher2/RocketLauncher.java b/src/com/android/launcher2/RocketLauncher.java index 125537464..505ac4c9e 100644 --- a/src/com/android/launcher2/RocketLauncher.java +++ b/src/com/android/launcher2/RocketLauncher.java @@ -23,7 +23,6 @@ package com.android.launcher2; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeAnimator; -import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -31,6 +30,7 @@ import android.graphics.Bitmap; import android.graphics.Point; import android.graphics.Rect; import android.os.Handler; +import android.support.v13.dreams.BasicDream; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.MotionEvent; @@ -44,7 +44,7 @@ import com.android.launcher.R; import java.util.HashMap; import java.util.Random; -public class RocketLauncher extends Activity { +public class RocketLauncher extends BasicDream { public static final boolean ROCKET_LAUNCHER = true; public static class Board extends FrameLayout diff --git a/src/com/android/launcher2/SearchDropTargetBar.java b/src/com/android/launcher2/SearchDropTargetBar.java index 3a7f24b09..76d707618 100644 --- a/src/com/android/launcher2/SearchDropTargetBar.java +++ b/src/com/android/launcher2/SearchDropTargetBar.java @@ -69,6 +69,7 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D dragController.addDragListener(mDeleteDropTarget); dragController.addDropTarget(mInfoDropTarget); dragController.addDropTarget(mDeleteDropTarget); + dragController.setFlingToDeleteDropTarget(mDeleteDropTarget); mInfoDropTarget.setLauncher(launcher); mDeleteDropTarget.setLauncher(launcher); } @@ -129,18 +130,37 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D @Override public void onAnimationStart(Animator animation) { mQSBSearchBar.setVisibility(View.VISIBLE); + mQSBSearchBar.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + + @Override + public void onAnimationEnd(Animator animation) { + mQSBSearchBar.setLayerType(View.LAYER_TYPE_NONE, null); } }); mQSBSearchBarFadeOutAnim = ObjectAnimator.ofFloat(mQSBSearchBar, "alpha", 0f); mQSBSearchBarFadeOutAnim.setDuration(sTransitionOutDuration); mQSBSearchBarFadeOutAnim.addListener(new AnimatorListenerAdapter() { @Override + public void onAnimationStart(Animator animation) { + mQSBSearchBar.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + + @Override public void onAnimationEnd(Animator animation) { mQSBSearchBar.setVisibility(View.INVISIBLE); + mQSBSearchBar.setLayerType(View.LAYER_TYPE_NONE, null); } }); } + public void finishAnimations() { + mDropTargetBarFadeInAnim.end(); + mDropTargetBarFadeOutAnim.end(); + mQSBSearchBarFadeInAnim.end(); + mQSBSearchBarFadeOutAnim.end(); + } + private void cancelAnimations() { mDropTargetBarFadeInAnim.cancel(); mDropTargetBarFadeOutAnim.cancel(); diff --git a/src/com/android/launcher2/CellLayoutChildren.java b/src/com/android/launcher2/ShortcutAndWidgetContainer.java index 35f5af104..7e5e94072 100644 --- a/src/com/android/launcher2/CellLayoutChildren.java +++ b/src/com/android/launcher2/ShortcutAndWidgetContainer.java @@ -18,11 +18,15 @@ package com.android.launcher2; import android.app.WallpaperManager; import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; import android.graphics.Rect; import android.view.View; import android.view.ViewGroup; -public class CellLayoutChildren extends ViewGroup { +import com.android.launcher2.CellLayout.LayoutParams; + +public class ShortcutAndWidgetContainer extends ViewGroup { static final String TAG = "CellLayoutChildren"; // These are temporary variables to prevent having to allocate a new object just to @@ -37,7 +41,7 @@ public class CellLayoutChildren extends ViewGroup { private int mWidthGap; private int mHeightGap; - public CellLayoutChildren(Context context) { + public ShortcutAndWidgetContainer(Context context) { super(context); mWallpaperManager = WallpaperManager.getInstance(context); } @@ -68,6 +72,22 @@ public class CellLayoutChildren extends ViewGroup { } @Override + protected void dispatchDraw(Canvas canvas) { + // Debug drawing for hit space + if (false) { + Paint p = new Paint(); + p.setColor(0x6600FF00); + for (int i = getChildCount() - 1; i >= 0; i--) { + final View child = getChildAt(i); + final CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); + + canvas.drawRect(lp.x, lp.y, lp.x + lp.width, lp.y + lp.height, p); + } + } + super.dispatchDraw(canvas); + } + + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); for (int i = 0; i < count; i++) { diff --git a/src/com/android/launcher2/ShortcutInfo.java b/src/com/android/launcher2/ShortcutInfo.java index c0f80aeec..ff3028beb 100644 --- a/src/com/android/launcher2/ShortcutInfo.java +++ b/src/com/android/launcher2/ShortcutInfo.java @@ -93,12 +93,16 @@ class ShortcutInfo extends ItemInfo { public Bitmap getIcon(IconCache iconCache) { if (mIcon == null) { - mIcon = iconCache.getIcon(this.intent); - this.usingFallbackIcon = iconCache.isDefaultIcon(mIcon); + updateIcon(iconCache); } return mIcon; } + public void updateIcon(IconCache iconCache) { + mIcon = iconCache.getIcon(intent); + usingFallbackIcon = iconCache.isDefaultIcon(mIcon); + } + /** * Creates the application intent based on a component name and various launch flags. * Sets {@link #itemType} to {@link LauncherSettings.BaseLauncherColumns#ITEM_TYPE_APPLICATION}. diff --git a/src/com/android/launcher2/SmoothPagedView.java b/src/com/android/launcher2/SmoothPagedView.java index fe763f508..e6414d960 100644 --- a/src/com/android/launcher2/SmoothPagedView.java +++ b/src/com/android/launcher2/SmoothPagedView.java @@ -35,11 +35,11 @@ public abstract class SmoothPagedView extends PagedView { private Interpolator mScrollInterpolator; - private static class WorkspaceOvershootInterpolator implements Interpolator { + public static class OvershootInterpolator implements Interpolator { private static final float DEFAULT_TENSION = 1.3f; private float mTension; - public WorkspaceOvershootInterpolator() { + public OvershootInterpolator() { mTension = DEFAULT_TENSION; } @@ -101,7 +101,7 @@ public abstract class SmoothPagedView extends PagedView { if (mScrollMode == DEFAULT_MODE) { mBaseLineFlingVelocity = 2500.0f; mFlingVelocityInfluence = 0.4f; - mScrollInterpolator = new WorkspaceOvershootInterpolator(); + mScrollInterpolator = new OvershootInterpolator(); mScroller = new Scroller(getContext(), mScrollInterpolator); } } @@ -139,9 +139,9 @@ public abstract class SmoothPagedView extends PagedView { } if (settle) { - ((WorkspaceOvershootInterpolator) mScrollInterpolator).setDistance(screenDelta); + ((OvershootInterpolator) mScrollInterpolator).setDistance(screenDelta); } else { - ((WorkspaceOvershootInterpolator) mScrollInterpolator).disableSettle(); + ((OvershootInterpolator) mScrollInterpolator).disableSettle(); } velocity = Math.abs(velocity); diff --git a/src/com/android/launcher2/SpringLoadedDragController.java b/src/com/android/launcher2/SpringLoadedDragController.java index 358362c85..d96aab794 100644 --- a/src/com/android/launcher2/SpringLoadedDragController.java +++ b/src/com/android/launcher2/SpringLoadedDragController.java @@ -18,7 +18,7 @@ package com.android.launcher2; public class SpringLoadedDragController implements OnAlarmListener { // how long the user must hover over a mini-screen before it unshrinks - final long ENTER_SPRING_LOAD_HOVER_TIME = 550; + final long ENTER_SPRING_LOAD_HOVER_TIME = 500; final long ENTER_SPRING_LOAD_CANCEL_HOVER_TIME = 950; final long EXIT_SPRING_LOAD_HOVER_TIME = 200; diff --git a/src/com/android/launcher2/Utilities.java b/src/com/android/launcher2/Utilities.java index d7562a947..0387011a4 100644 --- a/src/com/android/launcher2/Utilities.java +++ b/src/com/android/launcher2/Utilities.java @@ -113,7 +113,6 @@ final class Utilities { } int sourceWidth = icon.getIntrinsicWidth(); int sourceHeight = icon.getIntrinsicHeight(); - if (sourceWidth > 0 && sourceHeight > 0) { // There are intrinsic sizes. if (width < sourceWidth || height < sourceHeight) { diff --git a/src/com/android/launcher2/WallpaperChooserDialogFragment.java b/src/com/android/launcher2/WallpaperChooserDialogFragment.java index 7c4ac2e33..eec699e35 100644 --- a/src/com/android/launcher2/WallpaperChooserDialogFragment.java +++ b/src/com/android/launcher2/WallpaperChooserDialogFragment.java @@ -82,10 +82,7 @@ public class WallpaperChooserDialogFragment extends DialogFragment implements outState.putBoolean(EMBEDDED_KEY, mEmbedded); } - @Override - public void onDestroy() { - super.onDestroy(); - + private void cancelLoader() { if (mLoader != null && mLoader.getStatus() != WallpaperLoader.Status.FINISHED) { mLoader.cancel(true); mLoader = null; @@ -93,6 +90,20 @@ public class WallpaperChooserDialogFragment extends DialogFragment implements } @Override + public void onDetach() { + super.onDetach(); + + cancelLoader(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + cancelLoader(); + } + + @Override public void onDismiss(DialogInterface dialog) { super.onDismiss(dialog); /* On orientation changes, the dialog is effectively "dismissed" so this is called @@ -112,26 +123,6 @@ public class WallpaperChooserDialogFragment extends DialogFragment implements public Dialog onCreateDialog(Bundle savedInstanceState) { findWallpapers(); - // TODO: The following code is not exercised right now and may be removed - // if the dialog version is not needed. - /* - final View v = getActivity().getLayoutInflater().inflate( - R.layout.wallpaper_chooser, null, false); - - GridView gridView = (GridView) v.findViewById(R.id.gallery); - gridView.setOnItemClickListener(this); - gridView.setAdapter(new ImageAdapter(getActivity())); - - final int viewInset = - getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_inset); - - FrameLayout wallPaperList = (FrameLayout) v.findViewById(R.id.wallpaper_list); - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setNegativeButton(R.string.wallpaper_cancel, null); - builder.setTitle(R.string.wallpaper_dialog_title); - builder.setView(wallPaperList, - viewInset, viewInset, viewInset, viewInset); return builder.create(); - */ return null; } @@ -366,4 +357,4 @@ public class WallpaperChooserDialogFragment extends DialogFragment implements // Ignore } } -}
\ No newline at end of file +} diff --git a/src/com/android/launcher2/Workspace.java b/src/com/android/launcher2/Workspace.java index 41e8a461b..ba8b56f20 100644 --- a/src/com/android/launcher2/Workspace.java +++ b/src/com/android/launcher2/Workspace.java @@ -17,19 +17,15 @@ package com.android.launcher2; import android.animation.Animator; -import android.animation.Animator.AnimatorListener; -import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.app.AlertDialog; import android.app.WallpaperManager; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; -import android.content.ClipData; import android.content.ClipDescription; import android.content.ComponentName; import android.content.Context; @@ -42,9 +38,8 @@ import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Point; -import android.graphics.PorterDuff; +import android.graphics.PointF; import android.graphics.Rect; -import android.graphics.RectF; import android.graphics.Region.Op; import android.graphics.drawable.Drawable; import android.os.IBinder; @@ -57,16 +52,17 @@ import android.view.Display; import android.view.DragEvent; import android.view.MotionEvent; import android.view.View; -import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.widget.ImageView; import android.widget.TextView; -import android.widget.Toast; import com.android.launcher.R; +import com.android.launcher2.DropTarget.DragObject; import com.android.launcher2.FolderIcon.FolderRingAnimator; import com.android.launcher2.InstallWidgetReceiver.WidgetMimeTypeHandlerData; +import com.android.launcher2.LauncherSettings.Favorites; import java.util.ArrayList; import java.util.HashSet; @@ -79,7 +75,7 @@ import java.util.List; */ public class Workspace extends SmoothPagedView implements DropTarget, DragSource, DragScroller, View.OnTouchListener, - DragController.DragListener { + DragController.DragListener, LauncherTransitionable { @SuppressWarnings({"UnusedDeclaration"}) private static final String TAG = "Launcher.Workspace"; @@ -94,6 +90,9 @@ public class Workspace extends SmoothPagedView private static final int BACKGROUND_FADE_OUT_DURATION = 350; private static final int ADJACENT_SCREEN_DROP_DURATION = 300; + private static final int FLING_THRESHOLD_VELOCITY = 500; + + private float mMaxDistanceForFolderCreation = 50.0f; // These animators are used to fade the children's outlines private ObjectAnimator mChildrenOutlineFadeInAnimation; @@ -131,6 +130,7 @@ public class Workspace extends SmoothPagedView * The CellLayout that is currently being dragged over */ private CellLayout mDragTargetLayout = null; + private boolean mDragHasEnteredWorkspace = false; private Launcher mLauncher; private IconCache mIconCache; @@ -158,11 +158,6 @@ public class Workspace extends SmoothPagedView enum State { NORMAL, SPRING_LOADED, SMALL }; private State mState = State.NORMAL; private boolean mIsSwitchingState = false; - private boolean mSwitchStateAfterFirstLayout = false; - private State mStateAfterFirstLayout; - - private AnimatorSet mAnimator; - private AnimatorListener mChangeStateAnimationListener; boolean mAnimatingViewIntoPlace = false; boolean mIsDragOccuring = false; @@ -175,11 +170,8 @@ public class Workspace extends SmoothPagedView private Bitmap mDragOutline = null; private final Rect mTempRect = new Rect(); private final int[] mTempXY = new int[2]; - private int mDragViewMultiplyColor; private float mOverscrollFade = 0; - - // Paint used to draw external drop outline - private final Paint mExternalDragOutlinePaint = new Paint(); + public static final int DRAG_BITMAP_PADDING = 0; // Camera and Matrix used to determine the final position of a neighboring CellLayout private final Matrix mMatrix = new Matrix(); @@ -192,16 +184,21 @@ public class Workspace extends SmoothPagedView WallpaperOffsetInterpolator mWallpaperOffset; boolean mUpdateWallpaperOffsetImmediately = false; private Runnable mDelayedResizeRunnable; + private Runnable mDelayedSnapToPageRunnable; private int mDisplayWidth; private int mDisplayHeight; + private boolean mIsStaticWallpaper; private int mWallpaperTravelWidth; // Variables relating to the creation of user folders by hovering shortcuts over shortcuts - private static final int FOLDER_CREATION_TIMEOUT = 250; + private static final int FOLDER_CREATION_TIMEOUT = 0; + private static final int REORDER_TIMEOUT = 250; private final Alarm mFolderCreationAlarm = new Alarm(); + private final Alarm mReorderAlarm = new Alarm(); private FolderRingAnimator mDragFolderRingAnimator = null; private View mLastDragOverView = null; private boolean mCreateUserFolderOnDrop = false; + private boolean mWillAddToExistingFolder = false; // Variables relating to touch disambiguation (scrolling workspace vs. scrolling a widget) private float mXDown; @@ -210,6 +207,26 @@ public class Workspace extends SmoothPagedView final static float MAX_SWIPE_ANGLE = (float) Math.PI / 3; final static float TOUCH_SLOP_DAMPING_FACTOR = 4; + // Relating to the animation of items being dropped externally + public static final int ANIMATE_INTO_POSITION_AND_DISAPPEAR = 0; + public static final int ANIMATE_INTO_POSITION_AND_REMAIN = 1; + public static final int ANIMATE_INTO_POSITION_AND_RESIZE = 2; + public static final int COMPLETE_TWO_STAGE_WIDGET_DROP_ANIMATION = 3; + public static final int CANCEL_TWO_STAGE_WIDGET_DROP_ANIMATION = 4; + + // Related to dragging, folder creation and reordering + private static final int DRAG_MODE_NONE = 0; + private static final int DRAG_MODE_CREATE_FOLDER = 1; + private static final int DRAG_MODE_ADD_TO_FOLDER = 2; + private static final int DRAG_MODE_REORDER = 3; + private int mDragMode = DRAG_MODE_NONE; + private int mLastReorderX = -1; + private int mLastReorderY = -1; + + // Relating to workspace drag fade out + private float mDragFadeOutAlpha; + private int mDragFadeOutDuration; + // These variables are used for storing the initial and final values during workspace animations private int mSavedScrollX; private float mSavedRotationY; @@ -261,8 +278,10 @@ public class Workspace extends SmoothPagedView // With workspace, data is available straight from the get-go setDataIsReady(); - mFadeInAdjacentScreens = - getResources().getBoolean(R.bool.config_workspaceFadeAdjacentScreens); + final Resources res = getResources(); + mFadeInAdjacentScreens = res.getBoolean(R.bool.config_workspaceFadeAdjacentScreens); + mDragFadeOutAlpha = res.getInteger(R.integer.config_dragFadeOutAlpha) / 100f; + mDragFadeOutDuration = res.getInteger(R.integer.config_dragFadeOutDuration); mWallpaperManager = WallpaperManager.getInstance(context); int cellCountX = DEFAULT_CELL_COUNT_X; @@ -271,7 +290,6 @@ public class Workspace extends SmoothPagedView TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Workspace, defStyle, 0); - final Resources res = context.getResources(); if (LauncherApplication.isScreenLarge()) { // Determine number of rows/columns dynamically // TODO: This code currently fails on tablets with an aspect ratio < 1.3. @@ -279,9 +297,11 @@ public class Workspace extends SmoothPagedView // landscape TypedArray actionBarSizeTypedArray = context.obtainStyledAttributes(new int[] { android.R.attr.actionBarSize }); + DisplayMetrics displayMetrics = res.getDisplayMetrics(); final float actionBarHeight = actionBarSizeTypedArray.getDimension(0, 0f); final float systemBarHeight = res.getDimension(R.dimen.status_bar_height); - final float smallestScreenDim = res.getConfiguration().smallestScreenWidthDp; + final float smallestScreenDim = res.getConfiguration().smallestScreenWidthDp * + displayMetrics.density; cellCountX = 1; while (CellLayout.widthInPortrait(res, cellCountX + 1) <= smallestScreenDim) { @@ -297,7 +317,6 @@ public class Workspace extends SmoothPagedView mSpringLoadedShrinkFactor = res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f; - mDragViewMultiplyColor = res.getColor(R.color.drag_view_multiply_color); // if the value is manually specified, use that instead cellCountX = a.getInt(R.styleable.Workspace_cellCountX, cellCountX); @@ -318,13 +337,13 @@ public class Workspace extends SmoothPagedView // estimate the size of a widget with spans hSpan, vSpan. return MAX_VALUE for each // dimension if unsuccessful public int[] estimateItemSize(int hSpan, int vSpan, - PendingAddItemInfo pendingItemInfo, boolean springLoaded) { + ItemInfo itemInfo, boolean springLoaded) { int[] size = new int[2]; if (getChildCount() > 0) { CellLayout cl = (CellLayout) mLauncher.getWorkspace().getChildAt(0); - RectF r = estimateItemPosition(cl, pendingItemInfo, 0, 0, hSpan, vSpan); - size[0] = (int) r.width(); - size[1] = (int) r.height(); + Rect r = estimateItemPosition(cl, itemInfo, 0, 0, hSpan, vSpan); + size[0] = r.width(); + size[1] = r.height(); if (springLoaded) { size[0] *= mSpringLoadedShrinkFactor; size[1] *= mSpringLoadedShrinkFactor; @@ -336,19 +355,10 @@ public class Workspace extends SmoothPagedView return size; } } - public RectF estimateItemPosition(CellLayout cl, ItemInfo pendingInfo, + public Rect estimateItemPosition(CellLayout cl, ItemInfo pendingInfo, int hCell, int vCell, int hSpan, int vSpan) { - RectF r = new RectF(); + Rect r = new Rect(); cl.cellToRect(hCell, vCell, hSpan, vSpan, r); - if (pendingInfo instanceof PendingAddWidgetInfo) { - PendingAddWidgetInfo widgetInfo = (PendingAddWidgetInfo) pendingInfo; - Rect p = AppWidgetHostView.getDefaultPaddingForWidget(mContext, - widgetInfo.componentName, null); - r.top += p.top; - r.left += p.left; - r.right -= p.right; - r.bottom -= p.bottom; - } return r; } @@ -357,7 +367,7 @@ public class Workspace extends SmoothPagedView final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { CellLayout cl = (CellLayout) getChildAt(i); - cl.buildChildrenLayer(); + cl.getShortcutsAndWidgets().buildLayer(); } } } @@ -383,39 +393,25 @@ public class Workspace extends SmoothPagedView Launcher.setScreen(mCurrentPage); LauncherApplication app = (LauncherApplication)context.getApplicationContext(); mIconCache = app.getIconCache(); - mExternalDragOutlinePaint.setAntiAlias(true); setWillNotDraw(false); setChildrenDrawnWithCacheEnabled(true); + final Resources res = getResources(); try { - final Resources res = getResources(); mBackground = res.getDrawable(R.drawable.apps_customize_bg); } catch (Resources.NotFoundException e) { // In this case, we will skip drawing background protection } - mChangeStateAnimationListener = new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - mIsSwitchingState = true; - } - - @Override - public void onAnimationEnd(Animator animation) { - mIsSwitchingState = false; - mWallpaperOffset.setOverrideHorizontalCatchupConstant(false); - mAnimator = null; - updateChildrenLayersEnabled(); - } - }; - - mSnapVelocity = 600; mWallpaperOffset = new WallpaperOffsetInterpolator(); Display display = mLauncher.getWindowManager().getDefaultDisplay(); mDisplayWidth = display.getWidth(); mDisplayHeight = display.getHeight(); mWallpaperTravelWidth = (int) (mDisplayWidth * wallpaperTravelToScreenWidthRatio(mDisplayWidth, mDisplayHeight)); + + mMaxDistanceForFolderCreation = (0.5f * res.getDimensionPixelSize(R.dimen.app_icon_size)); + mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity); } @Override @@ -521,10 +517,12 @@ public class Workspace extends SmoothPagedView child.setOnKeyListener(new IconKeyEventListener()); } - CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); - if (lp == null) { + LayoutParams genericLp = child.getLayoutParams(); + CellLayout.LayoutParams lp; + if (genericLp == null || !(genericLp instanceof CellLayout.LayoutParams)) { lp = new CellLayout.LayoutParams(x, y, spanX, spanY); } else { + lp = (CellLayout.LayoutParams) genericLp; lp.cellX = x; lp.cellY = y; lp.cellHSpan = spanX; @@ -597,20 +595,26 @@ public class Workspace extends SmoothPagedView */ @Override public boolean onTouch(View v, MotionEvent event) { - return (isSmall() || mIsSwitchingState); + return (isSmall() || !isFinishedSwitchingState()); } public boolean isSwitchingState() { return mIsSwitchingState; } + /** This differs from isSwitchingState in that we take into account how far the transition + * has completed. */ + public boolean isFinishedSwitchingState() { + return !mIsSwitchingState || (mTransitionProgress > 0.5f); + } + protected void onWindowVisibilityChanged (int visibility) { mLauncher.onWindowVisibilityChanged(visibility); } @Override public boolean dispatchUnhandledMove(View focused, int direction) { - if (isSmall() || mIsSwitchingState) { + if (isSmall() || !isFinishedSwitchingState()) { // when the home screens are shrunken, shouldn't allow side-scrolling return false; } @@ -638,41 +642,42 @@ public class Workspace extends SmoothPagedView @Override protected void determineScrollingStart(MotionEvent ev) { - if (!isSmall() && !mIsSwitchingState) { - float deltaX = Math.abs(ev.getX() - mXDown); - float deltaY = Math.abs(ev.getY() - mYDown); + if (isSmall()) return; + if (!isFinishedSwitchingState()) return; - if (Float.compare(deltaX, 0f) == 0) return; + float deltaX = Math.abs(ev.getX() - mXDown); + float deltaY = Math.abs(ev.getY() - mYDown); - float slope = deltaY / deltaX; - float theta = (float) Math.atan(slope); + if (Float.compare(deltaX, 0f) == 0) return; - if (deltaX > mTouchSlop || deltaY > mTouchSlop) { - cancelCurrentPageLongPress(); - } + float slope = deltaY / deltaX; + float theta = (float) Math.atan(slope); - if (theta > MAX_SWIPE_ANGLE) { - // Above MAX_SWIPE_ANGLE, we don't want to ever start scrolling the workspace - return; - } else if (theta > START_DAMPING_TOUCH_SLOP_ANGLE) { - // Above START_DAMPING_TOUCH_SLOP_ANGLE and below MAX_SWIPE_ANGLE, we want to - // increase the touch slop to make it harder to begin scrolling the workspace. This - // results in vertically scrolling widgets to more easily. The higher the angle, the - // more we increase touch slop. - theta -= START_DAMPING_TOUCH_SLOP_ANGLE; - float extraRatio = (float) - Math.sqrt((theta / (MAX_SWIPE_ANGLE - START_DAMPING_TOUCH_SLOP_ANGLE))); - super.determineScrollingStart(ev, 1 + TOUCH_SLOP_DAMPING_FACTOR * extraRatio); - } else { - // Below START_DAMPING_TOUCH_SLOP_ANGLE, we don't do anything special - super.determineScrollingStart(ev); - } + if (deltaX > mTouchSlop || deltaY > mTouchSlop) { + cancelCurrentPageLongPress(); + } + + if (theta > MAX_SWIPE_ANGLE) { + // Above MAX_SWIPE_ANGLE, we don't want to ever start scrolling the workspace + return; + } else if (theta > START_DAMPING_TOUCH_SLOP_ANGLE) { + // Above START_DAMPING_TOUCH_SLOP_ANGLE and below MAX_SWIPE_ANGLE, we want to + // increase the touch slop to make it harder to begin scrolling the workspace. This + // results in vertically scrolling widgets to more easily. The higher the angle, the + // more we increase touch slop. + theta -= START_DAMPING_TOUCH_SLOP_ANGLE; + float extraRatio = (float) + Math.sqrt((theta / (MAX_SWIPE_ANGLE - START_DAMPING_TOUCH_SLOP_ANGLE))); + super.determineScrollingStart(ev, 1 + TOUCH_SLOP_DAMPING_FACTOR * extraRatio); + } else { + // Below START_DAMPING_TOUCH_SLOP_ANGLE, we don't do anything special + super.determineScrollingStart(ev); } } @Override protected boolean isScrollingIndicatorEnabled() { - return mState != State.SPRING_LOADED; + return super.isScrollingIndicatorEnabled() && (mState != State.SPRING_LOADED); } protected void onPageBeginMoving() { @@ -694,7 +699,19 @@ public class Workspace extends SmoothPagedView // Only show page outlines as we pan if we are on large screen if (LauncherApplication.isScreenLarge()) { showOutlines(); + mIsStaticWallpaper = mWallpaperManager.getWallpaperInfo() == null; } + + // If we are not fading in adjacent screens, we still need to restore the alpha in case the + // user scrolls while we are transitioning (should not affect dispatchDraw optimizations) + if (!mFadeInAdjacentScreens) { + for (int i = 0; i < getChildCount(); ++i) { + ((CellLayout) getPageAt(i)).setShortcutAndWidgetAlpha(1f); + } + } + + // Show the scroll indicator as you pan the page + showScrollingIndicator(false); } protected void onPageEndMoving() { @@ -706,12 +723,23 @@ public class Workspace extends SmoothPagedView clearChildrenCache(); } - // Hide the outlines, as long as we're not dragging - if (!mDragController.dragging()) { - // Only hide page outlines as we pan if we are on large screen + + if (mDragController.isDragging()) { + if (isSmall()) { + // If we are in springloaded mode, then force an event to check if the current touch + // is under a new page (to scroll to) + mDragController.forceMoveEvent(); + } + } else { + // If we are not mid-dragging, hide the page outlines if we are on a large screen if (LauncherApplication.isScreenLarge()) { hideOutlines(); } + + // Hide the scroll indicator as you pan the page + if (!mDragController.isDragging()) { + hideScrollingIndicator(false); + } } mOverScrollMaxBackgroundAlpha = 0.0f; mOverScrollPageIndex = -1; @@ -720,6 +748,11 @@ public class Workspace extends SmoothPagedView mDelayedResizeRunnable.run(); mDelayedResizeRunnable = null; } + + if (mDelayedSnapToPageRunnable != null) { + mDelayedSnapToPageRunnable.run(); + mDelayedSnapToPageRunnable = null; + } } @Override @@ -782,28 +815,7 @@ public class Workspace extends SmoothPagedView }.start(); } - public void setVerticalWallpaperOffset(float offset) { - mWallpaperOffset.setFinalY(offset); - } - public float getVerticalWallpaperOffset() { - return mWallpaperOffset.getCurrY(); - } - public void setHorizontalWallpaperOffset(float offset) { - mWallpaperOffset.setFinalX(offset); - } - public float getHorizontalWallpaperOffset() { - return mWallpaperOffset.getCurrX(); - } - private float wallpaperOffsetForCurrentScroll() { - // The wallpaper travel width is how far, from left to right, the wallpaper will move - // at this orientation. On tablets in portrait mode we don't move all the way to the - // edges of the wallpaper, or otherwise the parallax effect would be too strong. - int wallpaperTravelWidth = mWallpaperWidth; - if (LauncherApplication.isScreenLarge()) { - wallpaperTravelWidth = mWallpaperTravelWidth; - } - // Set wallpaper offset steps (1 / (number of screens - 1)) mWallpaperManager.setWallpaperOffsetSteps(1.0f / (getChildCount() - 1), 1.0f); @@ -821,11 +833,22 @@ public class Workspace extends SmoothPagedView float scrollProgress = adjustedScrollX / (float) scrollRange; - float offsetInDips = wallpaperTravelWidth * scrollProgress + - (mWallpaperWidth - wallpaperTravelWidth) / 2; // center it - float offset = offsetInDips / (float) mWallpaperWidth; - return offset; + + if (LauncherApplication.isScreenLarge() && mIsStaticWallpaper) { + // The wallpaper travel width is how far, from left to right, the wallpaper will move + // at this orientation. On tablets in portrait mode we don't move all the way to the + // edges of the wallpaper, or otherwise the parallax effect would be too strong. + int wallpaperTravelWidth = Math.min(mWallpaperTravelWidth, mWallpaperWidth); + + float offsetInDips = wallpaperTravelWidth * scrollProgress + + (mWallpaperWidth - wallpaperTravelWidth) / 2; // center it + float offset = offsetInDips / (float) mWallpaperWidth; + return offset; + } else { + return scrollProgress; + } } + private void syncWallpaperOffsetWithScroll() { final boolean enableWallpaperEffects = isHardwareAccelerated(); if (enableWallpaperEffects) { @@ -855,7 +878,7 @@ public class Workspace extends SmoothPagedView } } if (keepUpdating) { - fastInvalidate(); + invalidate(); } } @@ -871,6 +894,20 @@ public class Workspace extends SmoothPagedView computeWallpaperScrollRatio(whichPage); } + @Override + protected void snapToPage(int whichPage, int duration) { + super.snapToPage(whichPage, duration); + computeWallpaperScrollRatio(whichPage); + } + + protected void snapToPage(int whichPage, Runnable r) { + if (mDelayedSnapToPageRunnable != null) { + mDelayedSnapToPageRunnable.run(); + } + mDelayedSnapToPageRunnable = r; + snapToPage(whichPage, SLOW_PAGE_SNAP_ANIMATION_DURATION); + } + private void computeWallpaperScrollRatio(int page) { // Here, we determine what the desired scroll would be with and without a layout scale, // and compute a ratio between the two. This allows us to adjust the wallpaper offset @@ -1167,13 +1204,12 @@ public class Workspace extends SmoothPagedView backgroundAlphaInterpolator(Math.abs(scrollProgress))); } } - cl.setFastTranslationX(translationX); - cl.setFastRotationY(rotation); + cl.setTranslationX(translationX); + cl.setRotationY(rotation); if (mFadeInAdjacentScreens && !isSmall()) { float alpha = 1 - Math.abs(scrollProgress); - cl.setFastAlpha(alpha); + cl.setShortcutAndWidgetAlpha(alpha); } - cl.fastInvalidate(); } } if (!isSwitchingState() && !isInOverscroll) { @@ -1246,19 +1282,6 @@ public class Workspace extends SmoothPagedView mUpdateWallpaperOffsetImmediately = true; } super.onLayout(changed, left, top, right, bottom); - - // if shrinkToBottom() is called on initialization, it has to be deferred - // until after the first call to onLayout so that it has the correct width - if (mSwitchStateAfterFirstLayout) { - mSwitchStateAfterFirstLayout = false; - // shrink can trigger a synchronous onLayout call, so we - // post this to avoid a stack overflow / tangled onLayout calls - post(new Runnable() { - public void run() { - changeState(mStateAfterFirstLayout, false); - } - }); - } } @Override @@ -1281,32 +1304,6 @@ public class Workspace extends SmoothPagedView return (mBackground != null && mBackgroundAlpha > 0.0f && mDrawBackground); } - public void scrollTo (int x, int y) { - super.scrollTo(x, y); - syncChildrenLayersEnabledOnVisiblePages(); - } - - // This method just applies the value mChildrenLayersEnabled to all the pages that - // will be rendered on the next frame. - // We do this because calling setChildrenLayersEnabled on a view that's not - // visible/rendered causes slowdowns on some graphics cards - private void syncChildrenLayersEnabledOnVisiblePages() { - if (mChildrenLayersEnabled) { - getVisiblePages(mTempVisiblePagesRange); - final int leftScreen = mTempVisiblePagesRange[0]; - final int rightScreen = mTempVisiblePagesRange[1]; - if (leftScreen != -1 && rightScreen != -1) { - for (int i = leftScreen; i <= rightScreen; i++) { - ViewGroup page = (ViewGroup) getPageAt(i); - if (page.getVisibility() == VISIBLE && - page.getAlpha() > ViewConfiguration.ALPHA_THRESHOLD) { - ((ViewGroup)getPageAt(i)).setChildrenLayersEnabled(true); - } - } - } - } - } - @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); @@ -1321,8 +1318,9 @@ public class Workspace extends SmoothPagedView final int paddingTop = mPaddingTop + offset; final int paddingBottom = mPaddingBottom + offset; - final CellLayout leftPage = (CellLayout) getChildAt(mCurrentPage - 1); - final CellLayout rightPage = (CellLayout) getChildAt(mCurrentPage + 1); + final int page = (mNextPage != INVALID_PAGE ? mNextPage : mCurrentPage); + final CellLayout leftPage = (CellLayout) getChildAt(page - 1); + final CellLayout rightPage = (CellLayout) getChildAt(page + 1); if (leftPage != null && leftPage.getIsDragOverlapping()) { final Drawable d = getResources().getDrawable(R.drawable.page_hover_left_holo); @@ -1407,19 +1405,13 @@ public class Workspace extends SmoothPagedView } private void updateChildrenLayersEnabled() { - boolean small = isSmall() || mIsSwitchingState; - boolean dragging = mAnimatingViewIntoPlace || mIsDragOccuring; - boolean enableChildrenLayers = small || dragging || isPageMoving(); + boolean small = mState == State.SMALL || mIsSwitchingState; + boolean enableChildrenLayers = small || mAnimatingViewIntoPlace || isPageMoving(); if (enableChildrenLayers != mChildrenLayersEnabled) { mChildrenLayersEnabled = enableChildrenLayers; - // calling setChildrenLayersEnabled on a view that's not visible/rendered - // causes slowdowns on some graphics cards, so we only disable it here and leave - // the enabling to dispatchDraw - if (!enableChildrenLayers) { - for (int i = 0; i < getPageCount(); i++) { - ((ViewGroup)getChildAt(i)).setChildrenLayersEnabled(false); - } + for (int i = 0; i < getPageCount(); i++) { + ((ViewGroup)getChildAt(i)).setChildrenLayersEnabled(mChildrenLayersEnabled); } } } @@ -1442,7 +1434,7 @@ public class Workspace extends SmoothPagedView * This interpolator emulates the rate at which the perceived scale of an object changes * as its distance from a camera increases. When this interpolator is applied to a scale * animation on a view, it evokes the sense that the object is shrinking due to moving away - * from the camera. + * from the camera. */ static class ZInterpolator implements TimeInterpolator { private float focalLength; @@ -1508,23 +1500,18 @@ public class Workspace extends SmoothPagedView public void onDragStartedWithItem(View v) { final Canvas canvas = new Canvas(); - // We need to add extra padding to the bitmap to make room for the glow effect - final int bitmapPadding = HolographicOutlineHelper.MAX_OUTER_BLUR_RADIUS; - // The outline is used to visualize where the item will land if dropped - mDragOutline = createDragOutline(v, canvas, bitmapPadding); + mDragOutline = createDragOutline(v, canvas, DRAG_BITMAP_PADDING); } public void onDragStartedWithItem(PendingAddItemInfo info, Bitmap b, Paint alphaClipPaint) { final Canvas canvas = new Canvas(); - // We need to add extra padding to the bitmap to make room for the glow effect - final int bitmapPadding = HolographicOutlineHelper.MAX_OUTER_BLUR_RADIUS; - int[] size = estimateItemSize(info.spanX, info.spanY, info, false); // The outline is used to visualize where the item will land if dropped - mDragOutline = createDragOutline(b, canvas, bitmapPadding, size[0], size[1], alphaClipPaint); + mDragOutline = createDragOutline(b, canvas, DRAG_BITMAP_PADDING, size[0], + size[1], alphaClipPaint); } // we call this method whenever a drag and drop in Launcher finishes, even if Workspace was @@ -1562,38 +1549,26 @@ public class Workspace extends SmoothPagedView mNewRotationYs = new float[childCount]; } - public void changeState(State shrinkState) { - changeState(shrinkState, true); + Animator getChangeStateAnimation(final State state, boolean animated) { + return getChangeStateAnimation(state, animated, 0); } - void changeState(final State state, boolean animated) { - changeState(state, animated, 0); - } - - void changeState(final State state, boolean animated, int delay) { + Animator getChangeStateAnimation(final State state, boolean animated, int delay) { if (mState == state) { - return; - } - if (mFirstLayout) { - // (mFirstLayout == "first layout has not happened yet") - // cancel any pending shrinks that were set earlier - mSwitchStateAfterFirstLayout = false; - mStateAfterFirstLayout = state; - return; + return null; } // Initialize animation arrays for the first time if necessary initAnimationArrays(); - // Cancel any running transition animations - if (mAnimator != null) mAnimator.cancel(); - mAnimator = new AnimatorSet(); + AnimatorSet anim = animated ? new AnimatorSet() : null; // Stop any scrolling, move to the current page right away setCurrentPage((mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage); final State oldState = mState; final boolean oldStateIsNormal = (oldState == State.NORMAL); + final boolean oldStateIsSpringLoaded = (oldState == State.SPRING_LOADED); final boolean oldStateIsSmall = (oldState == State.SMALL); mState = state; final boolean stateIsNormal = (state == State.NORMAL); @@ -1619,7 +1594,7 @@ public class Workspace extends SmoothPagedView setLayoutScale(1.0f); } - final int duration = zoomIn ? + final int duration = zoomIn ? getResources().getInteger(R.integer.config_workspaceUnshrinkTime) : getResources().getInteger(R.integer.config_appsCustomizeWorkspaceShrinkTime); for (int i = 0; i < getChildCount(); i++) { @@ -1634,8 +1609,9 @@ public class Workspace extends SmoothPagedView if ((oldStateIsSmall && stateIsNormal) || (oldStateIsNormal && stateIsSmall)) { // To/from workspace - only show the current page unless the transition is not - // animated and the animation end callback below doesn't run - if (i == mCurrentPage || !animated) { + // animated and the animation end callback below doesn't run; + // or, if we're in spring-loaded mode + if (i == mCurrentPage || !animated || oldStateIsSpringLoaded) { finalAlpha = 1f; finalAlphaMultiplierValue = 0f; } else { @@ -1685,83 +1661,73 @@ public class Workspace extends SmoothPagedView cl.setScaleY(finalScaleFactor); cl.setBackgroundAlpha(finalBackgroundAlpha); cl.setBackgroundAlphaMultiplier(finalAlphaMultiplierValue); - cl.setAlpha(finalAlpha); + cl.setShortcutAndWidgetAlpha(finalAlpha); cl.setRotationY(rotation); - mChangeStateAnimationListener.onAnimationEnd(null); } } if (animated) { - ValueAnimator animWithInterpolator = - ValueAnimator.ofFloat(0f, 1f).setDuration(duration); - - if (zoomIn) { - animWithInterpolator.setInterpolator(mZoomInInterpolator); - } - - animWithInterpolator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(android.animation.Animator animation) { - // The above code to determine initialAlpha and finalAlpha will ensure that only - // the current page is visible during (and subsequently, after) the transition - // animation. If fade adjacent pages is disabled, then re-enable the page - // visibility after the transition animation. - if (!mFadeInAdjacentScreens && stateIsNormal && oldStateIsSmall) { - for (int i = 0; i < getChildCount(); i++) { - final CellLayout cl = (CellLayout) getChildAt(i); - cl.setAlpha(1f); - } - } - } - }); - animWithInterpolator.addUpdateListener(new LauncherAnimatorUpdateListener() { - public void onAnimationUpdate(float a, float b) { - mTransitionProgress = b; - if (b == 0f) { - // an optimization, but not required - return; - } - invalidate(); - for (int i = 0; i < getChildCount(); i++) { - final CellLayout cl = (CellLayout) getChildAt(i); - cl.invalidate(); - cl.setFastTranslationX(a * mOldTranslationXs[i] + b * mNewTranslationXs[i]); - cl.setFastTranslationY(a * mOldTranslationYs[i] + b * mNewTranslationYs[i]); - cl.setFastScaleX(a * mOldScaleXs[i] + b * mNewScaleXs[i]); - cl.setFastScaleY(a * mOldScaleYs[i] + b * mNewScaleYs[i]); - cl.setFastBackgroundAlpha( - a * mOldBackgroundAlphas[i] + b * mNewBackgroundAlphas[i]); - cl.setBackgroundAlphaMultiplier(a * mOldBackgroundAlphaMultipliers[i] + - b * mNewBackgroundAlphaMultipliers[i]); - cl.setFastAlpha(a * mOldAlphas[i] + b * mNewAlphas[i]); - cl.invalidate(); + for (int index = 0; index < getChildCount(); index++) { + final int i = index; + final CellLayout cl = (CellLayout) getChildAt(i); + if (mOldAlphas[i] == 0 && mNewAlphas[i] == 0) { + cl.setTranslationX(mNewTranslationXs[i]); + cl.setTranslationY(mNewTranslationYs[i]); + cl.setScaleX(mNewScaleXs[i]); + cl.setScaleY(mNewScaleYs[i]); + cl.setBackgroundAlpha(mNewBackgroundAlphas[i]); + cl.setBackgroundAlphaMultiplier(mNewBackgroundAlphaMultipliers[i]); + cl.setShortcutAndWidgetAlpha(mNewAlphas[i]); + cl.setRotationY(mNewRotationYs[i]); + } else { + LauncherViewPropertyAnimator a = new LauncherViewPropertyAnimator(cl); + a.translationX(mNewTranslationXs[i]) + .translationY(mNewTranslationYs[i]) + .scaleX(mNewScaleXs[i]) + .scaleY(mNewScaleYs[i]) + .setDuration(duration) + .setInterpolator(mZoomInInterpolator); + anim.play(a); + + if (mOldAlphas[i] != mNewAlphas[i]) { + LauncherViewPropertyAnimator alphaAnim = + new LauncherViewPropertyAnimator(cl.getShortcutsAndWidgets()); + alphaAnim.alpha(mNewAlphas[i]) + .setDuration(duration) + .setInterpolator(mZoomInInterpolator); + anim.play(alphaAnim); } - syncChildrenLayersEnabledOnVisiblePages(); - } - }); - - ValueAnimator rotationAnim = - ValueAnimator.ofFloat(0f, 1f).setDuration(duration); - rotationAnim.setInterpolator(new DecelerateInterpolator(2.0f)); - rotationAnim.addUpdateListener(new LauncherAnimatorUpdateListener() { - public void onAnimationUpdate(float a, float b) { - if (b == 0f) { - // an optimization, but not required - return; + if (mOldRotationYs[i] != 0 || mNewRotationYs[i] != 0) { + ValueAnimator rotate = ValueAnimator.ofFloat(0f, 1f).setDuration(duration); + rotate.setInterpolator(new DecelerateInterpolator(2.0f)); + rotate.addUpdateListener(new LauncherAnimatorUpdateListener() { + public void onAnimationUpdate(float a, float b) { + cl.setRotationY(a * mOldRotationYs[i] + b * mNewRotationYs[i]); + } + }); + anim.play(rotate); } - for (int i = 0; i < getChildCount(); i++) { - final CellLayout cl = (CellLayout) getChildAt(i); - cl.setFastRotationY(a * mOldRotationYs[i] + b * mNewRotationYs[i]); + if (mOldBackgroundAlphas[i] != 0 || + mNewBackgroundAlphas[i] != 0 || + mOldBackgroundAlphaMultipliers[i] != 0 || + mNewBackgroundAlphaMultipliers[i] != 0) { + ValueAnimator bgAnim = ValueAnimator.ofFloat(0f, 1f).setDuration(duration); + bgAnim.setInterpolator(mZoomInInterpolator); + bgAnim.addUpdateListener(new LauncherAnimatorUpdateListener() { + public void onAnimationUpdate(float a, float b) { + cl.setBackgroundAlpha( + a * mOldBackgroundAlphas[i] + + b * mNewBackgroundAlphas[i]); + cl.setBackgroundAlphaMultiplier( + a * mOldBackgroundAlphaMultipliers[i] + + b * mNewBackgroundAlphaMultipliers[i]); + } + }); + anim.play(bgAnim); } } - }); - - mAnimator.playTogether(animWithInterpolator, rotationAnim); - mAnimator.setStartDelay(delay); - // If we call this when we're not animated, onAnimationEnd is never called on - // the listener; make sure we only use the listener when we're actually animating - mAnimator.addListener(mChangeStateAnimationListener); - mAnimator.start(); + } + anim.setStartDelay(delay); } if (stateIsSpringLoaded) { @@ -1774,7 +1740,40 @@ public class Workspace extends SmoothPagedView // Fade the background gradient away animateBackgroundGradient(0f, true); } - syncChildrenLayersEnabledOnVisiblePages(); + return anim; + } + + @Override + public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { + mIsSwitchingState = true; + cancelScrollingIndicatorAnimations(); + } + + @Override + public void onLauncherTransitionStep(Launcher l, float t) { + mTransitionProgress = t; + } + + @Override + public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { + mIsSwitchingState = false; + mWallpaperOffset.setOverrideHorizontalCatchupConstant(false); + updateChildrenLayersEnabled(); + // The code in getChangeStateAnimation to determine initialAlpha and finalAlpha will ensure + // ensure that only the current page is visible during (and subsequently, after) the + // transition animation. If fade adjacent pages is disabled, then re-enable the page + // visibility after the transition animation. + if (!mFadeInAdjacentScreens) { + for (int i = 0; i < getChildCount(); i++) { + final CellLayout cl = (CellLayout) getChildAt(i); + cl.setShortcutAndWidgetAlpha(1f); + } + } + } + + @Override + public View getContent() { + return this; } /** @@ -1844,8 +1843,6 @@ public class Workspace extends SmoothPagedView canvas.setBitmap(b); drawDragView(v, canvas, padding, true); - mOutlineHelper.applyOuterBlur(b, canvas, outlineColor); - canvas.drawColor(mDragViewMultiplyColor, PorterDuff.Mode.MULTIPLY); canvas.setBitmap(null); return b; @@ -1895,29 +1892,6 @@ public class Workspace extends SmoothPagedView return b; } - /** - * Creates a drag outline to represent a drop (that we don't have the actual information for - * yet). May be changed in the future to alter the drop outline slightly depending on the - * clip description mime data. - */ - private Bitmap createExternalDragOutline(Canvas canvas, int padding) { - Resources r = getResources(); - final int outlineColor = r.getColor(android.R.color.holo_blue_light); - final int iconWidth = r.getDimensionPixelSize(R.dimen.workspace_cell_width); - final int iconHeight = r.getDimensionPixelSize(R.dimen.workspace_cell_height); - final int rectRadius = r.getDimensionPixelSize(R.dimen.external_drop_icon_rect_radius); - final int inset = (int) (Math.min(iconWidth, iconHeight) * 0.2f); - final Bitmap b = Bitmap.createBitmap( - iconWidth + padding, iconHeight + padding, Bitmap.Config.ARGB_8888); - - canvas.setBitmap(b); - canvas.drawRoundRect(new RectF(inset, inset, iconWidth - inset, iconHeight - inset), - rectRadius, rectRadius, mExternalDragOutlinePaint); - mOutlineHelper.applyMediumExpensiveOutlineWithBlur(b, canvas, outlineColor, outlineColor); - canvas.setBitmap(null); - return b; - } - void startDrag(CellLayout.CellInfo cellInfo) { View child = cellInfo.cell; @@ -1927,35 +1901,35 @@ public class Workspace extends SmoothPagedView } mDragInfo = cellInfo; - child.setVisibility(GONE); + child.setVisibility(INVISIBLE); + CellLayout layout = (CellLayout) child.getParent().getParent(); + layout.prepareChildForDrag(child); child.clearFocus(); child.setPressed(false); final Canvas canvas = new Canvas(); - // We need to add extra padding to the bitmap to make room for the glow effect - final int bitmapPadding = HolographicOutlineHelper.MAX_OUTER_BLUR_RADIUS; - // The outline is used to visualize where the item will land if dropped - mDragOutline = createDragOutline(child, canvas, bitmapPadding); + mDragOutline = createDragOutline(child, canvas, DRAG_BITMAP_PADDING); beginDragShared(child, this); } public void beginDragShared(View child, DragSource source) { Resources r = getResources(); - // We need to add extra padding to the bitmap to make room for the glow effect - final int bitmapPadding = HolographicOutlineHelper.MAX_OUTER_BLUR_RADIUS; - // The drag bitmap follows the touch point around on the screen - final Bitmap b = createDragBitmap(child, new Canvas(), bitmapPadding); + final Bitmap b = createDragBitmap(child, new Canvas(), DRAG_BITMAP_PADDING); final int bmpWidth = b.getWidth(); + final int bmpHeight = b.getHeight(); mLauncher.getDragLayer().getLocationInDragLayer(child, mTempXY); - final int dragLayerX = (int) mTempXY[0] + (child.getWidth() - bmpWidth) / 2; - int dragLayerY = mTempXY[1] - bitmapPadding / 2; + int dragLayerX = + Math.round(mTempXY[0] - (bmpWidth - child.getScaleX() * child.getWidth()) / 2); + int dragLayerY = + Math.round(mTempXY[1] - (bmpHeight - child.getScaleY() * bmpHeight) / 2 + - DRAG_BITMAP_PADDING / 2); Point dragVisualizeOffset = null; Rect dragRect = null; @@ -1969,7 +1943,8 @@ public class Workspace extends SmoothPagedView dragLayerY += top; // Note: The drag region is used to calculate drag layer offsets, but the // dragVisualizeOffset in addition to the dragRect (the size) to position the outline. - dragVisualizeOffset = new Point(-bitmapPadding / 2, iconPaddingTop - bitmapPadding / 2); + dragVisualizeOffset = new Point(-DRAG_BITMAP_PADDING / 2, + iconPaddingTop - DRAG_BITMAP_PADDING / 2); dragRect = new Rect(left, top, right, bottom); } else if (child instanceof FolderIcon) { int previewSize = r.getDimensionPixelSize(R.dimen.folder_preview_size); @@ -1983,8 +1958,11 @@ public class Workspace extends SmoothPagedView } mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(), - DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect); + DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, child.getScaleX()); b.recycle(); + + // Show the scrolling indicator when you pick up an item + showScrollingIndicator(false); } void addApplicationShortcut(ShortcutInfo info, CellLayout target, long container, int screen, @@ -1999,7 +1977,7 @@ public class Workspace extends SmoothPagedView } public boolean transitionStateShouldAllowDrop() { - return (!isSwitchingState() || mTransitionProgress > 0.5f); + return ((!isSwitchingState() || mTransitionProgress > 0.5f) && mState != State.SMALL); } /** @@ -2026,56 +2004,78 @@ public class Workspace extends SmoothPagedView int spanX = 1; int spanY = 1; - View ignoreView = null; if (mDragInfo != null) { final CellLayout.CellInfo dragCellInfo = mDragInfo; spanX = dragCellInfo.spanX; spanY = dragCellInfo.spanY; - ignoreView = dragCellInfo.cell; } else { final ItemInfo dragInfo = (ItemInfo) d.dragInfo; spanX = dragInfo.spanX; spanY = dragInfo.spanY; } + int minSpanX = spanX; + int minSpanY = spanY; + if (d.dragInfo instanceof PendingAddWidgetInfo) { + minSpanX = ((PendingAddWidgetInfo) d.dragInfo).minSpanX; + minSpanY = ((PendingAddWidgetInfo) d.dragInfo).minSpanY; + } + mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], - (int) mDragViewVisualCenter[1], spanX, spanY, mDragTargetLayout, mTargetCell); - if (willCreateUserFolder((ItemInfo) d.dragInfo, mDragTargetLayout, mTargetCell, true)) { + (int) mDragViewVisualCenter[1], minSpanX, minSpanY, mDragTargetLayout, + mTargetCell); + float distance = mDragTargetLayout.getDistanceFromCell(mDragViewVisualCenter[0], + mDragViewVisualCenter[1], mTargetCell); + if (willCreateUserFolder((ItemInfo) d.dragInfo, mDragTargetLayout, + mTargetCell, distance, true)) { return true; } if (willAddToExistingUserFolder((ItemInfo) d.dragInfo, mDragTargetLayout, - mTargetCell)) { + mTargetCell, distance)) { return true; } + int[] resultSpan = new int[2]; + mTargetCell = mDragTargetLayout.createArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY, + null, mTargetCell, resultSpan, CellLayout.MODE_ACCEPT_DROP); + boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0; + // Don't accept the drop if there's no room for the item - if (!mDragTargetLayout.findCellForSpanIgnoring(null, spanX, spanY, ignoreView)) { + if (!foundCell) { // Don't show the message if we are dropping on the AllApps button and the hotseat // is full - if (mTargetCell != null && mLauncher.isHotseatLayout(mDragTargetLayout)) { + boolean isHotseat = mLauncher.isHotseatLayout(mDragTargetLayout); + if (mTargetCell != null && isHotseat) { Hotseat hotseat = mLauncher.getHotseat(); - if (Hotseat.isAllAppsButtonRank( + if (hotseat.isAllAppsButtonRank( hotseat.getOrderInHotseat(mTargetCell[0], mTargetCell[1]))) { return false; } } - mLauncher.showOutOfSpaceMessage(); + mLauncher.showOutOfSpaceMessage(isHotseat); return false; } } return true; } - boolean willCreateUserFolder(ItemInfo info, CellLayout target, int[] targetCell, - boolean considerTimeout) { + boolean willCreateUserFolder(ItemInfo info, CellLayout target, int[] targetCell, float + distance, boolean considerTimeout) { + if (distance > mMaxDistanceForFolderCreation) return false; View dropOverView = target.getChildAt(targetCell[0], targetCell[1]); + if (dropOverView != null) { + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) dropOverView.getLayoutParams(); + if (lp.useTmpCoords && (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.tmpCellY)) { + return false; + } + } + boolean hasntMoved = false; if (mDragInfo != null) { - CellLayout cellParent = getParentCellLayoutForView(mDragInfo.cell); - hasntMoved = (mDragInfo.cellX == targetCell[0] && - mDragInfo.cellY == targetCell[1]) && (cellParent == target); + hasntMoved = dropOverView == mDragInfo.cell; } if (dropOverView == null || hasntMoved || (considerTimeout && !mCreateUserFolderOnDrop)) { @@ -2090,8 +2090,18 @@ public class Workspace extends SmoothPagedView return (aboveShortcut && willBecomeShortcut); } - boolean willAddToExistingUserFolder(Object dragInfo, CellLayout target, int[] targetCell) { + boolean willAddToExistingUserFolder(Object dragInfo, CellLayout target, int[] targetCell, + float distance) { + if (distance > mMaxDistanceForFolderCreation) return false; View dropOverView = target.getChildAt(targetCell[0], targetCell[1]); + + if (dropOverView != null) { + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) dropOverView.getLayoutParams(); + if (lp.useTmpCoords && (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.tmpCellY)) { + return false; + } + } + if (dropOverView instanceof FolderIcon) { FolderIcon fi = (FolderIcon) dropOverView; if (fi.acceptDrop(dragInfo)) { @@ -2102,8 +2112,11 @@ public class Workspace extends SmoothPagedView } boolean createUserFolderIfNecessary(View newView, long container, CellLayout target, - int[] targetCell, boolean external, DragView dragView, Runnable postAnimationRunnable) { + int[] targetCell, float distance, boolean external, DragView dragView, + Runnable postAnimationRunnable) { + if (distance > mMaxDistanceForFolderCreation) return false; View v = target.getChildAt(targetCell[0], targetCell[1]); + boolean hasntMoved = false; if (mDragInfo != null) { CellLayout cellParent = getParentCellLayoutForView(mDragInfo.cell); @@ -2152,8 +2165,13 @@ public class Workspace extends SmoothPagedView } boolean addToExistingFolderIfNecessary(View newView, CellLayout target, int[] targetCell, - DragObject d, boolean external) { + float distance, DragObject d, boolean external) { + if (distance > mMaxDistanceForFolderCreation) return false; + View dropOverView = target.getChildAt(targetCell[0], targetCell[1]); + if (!mWillAddToExistingFolder) return false; + mWillAddToExistingFolder = false; + if (dropOverView instanceof FolderIcon) { FolderIcon fi = (FolderIcon) dropOverView; if (fi.acceptDrop(d.dragInfo)) { @@ -2169,7 +2187,7 @@ public class Workspace extends SmoothPagedView return false; } - public void onDrop(DragObject d) { + public void onDrop(final DragObject d) { mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView, mDragViewVisualCenter); @@ -2185,6 +2203,7 @@ public class Workspace extends SmoothPagedView CellLayout dropTargetLayout = mDragTargetLayout; int snapScreen = -1; + boolean resizeOnDrop = false; if (d.dragSource != this) { final int[] touchXY = new int[] { (int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1] }; @@ -2192,6 +2211,7 @@ public class Workspace extends SmoothPagedView } else if (mDragInfo != null) { final View cell = mDragInfo.cell; + Runnable resizeRunnable = null; if (dropTargetLayout != null) { // Move internally boolean hasMovedLayouts = (getParentCellLayoutForView(cell) != dropTargetLayout); @@ -2205,44 +2225,67 @@ public class Workspace extends SmoothPagedView int spanY = mDragInfo != null ? mDragInfo.spanY : 1; // First we find the cell nearest to point at which the item is // dropped, without any consideration to whether there is an item there. + mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], spanX, spanY, dropTargetLayout, mTargetCell); + float distance = dropTargetLayout.getDistanceFromCell(mDragViewVisualCenter[0], + mDragViewVisualCenter[1], mTargetCell); + // If the item being dropped is a shortcut and the nearest drop // cell also contains a shortcut, then create a folder with the two shortcuts. if (!mInScrollArea && createUserFolderIfNecessary(cell, container, - dropTargetLayout, mTargetCell, false, d.dragView, null)) { + dropTargetLayout, mTargetCell, distance, false, d.dragView, null)) { return; } - if (addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell, d, false)) { + if (addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell, + distance, d, false)) { return; } // Aside from the special case where we're dropping a shortcut onto a shortcut, // we need to find the nearest cell location that is vacant - mTargetCell = findNearestVacantArea((int) mDragViewVisualCenter[0], - (int) mDragViewVisualCenter[1], mDragInfo.spanX, mDragInfo.spanY, cell, - dropTargetLayout, mTargetCell); + ItemInfo item = (ItemInfo) d.dragInfo; + int minSpanX = item.spanX; + int minSpanY = item.spanY; + if (item.minSpanX > 0 && item.minSpanY > 0) { + minSpanX = item.minSpanX; + minSpanY = item.minSpanY; + } + + int[] resultSpan = new int[2]; + mTargetCell = mDragTargetLayout.createArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY, cell, + mTargetCell, resultSpan, CellLayout.MODE_ON_DROP); + + boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0; + if (foundCell && (resultSpan[0] != item.spanX || resultSpan[1] != item.spanY)) { + resizeOnDrop = true; + item.spanX = resultSpan[0]; + item.spanY = resultSpan[1]; + } if (mCurrentPage != screen && !hasMovedIntoHotseat) { snapScreen = screen; snapToPage(screen); } - if (mTargetCell[0] >= 0 && mTargetCell[1] >= 0) { + if (foundCell) { + final ItemInfo info = (ItemInfo) cell.getTag(); if (hasMovedLayouts) { // Reparent the view getParentCellLayoutForView(cell).removeView(cell); addInScreen(cell, container, screen, mTargetCell[0], mTargetCell[1], - mDragInfo.spanX, mDragInfo.spanY); + info.spanX, info.spanY); } // update the item's position after drop - final ItemInfo info = (ItemInfo) cell.getTag(); CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams(); - dropTargetLayout.onMove(cell, mTargetCell[0], mTargetCell[1]); - lp.cellX = mTargetCell[0]; - lp.cellY = mTargetCell[1]; + lp.cellX = lp.tmpCellX = mTargetCell[0]; + lp.cellY = lp.tmpCellY = mTargetCell[1]; + lp.cellHSpan = item.spanX; + lp.cellVSpan = item.spanY; + lp.isLockedToGrid = true; cell.setId(LauncherModel.getCellLayoutChildId(container, mDragInfo.screen, mTargetCell[0], mTargetCell[1], mDragInfo.spanX, mDragInfo.spanY)); @@ -2254,19 +2297,20 @@ public class Workspace extends SmoothPagedView final LauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) cell; AppWidgetProviderInfo pinfo = hostView.getAppWidgetInfo(); - if (pinfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE) { - final Runnable resizeRunnable = new Runnable() { + if (pinfo != null && + pinfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE) { + final Runnable addResizeFrame = new Runnable() { public void run() { DragLayer dragLayer = mLauncher.getDragLayer(); dragLayer.addResizeFrame(info, hostView, cellLayout); } }; - post(new Runnable() { + resizeRunnable = (new Runnable() { public void run() { if (!isPageMoving()) { - resizeRunnable.run(); + addResizeFrame.run(); } else { - mDelayedResizeRunnable = resizeRunnable; + mDelayedResizeRunnable = addResizeFrame; } } }); @@ -2275,28 +2319,43 @@ public class Workspace extends SmoothPagedView LauncherModel.moveItemInDatabase(mLauncher, info, container, screen, lp.cellX, lp.cellY); + } else { + // If we can't find a drop location, we return the item to its original position + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams(); + mTargetCell[0] = lp.cellX; + mTargetCell[1] = lp.cellY; } } final CellLayout parent = (CellLayout) cell.getParent().getParent(); - + final Runnable finalResizeRunnable = resizeRunnable; // Prepare it to be animated into its new position // This must be called after the view has been re-parented - final Runnable disableHardwareLayersRunnable = new Runnable() { + final Runnable onCompleteRunnable = new Runnable() { @Override public void run() { mAnimatingViewIntoPlace = false; updateChildrenLayersEnabled(); + if (finalResizeRunnable != null) { + finalResizeRunnable.run(); + } } }; mAnimatingViewIntoPlace = true; if (d.dragView.hasDrawn()) { - int duration = snapScreen < 0 ? -1 : ADJACENT_SCREEN_DROP_DURATION; - setFinalScrollForPageChange(snapScreen); - mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, cell, duration, - disableHardwareLayersRunnable); - resetFinalScrollForPageChange(snapScreen); + final ItemInfo info = (ItemInfo) cell.getTag(); + if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET) { + int animationType = resizeOnDrop ? ANIMATE_INTO_POSITION_AND_RESIZE : + ANIMATE_INTO_POSITION_AND_DISAPPEAR; + animateWidgetDrop(info, parent, d.dragView, + onCompleteRunnable, animationType, cell, false); + } else { + int duration = snapScreen < 0 ? -1 : ADJACENT_SCREEN_DROP_DURATION; + mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, cell, duration, + onCompleteRunnable, this); + } } else { + d.deferDragViewCleanupPostAnimation = false; cell.setVisibility(VISIBLE); } parent.onDropChild(cell); @@ -2339,6 +2398,7 @@ public class Workspace extends SmoothPagedView } public void onDragEnter(DragObject d) { + mDragHasEnteredWorkspace = true; if (mDragTargetLayout != null) { mDragTargetLayout.setIsDragOverlapping(false); mDragTargetLayout.onDragExit(); @@ -2358,6 +2418,13 @@ public class Workspace extends SmoothPagedView // Clean up folders cleanupFolderCreation(d); + // Clean up reorder + if (mReorderAlarm != null) { + mReorderAlarm.cancelAlarm(); + mLastReorderX = -1; + mLastReorderY = -1; + } + // Reset the scroll area and previous drag target onResetScrollArea(); @@ -2366,6 +2433,7 @@ public class Workspace extends SmoothPagedView mDragTargetLayout.onDragExit(); } mLastDragOverView = null; + mDragMode = DRAG_MODE_NONE; mSpringLoadedDragController.cancel(); if (!mIsPageMoving) { @@ -2374,6 +2442,7 @@ public class Workspace extends SmoothPagedView } public void onDragExit(DragObject d) { + mDragHasEnteredWorkspace = false; doDragExit(d); } @@ -2405,116 +2474,6 @@ public class Workspace extends SmoothPagedView return null; } - /** - * Global drag and drop handler - */ - @Override - public boolean onDragEvent(DragEvent event) { - final ClipDescription desc = event.getClipDescription(); - final CellLayout layout = (CellLayout) getChildAt(mCurrentPage); - final int[] pos = new int[2]; - layout.getLocationOnScreen(pos); - // We need to offset the drag coordinates to layout coordinate space - final int x = (int) event.getX() - pos[0]; - final int y = (int) event.getY() - pos[1]; - - switch (event.getAction()) { - case DragEvent.ACTION_DRAG_STARTED: { - // Validate this drag - Pair<Integer, List<WidgetMimeTypeHandlerData>> test = validateDrag(event); - if (test != null) { - boolean isShortcut = (test.second == null); - if (isShortcut) { - // Check if we have enough space on this screen to add a new shortcut - if (!layout.findCellForSpan(pos, 1, 1)) { - mLauncher.showOutOfSpaceMessage(); - return false; - } - } - } else { - // Show error message if we couldn't accept any of the items - Toast.makeText(mContext, mContext.getString(R.string.external_drop_widget_error), - Toast.LENGTH_SHORT).show(); - return false; - } - - // Create the drag outline - // We need to add extra padding to the bitmap to make room for the glow effect - final Canvas canvas = new Canvas(); - final int bitmapPadding = HolographicOutlineHelper.MAX_OUTER_BLUR_RADIUS; - mDragOutline = createExternalDragOutline(canvas, bitmapPadding); - - // Show the current page outlines to indicate that we can accept this drop - showOutlines(); - layout.onDragEnter(); - layout.visualizeDropLocation(null, mDragOutline, x, y, 1, 1, null, null); - - return true; - } - case DragEvent.ACTION_DRAG_LOCATION: - // Visualize the drop location - layout.visualizeDropLocation(null, mDragOutline, x, y, 1, 1, null, null); - return true; - case DragEvent.ACTION_DROP: { - // Try and add any shortcuts - final LauncherModel model = mLauncher.getModel(); - final ClipData data = event.getClipData(); - - // We assume that the mime types are ordered in descending importance of - // representation. So we enumerate the list of mime types and alert the - // user if any widgets can handle the drop. Only the most preferred - // representation will be handled. - pos[0] = x; - pos[1] = y; - Pair<Integer, List<WidgetMimeTypeHandlerData>> test = validateDrag(event); - if (test != null) { - final int index = test.first; - final List<WidgetMimeTypeHandlerData> widgets = test.second; - final boolean isShortcut = (widgets == null); - final String mimeType = desc.getMimeType(index); - if (isShortcut) { - final Intent intent = data.getItemAt(index).getIntent(); - Object info = model.infoFromShortcutIntent(mContext, intent, data.getIcon()); - if (info != null) { - onDropExternal(new int[] { x, y }, info, layout, false); - } - } else { - if (widgets.size() == 1) { - // If there is only one item, then go ahead and add and configure - // that widget - final AppWidgetProviderInfo widgetInfo = widgets.get(0).widgetInfo; - final PendingAddWidgetInfo createInfo = - new PendingAddWidgetInfo(widgetInfo, mimeType, data); - mLauncher.addAppWidgetFromDrop(createInfo, - LauncherSettings.Favorites.CONTAINER_DESKTOP, mCurrentPage, null, pos); - } else { - // Show the widget picker dialog if there is more than one widget - // that can handle this data type - final InstallWidgetReceiver.WidgetListAdapter adapter = - new InstallWidgetReceiver.WidgetListAdapter(mLauncher, mimeType, - data, widgets, layout, mCurrentPage, pos); - final AlertDialog.Builder builder = - new AlertDialog.Builder(mContext); - builder.setAdapter(adapter, adapter); - builder.setCancelable(true); - builder.setTitle(mContext.getString( - R.string.external_drop_widget_pick_title)); - builder.setIcon(R.drawable.ic_no_applications); - builder.show(); - } - } - } - return true; - } - case DragEvent.ACTION_DRAG_ENDED: - // Hide the page outlines after the drop - layout.onDragExit(); - hideOutlines(); - return true; - } - return super.onDragEvent(event); - } - /* * * Convert the 2D coordinate xy from the parent View's coordinate space to this CellLayout's @@ -2540,7 +2499,11 @@ public class Workspace extends SmoothPagedView v.getMatrix().invert(mTempInverseMatrix); cachedInverseMatrix = mTempInverseMatrix; } - xy[0] = xy[0] + mScrollX - v.getLeft(); + int scrollX = mScrollX; + if (mNextPage != INVALID_PAGE) { + scrollX = mScroller.getFinalX(); + } + xy[0] = xy[0] + scrollX - v.getLeft(); xy[1] = xy[1] + mScrollY - v.getTop(); cachedInverseMatrix.mapPoints(xy); } @@ -2562,7 +2525,11 @@ public class Workspace extends SmoothPagedView */ void mapPointFromChildToSelf(View v, float[] xy) { v.getMatrix().mapPoints(xy); - xy[0] -= (mScrollX - v.getLeft()); + int scrollX = mScrollX; + if (mNextPage != INVALID_PAGE) { + scrollX = mScroller.getFinalX(); + } + xy[0] -= (scrollX - v.getLeft()); xy[1] -= (mScrollY - v.getTop()); } @@ -2707,8 +2674,7 @@ public class Workspace extends SmoothPagedView public void onDragOver(DragObject d) { // Skip drag over events while we are dragging over side pages - if (mInScrollArea) return; - if (mIsSwitchingState) return; + if (mInScrollArea || mIsSwitchingState || mState == State.SMALL) return; Rect r = new Rect(); CellLayout layout = null; @@ -2719,6 +2685,7 @@ public class Workspace extends SmoothPagedView mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView, mDragViewVisualCenter); + final View child = (mDragInfo == null) ? null : mDragInfo.cell; // Identify whether we have dragged over a side page if (isSmall()) { if (mLauncher.getHotseat() != null && !isExternalDragWidget(d)) { @@ -2744,6 +2711,7 @@ public class Workspace extends SmoothPagedView mDragTargetLayout.onDragEnter(); } else { mLastDragOverView = null; + mDragMode = DRAG_MODE_NONE; } boolean isInSpringLoadedMode = (mState == State.SPRING_LOADED); @@ -2779,8 +2747,6 @@ public class Workspace extends SmoothPagedView // Handle the drag over if (mDragTargetLayout != null) { - final View child = (mDragInfo == null) ? null : mDragInfo.cell; - // We want the point to be mapped to the dragTarget. if (mLauncher.isHotseatLayout(mDragTargetLayout)) { mapPointFromSelfToSibling(mLauncher.getHotseat(), mDragViewVisualCenter); @@ -2791,42 +2757,108 @@ public class Workspace extends SmoothPagedView mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], 1, 1, mDragTargetLayout, mTargetCell); + float targetCellDistance = mDragTargetLayout.getDistanceFromCell( + mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell); + final View dragOverView = mDragTargetLayout.getChildAt(mTargetCell[0], mTargetCell[1]); - boolean userFolderPending = willCreateUserFolder(info, mDragTargetLayout, - mTargetCell, false); - boolean isOverFolder = dragOverView instanceof FolderIcon; - if (dragOverView != mLastDragOverView) { - cancelFolderCreation(); - if (mLastDragOverView != null && mLastDragOverView instanceof FolderIcon) { - ((FolderIcon) mLastDragOverView).onDragExit(d.dragInfo); + final View lastDragOverView = mLastDragOverView; + if (mLastDragOverView != dragOverView) { + mDragMode = DRAG_MODE_NONE; + mLastDragOverView = dragOverView; + if (mReorderAlarm != null) { + mReorderAlarm.cancelAlarm(); } } - if (userFolderPending && dragOverView != mLastDragOverView) { - mFolderCreationAlarm.setOnAlarmListener(new - FolderCreationAlarmListener(mDragTargetLayout, mTargetCell[0], mTargetCell[1])); - mFolderCreationAlarm.setAlarm(FOLDER_CREATION_TIMEOUT); - } + boolean folder = willCreateOrAddToFolder(info, mDragTargetLayout, mTargetCell, + targetCellDistance, dragOverView, lastDragOverView); - if (dragOverView != mLastDragOverView && isOverFolder) { - ((FolderIcon) dragOverView).onDragEnter(d.dragInfo); - if (mDragTargetLayout != null) { - mDragTargetLayout.clearDragOutlines(); - } + int minSpanX = item.spanX; + int minSpanY = item.spanY; + if (item.minSpanX > 0 && item.minSpanY > 0) { + minSpanX = item.minSpanX; + minSpanY = item.minSpanY; } - mLastDragOverView = dragOverView; - if (!mCreateUserFolderOnDrop && !isOverFolder) { + int[] reorderPosition = new int[2]; + reorderPosition = findNearestArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], item.spanX, item.spanY, mDragTargetLayout, + reorderPosition); + + if (!mDragTargetLayout.isNearestDropLocationOccupied((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], item.spanX, item.spanY, child, mTargetCell)) { + // If the current hover area isn't occupied (permanently) by any items, then we + // reset all the reordering. + mDragTargetLayout.revertTempState(); + mDragMode = DRAG_MODE_NONE; + mLastDragOverView = dragOverView; + if (mReorderAlarm != null) { + mReorderAlarm.cancelAlarm(); + } + mLastReorderX = -1; + mLastReorderY = -1; mDragTargetLayout.visualizeDropLocation(child, mDragOutline, (int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], - item.spanX, item.spanY, d.dragView.getDragVisualizeOffset(), - d.dragView.getDragRegion()); + mTargetCell[0], mTargetCell[1], item.spanX, item.spanY, false, + d.dragView.getDragVisualizeOffset(), d.dragView.getDragRegion()); + } else if (!folder && !mReorderAlarm.alarmPending() && + (mLastReorderX != reorderPosition[0] || mLastReorderY != reorderPosition[1])) { + // Otherwise, if we aren't adding to or creating a folder and there's no pending + // reorder, then we schedule a reorder + cancelFolderCreation(); + ReorderAlarmListener listener = new ReorderAlarmListener(mDragViewVisualCenter, + minSpanX, minSpanY, item.spanX, item.spanY, d.dragView, child); + mReorderAlarm.setOnAlarmListener(listener); + mReorderAlarm.setAlarm(REORDER_TIMEOUT); + } else if (folder) { + if (mReorderAlarm != null) { + mReorderAlarm.cancelAlarm(); + } + mDragTargetLayout.revertTempState(); + mLastReorderX = -1; + mLastReorderY = -1; } } } + private boolean willCreateOrAddToFolder(ItemInfo info, CellLayout targetLayout, + int[] targetCell, float distance, View dragOverView, View lastDragOverView) { + boolean userFolderPending = willCreateUserFolder(info, targetLayout, targetCell, distance, + false); + + if (userFolderPending && mDragMode == DRAG_MODE_NONE) { + mFolderCreationAlarm.setOnAlarmListener(new + FolderCreationAlarmListener(targetLayout, targetCell[0], targetCell[1])); + mFolderCreationAlarm.setAlarm(FOLDER_CREATION_TIMEOUT); + } + + boolean willAddToFolder = + willAddToExistingUserFolder(info, targetLayout, targetCell, distance); + + if (willAddToFolder && mDragMode == DRAG_MODE_NONE) { + FolderIcon fi = ((FolderIcon) dragOverView); + mDragMode = DRAG_MODE_ADD_TO_FOLDER; + mWillAddToExistingFolder = true; + fi.onDragEnter(info); + if (targetLayout != null) { + targetLayout.clearDragOutlines(); + } + } + + if (dragOverView != lastDragOverView || (mCreateUserFolderOnDrop && !userFolderPending) + || (!willAddToFolder && mDragMode == DRAG_MODE_ADD_TO_FOLDER)) { + cancelFolderCreation(); + mWillAddToExistingFolder = false; + if (lastDragOverView != null && lastDragOverView instanceof FolderIcon) { + ((FolderIcon) lastDragOverView).onDragExit(info); + } + } + + return (willAddToFolder || userFolderPending) && mDragMode != DRAG_MODE_REORDER; + } + private void cleanupFolderCreation(DragObject d) { if (mDragFolderRingAnimator != null && mCreateUserFolderOnDrop) { mDragFolderRingAnimator.animateToNaturalState(); @@ -2868,6 +2900,51 @@ public class Workspace extends SmoothPagedView layout.showFolderAccept(mDragFolderRingAnimator); layout.clearDragOutlines(); mCreateUserFolderOnDrop = true; + mDragMode = DRAG_MODE_CREATE_FOLDER; + } + } + + class ReorderAlarmListener implements OnAlarmListener { + float[] dragViewCenter; + int minSpanX, minSpanY, spanX, spanY; + DragView dragView; + View child; + + public ReorderAlarmListener(float[] dragViewCenter, int minSpanX, int minSpanY, int spanX, + int spanY, DragView dragView, View child) { + this.dragViewCenter = dragViewCenter; + this.minSpanX = minSpanX; + this.minSpanY = minSpanY; + this.spanX = spanX; + this.spanY = spanY; + this.child = child; + this.dragView = dragView; + } + + public void onAlarm(Alarm alarm) { + int[] resultSpan = new int[2]; + mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], spanX, spanY, mDragTargetLayout, mTargetCell); + mLastReorderX = mTargetCell[0]; + mLastReorderY = mTargetCell[1]; + + mTargetCell = mDragTargetLayout.createArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY, + child, mTargetCell, resultSpan, CellLayout.MODE_DRAG_OVER); + + if (mTargetCell[0] < 0 || mTargetCell[1] < 0) { + mDragTargetLayout.revertTempState(); + } + + if (mDragMode == DRAG_MODE_ADD_TO_FOLDER) { + } + mDragMode = DRAG_MODE_REORDER; + + boolean resize = resultSpan[0] != spanX || resultSpan[1] != spanY; + mDragTargetLayout.visualizeDropLocation(child, mDragOutline, + (int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], + mTargetCell[0], mTargetCell[1], resultSpan[0], resultSpan[1], resize, + dragView.getDragVisualizeOffset(), dragView.getDragRegion()); } } @@ -2887,7 +2964,7 @@ public class Workspace extends SmoothPagedView onDropExternal(dragInfo.dropPos, (ItemInfo) dragInfo, (CellLayout) layout, false); return true; } - mLauncher.showOutOfSpaceMessage(); + mLauncher.showOutOfSpaceMessage(mLauncher.isHotseatLayout(layout)); return false; } @@ -2909,7 +2986,7 @@ public class Workspace extends SmoothPagedView final Runnable exitSpringLoadedRunnable = new Runnable() { @Override public void run() { - mLauncher.exitSpringLoadedDragModeDelayed(true, false); + mLauncher.exitSpringLoadedDragModeDelayed(true, false, null); } }; @@ -2937,15 +3014,29 @@ public class Workspace extends SmoothPagedView if (pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) { mTargetCell = findNearestArea((int) touchXY[0], (int) touchXY[1], spanX, spanY, cellLayout, mTargetCell); + float distance = cellLayout.getDistanceFromCell(mDragViewVisualCenter[0], + mDragViewVisualCenter[1], mTargetCell); if (willCreateUserFolder((ItemInfo) d.dragInfo, mDragTargetLayout, mTargetCell, - true) || willAddToExistingUserFolder((ItemInfo) d.dragInfo, - mDragTargetLayout, mTargetCell)) { + distance, true) || willAddToExistingUserFolder((ItemInfo) d.dragInfo, + mDragTargetLayout, mTargetCell, distance)) { findNearestVacantCell = false; } } + + final ItemInfo item = (ItemInfo) d.dragInfo; if (findNearestVacantCell) { - mTargetCell = findNearestVacantArea(touchXY[0], touchXY[1], spanX, spanY, null, - cellLayout, mTargetCell); + int minSpanX = item.spanX; + int minSpanY = item.spanY; + if (item.minSpanX > 0 && item.minSpanY > 0) { + minSpanX = item.minSpanX; + minSpanY = item.minSpanY; + } + int[] resultSpan = new int[2]; + mTargetCell = mDragTargetLayout.createArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], minSpanX, minSpanY, info.spanX, info.spanY, + null, mTargetCell, resultSpan, CellLayout.MODE_ON_DROP_EXTERNAL); + item.spanX = resultSpan[0]; + item.spanY = resultSpan[1]; } Runnable onAnimationCompleteRunnable = new Runnable() { @@ -2955,8 +3046,11 @@ public class Workspace extends SmoothPagedView // widgets/shortcuts/folders in a slightly different way switch (pendingInfo.itemType) { case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: + int span[] = new int[2]; + span[0] = item.spanX; + span[1] = item.spanY; mLauncher.addAppWidgetFromDrop((PendingAddWidgetInfo) pendingInfo, - container, screen, mTargetCell, null); + container, screen, mTargetCell, span, null); break; case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: mLauncher.processShortcutFromDrop(pendingInfo.componentName, @@ -2969,28 +3063,15 @@ public class Workspace extends SmoothPagedView cellLayout.onDragExit(); } }; - - // Now we animate the dragView, (ie. the widget or shortcut preview) into its final - // location and size on the home screen. - RectF r = estimateItemPosition(cellLayout, pendingInfo, - mTargetCell[0], mTargetCell[1], spanX, spanY); - int loc[] = new int[2]; - loc[0] = (int) r.left; - loc[1] = (int) r.top; - setFinalTransitionTransform(cellLayout); - float cellLayoutScale = - mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(cellLayout, loc); - resetTransitionTransform(cellLayout); - - float dragViewScale = Math.min(r.width() / d.dragView.getMeasuredWidth(), - r.height() / d.dragView.getMeasuredHeight()); - // The animation will scale the dragView about its center, so we need to center about - // the final location. - loc[0] -= (d.dragView.getMeasuredWidth() - cellLayoutScale * r.width()) / 2; - loc[1] -= (d.dragView.getMeasuredHeight() - cellLayoutScale * r.height()) / 2; - - mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, loc, - dragViewScale * cellLayoutScale, onAnimationCompleteRunnable); + View finalView = pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET + ? ((PendingAddWidgetInfo) pendingInfo).boundWidget : null; + int animationStyle = ANIMATE_INTO_POSITION_AND_DISAPPEAR; + if (pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET && + ((PendingAddWidgetInfo) pendingInfo).info.configure != null) { + animationStyle = ANIMATE_INTO_POSITION_AND_REMAIN; + } + animateWidgetDrop(info, cellLayout, d.dragView, onAnimationCompleteRunnable, + animationStyle, finalView, true); } else { // This is for other drag/drop cases, like dragging from All Apps View view = null; @@ -3018,20 +3099,24 @@ public class Workspace extends SmoothPagedView if (touchXY != null) { mTargetCell = findNearestArea((int) touchXY[0], (int) touchXY[1], spanX, spanY, cellLayout, mTargetCell); + float distance = cellLayout.getDistanceFromCell(mDragViewVisualCenter[0], + mDragViewVisualCenter[1], mTargetCell); d.postAnimationRunnable = exitSpringLoadedRunnable; - if (createUserFolderIfNecessary(view, container, cellLayout, mTargetCell, true, - d.dragView, d.postAnimationRunnable)) { + if (createUserFolderIfNecessary(view, container, cellLayout, mTargetCell, distance, + true, d.dragView, d.postAnimationRunnable)) { return; } - if (addToExistingFolderIfNecessary(view, cellLayout, mTargetCell, d, true)) { + if (addToExistingFolderIfNecessary(view, cellLayout, mTargetCell, distance, d, + true)) { return; } } if (touchXY != null) { // when dragging and dropping, just find the closest free spot - mTargetCell = findNearestVacantArea(touchXY[0], touchXY[1], 1, 1, null, - cellLayout, mTargetCell); + mTargetCell = mDragTargetLayout.createArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], 1, 1, 1, 1, + null, mTargetCell, null, CellLayout.MODE_ON_DROP_EXTERNAL); } else { cellLayout.findCellForSpan(mTargetCell, 1, 1); } @@ -3039,7 +3124,8 @@ public class Workspace extends SmoothPagedView info.spanY, insertAtFirst); cellLayout.onDropChild(view); CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams(); - cellLayout.getChildrenLayout().measureChild(view); + cellLayout.getShortcutsAndWidgets().measureChild(view); + LauncherModel.addOrMoveItemInDatabase(mLauncher, info, container, screen, lp.cellX, lp.cellY); @@ -3056,6 +3142,109 @@ public class Workspace extends SmoothPagedView } } + public Bitmap createWidgetBitmap(ItemInfo widgetInfo, View layout) { + int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(widgetInfo.spanX, + widgetInfo.spanY, widgetInfo, false); + int visibility = layout.getVisibility(); + layout.setVisibility(VISIBLE); + + int width = MeasureSpec.makeMeasureSpec(unScaledSize[0], MeasureSpec.EXACTLY); + int height = MeasureSpec.makeMeasureSpec(unScaledSize[1], MeasureSpec.EXACTLY); + Bitmap b = Bitmap.createBitmap(unScaledSize[0], unScaledSize[1], + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + + layout.measure(width, height); + layout.layout(0, 0, unScaledSize[0], unScaledSize[1]); + layout.draw(c); + c.setBitmap(null); + layout.setVisibility(visibility); + return b; + } + + private void getFinalPositionForDropAnimation(int[] loc, float[] scaleXY, + DragView dragView, CellLayout layout, ItemInfo info, int[] targetCell, View finalView, + boolean external) { + // Now we animate the dragView, (ie. the widget or shortcut preview) into its final + // location and size on the home screen. + int spanX = info.spanX; + int spanY = info.spanY; + + Rect r = estimateItemPosition(layout, info, targetCell[0], targetCell[1], spanX, spanY); + loc[0] = r.left; + loc[1] = r.top; + + setFinalTransitionTransform(layout); + float cellLayoutScale = + mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(layout, loc); + resetTransitionTransform(layout); + float dragViewScaleX = (1.0f * r.width()) / dragView.getMeasuredWidth(); + float dragViewScaleY = (1.0f * r.height()) / dragView.getMeasuredHeight(); + + // The animation will scale the dragView about its center, so we need to center about + // the final location. + loc[0] -= (dragView.getMeasuredWidth() - cellLayoutScale * r.width()) / 2; + loc[1] -= (dragView.getMeasuredHeight() - cellLayoutScale * r.height()) / 2; + + scaleXY[0] = dragViewScaleX * cellLayoutScale; + scaleXY[1] = dragViewScaleY * cellLayoutScale; + } + + public void animateWidgetDrop(ItemInfo info, CellLayout cellLayout, DragView dragView, + final Runnable onCompleteRunnable, int animationType, final View finalView, + boolean external) { + Rect from = new Rect(); + mLauncher.getDragLayer().getViewRectRelativeToSelf(dragView, from); + + int[] finalPos = new int[2]; + float scaleXY[] = new float[2]; + getFinalPositionForDropAnimation(finalPos, scaleXY, dragView, cellLayout, info, mTargetCell, + finalView, external); + + Resources res = mLauncher.getResources(); + int duration = res.getInteger(R.integer.config_dropAnimMaxDuration) - 200; + + // In the case where we've prebound the widget, we remove it from the DragLayer + if (finalView instanceof AppWidgetHostView && external) { + mLauncher.getDragLayer().removeView(finalView); + } + if ((animationType == ANIMATE_INTO_POSITION_AND_RESIZE || external) && finalView != null) { + Bitmap crossFadeBitmap = createWidgetBitmap(info, finalView); + dragView.setCrossFadeBitmap(crossFadeBitmap); + dragView.crossFade((int) (duration * 0.8f)); + } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET && external) { + scaleXY[0] = scaleXY[1] = Math.min(scaleXY[0], scaleXY[1]); + } + + DragLayer dragLayer = mLauncher.getDragLayer(); + if (animationType == CANCEL_TWO_STAGE_WIDGET_DROP_ANIMATION) { + mLauncher.getDragLayer().animateViewIntoPosition(dragView, finalPos, 0f, 0.1f, 0.1f, + DragLayer.ANIMATION_END_DISAPPEAR, onCompleteRunnable, duration); + } else { + int endStyle; + if (animationType == ANIMATE_INTO_POSITION_AND_REMAIN) { + endStyle = DragLayer.ANIMATION_END_REMAIN_VISIBLE; + } else { + endStyle = DragLayer.ANIMATION_END_DISAPPEAR;; + } + + Runnable onComplete = new Runnable() { + @Override + public void run() { + if (finalView != null) { + finalView.setVisibility(VISIBLE); + } + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } + } + }; + dragLayer.animateViewIntoPosition(dragView, from.left, from.top, finalPos[0], + finalPos[1], 1, 1, 1, scaleXY[0], scaleXY[1], onComplete, endStyle, + duration, this); + } + } + public void setFinalTransitionTransform(CellLayout layout) { if (isSwitchingState()) { int index = indexOfChild(layout); @@ -3109,17 +3298,6 @@ public class Workspace extends SmoothPagedView * * pixelX and pixelY should be in the coordinate system of layout */ - private int[] findNearestVacantArea(int pixelX, int pixelY, - int spanX, int spanY, View ignoreView, CellLayout layout, int[] recycle) { - return layout.findNearestVacantArea( - pixelX, pixelY, spanX, spanY, ignoreView, recycle); - } - - /** - * Calculate the nearest cell where the given object would be dropped. - * - * pixelX and pixelY should be in the coordinate system of layout - */ private int[] findNearestArea(int pixelX, int pixelY, int spanX, int spanY, CellLayout layout, int[] recycle) { return layout.findNearestArea( @@ -3139,7 +3317,8 @@ public class Workspace extends SmoothPagedView /** * Called at the end of a drag which originated on the workspace. */ - public void onDropCompleted(View target, DragObject d, boolean success) { + public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, + boolean success) { if (success) { if (target != this) { if (mDragInfo != null) { @@ -3167,6 +3346,46 @@ public class Workspace extends SmoothPagedView } mDragOutline = null; mDragInfo = null; + + // Hide the scrolling indicator after you pick up an item + hideScrollingIndicator(false); + } + + void updateItemLocationsInDatabase(CellLayout cl) { + int count = cl.getShortcutsAndWidgets().getChildCount(); + + int screen = indexOfChild(cl); + int container = Favorites.CONTAINER_DESKTOP; + + if (mLauncher.isHotseatLayout(cl)) { + screen = -1; + container = Favorites.CONTAINER_HOTSEAT; + } + + for (int i = 0; i < count; i++) { + View v = cl.getShortcutsAndWidgets().getChildAt(i); + ItemInfo info = (ItemInfo) v.getTag(); + // Null check required as the AllApps button doesn't have an item info + if (info != null) { + LauncherModel.moveItemInDatabase(mLauncher, info, container, screen, info.cellX, + info.cellY); + } + } + } + + @Override + public boolean supportsFlingToDelete() { + return true; + } + + @Override + public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { + // Do nothing + } + + @Override + public void onFlingToDeleteCompleted() { + // Do nothing } public boolean isDropEnabled() { @@ -3204,7 +3423,8 @@ public class Workspace extends SmoothPagedView @Override public boolean onEnterScrollArea(int x, int y, int direction) { // Ignore the scroll area if we are dragging over the hot seat - if (mLauncher.getHotseat() != null) { + boolean isPortrait = !LauncherApplication.isScreenLandscape(getContext()); + if (mLauncher.getHotseat() != null && isPortrait) { Rect r = new Rect(); mLauncher.getHotseat().getHitRect(r); if (r.contains(x, y)) { @@ -3216,11 +3436,12 @@ public class Workspace extends SmoothPagedView if (!isSmall() && !mIsSwitchingState) { mInScrollArea = true; - final int page = mCurrentPage + (direction == DragController.SCROLL_LEFT ? -1 : 1); - final CellLayout layout = (CellLayout) getChildAt(page); + final int page = (mNextPage != INVALID_PAGE ? mNextPage : mCurrentPage) + + (direction == DragController.SCROLL_LEFT ? -1 : 1); cancelFolderCreation(); - if (layout != null) { + if (0 <= page && page < getChildCount()) { + CellLayout layout = (CellLayout) getChildAt(page); // Exit the current layout and mark the overlapping layout if (mDragTargetLayout != null) { mDragTargetLayout.setIsDragOverlapping(false); @@ -3243,16 +3464,17 @@ public class Workspace extends SmoothPagedView boolean result = false; if (mInScrollArea) { if (mDragTargetLayout != null) { - // Unmark the overlapping layout and re-enter the current layout mDragTargetLayout.setIsDragOverlapping(false); - mDragTargetLayout = getCurrentDropLayout(); - mDragTargetLayout.onDragEnter(); - // Workspace is responsible for drawing the edge glow on adjacent pages, // so we need to redraw the workspace when this may have changed. invalidate(); - result = true; } + if (mDragTargetLayout != null && mDragHasEnteredWorkspace) { + // Unmark the overlapping layout and re-enter the current layout + mDragTargetLayout = getCurrentDropLayout(); + mDragTargetLayout.onDragEnter(); + } + result = true; mInScrollArea = false; } return result; @@ -3276,7 +3498,7 @@ public class Workspace extends SmoothPagedView CellLayout getParentCellLayoutForView(View v) { ArrayList<CellLayout> layouts = getWorkspaceAndHotseatCellLayouts(); for (CellLayout layout : layouts) { - if (layout.getChildrenLayout().indexOfChild(v) > -1) { + if (layout.getShortcutsAndWidgets().indexOfChild(v) > -1) { return layout; } } @@ -3300,23 +3522,26 @@ public class Workspace extends SmoothPagedView /** * We should only use this to search for specific children. Do not use this method to modify - * CellLayoutChildren directly. + * ShortcutsAndWidgetsContainer directly. Includes ShortcutAndWidgetContainers from + * the hotseat and workspace pages */ - ArrayList<CellLayoutChildren> getWorkspaceAndHotseatCellLayoutChildren() { - ArrayList<CellLayoutChildren> childrenLayouts = new ArrayList<CellLayoutChildren>(); + ArrayList<ShortcutAndWidgetContainer> getAllShortcutAndWidgetContainers() { + ArrayList<ShortcutAndWidgetContainer> childrenLayouts = + new ArrayList<ShortcutAndWidgetContainer>(); int screenCount = getChildCount(); for (int screen = 0; screen < screenCount; screen++) { - childrenLayouts.add(((CellLayout) getChildAt(screen)).getChildrenLayout()); + childrenLayouts.add(((CellLayout) getChildAt(screen)).getShortcutsAndWidgets()); } if (mLauncher.getHotseat() != null) { - childrenLayouts.add(mLauncher.getHotseat().getLayout().getChildrenLayout()); + childrenLayouts.add(mLauncher.getHotseat().getLayout().getShortcutsAndWidgets()); } return childrenLayouts; } public Folder getFolderForTag(Object tag) { - ArrayList<CellLayoutChildren> childrenLayouts = getWorkspaceAndHotseatCellLayoutChildren(); - for (CellLayoutChildren layout: childrenLayouts) { + ArrayList<ShortcutAndWidgetContainer> childrenLayouts = + getAllShortcutAndWidgetContainers(); + for (ShortcutAndWidgetContainer layout: childrenLayouts) { int count = layout.getChildCount(); for (int i = 0; i < count; i++) { View child = layout.getChildAt(i); @@ -3332,8 +3557,9 @@ public class Workspace extends SmoothPagedView } public View getViewForTag(Object tag) { - ArrayList<CellLayoutChildren> childrenLayouts = getWorkspaceAndHotseatCellLayoutChildren(); - for (CellLayoutChildren layout: childrenLayouts) { + ArrayList<ShortcutAndWidgetContainer> childrenLayouts = + getAllShortcutAndWidgetContainers(); + for (ShortcutAndWidgetContainer layout: childrenLayouts) { int count = layout.getChildCount(); for (int i = 0; i < count; i++) { View child = layout.getChildAt(i); @@ -3346,8 +3572,9 @@ public class Workspace extends SmoothPagedView } void clearDropTargets() { - ArrayList<CellLayoutChildren> childrenLayouts = getWorkspaceAndHotseatCellLayoutChildren(); - for (CellLayoutChildren layout: childrenLayouts) { + ArrayList<ShortcutAndWidgetContainer> childrenLayouts = + getAllShortcutAndWidgetContainers(); + for (ShortcutAndWidgetContainer layout: childrenLayouts) { int childCount = layout.getChildCount(); for (int j = 0; j < childCount; j++) { View v = layout.getChildAt(j); @@ -3369,7 +3596,7 @@ public class Workspace extends SmoothPagedView ArrayList<CellLayout> cellLayouts = getWorkspaceAndHotseatCellLayouts(); for (final CellLayout layoutParent: cellLayouts) { - final ViewGroup layout = layoutParent.getChildrenLayout(); + final ViewGroup layout = layoutParent.getShortcutsAndWidgets(); // Avoid ANRs by treating each screen separately post(new Runnable() { @@ -3455,14 +3682,14 @@ public class Workspace extends SmoothPagedView } void updateShortcuts(ArrayList<ApplicationInfo> apps) { - ArrayList<CellLayoutChildren> childrenLayouts = getWorkspaceAndHotseatCellLayoutChildren(); - for (CellLayoutChildren layout: childrenLayouts) { + ArrayList<ShortcutAndWidgetContainer> childrenLayouts = getAllShortcutAndWidgetContainers(); + for (ShortcutAndWidgetContainer layout: childrenLayouts) { int childCount = layout.getChildCount(); for (int j = 0; j < childCount; j++) { final View view = layout.getChildAt(j); Object tag = view.getTag(); if (tag instanceof ShortcutInfo) { - ShortcutInfo info = (ShortcutInfo)tag; + ShortcutInfo info = (ShortcutInfo) tag; // We need to check for ACTION_MAIN otherwise getComponent() might // return null for some shortcuts (for instance, for shortcuts to // web pages.) @@ -3474,11 +3701,11 @@ public class Workspace extends SmoothPagedView for (int k = 0; k < appCount; k++) { ApplicationInfo app = apps.get(k); if (app.componentName.equals(name)) { - info.setIcon(mIconCache.getIcon(info.intent)); - ((TextView)view).setCompoundDrawablesWithIntrinsicBounds(null, - new FastBitmapDrawable(info.getIcon(mIconCache)), - null, null); - } + BubbleTextView shortcut = (BubbleTextView) view; + info.updateIcon(mIconCache); + info.title = app.title.toString(); + shortcut.applyFromShortcutInfo(info, mIconCache); + } } } } @@ -3524,7 +3751,7 @@ public class Workspace extends SmoothPagedView final ViewGroup parent = (ViewGroup) getParent(); final ImageView qsbDivider = (ImageView) (parent.findViewById(R.id.qsb_divider)); final ImageView dockDivider = (ImageView) (parent.findViewById(R.id.dock_divider)); - final ImageView scrollIndicator = getScrollingIndicator(); + final View scrollIndicator = getScrollingIndicator(); cancelScrollingIndicatorAnimations(); if (qsbDivider != null) qsbDivider.setAlpha(reducedFade); |