diff options
Diffstat (limited to 'src')
52 files changed, 10931 insertions, 2517 deletions
diff --git a/src/com/android/launcher2/AllApps2D.java b/src/com/android/launcher2/AllApps2D.java index 7ad5e49..66d9395 100644 --- a/src/com/android/launcher2/AllApps2D.java +++ b/src/com/android/launcher2/AllApps2D.java @@ -16,32 +16,30 @@ package com.android.launcher2; +import com.android.launcher.R; + import android.content.ComponentName; import android.content.Context; import android.content.res.Resources; -import android.graphics.drawable.BitmapDrawable; import android.graphics.Bitmap; -import android.graphics.Color; +import android.graphics.drawable.BitmapDrawable; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; -import android.view.ViewGroup; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.view.animation.AnimationUtils; -import android.view.ViewConfiguration; import android.widget.AdapterView; -import android.widget.ImageButton; -import android.widget.TextView; import android.widget.ArrayAdapter; import android.widget.GridView; +import android.widget.ImageButton; import android.widget.RelativeLayout; +import android.widget.TextView; import java.util.ArrayList; import java.util.Collections; -import com.android.launcher.R; - public class AllApps2D extends RelativeLayout implements AllAppsView, @@ -58,8 +56,16 @@ public class AllApps2D private GridView mGrid; + /** All applications in the system (we might only be showing a subset) */ private ArrayList<ApplicationInfo> mAllAppsList = new ArrayList<ApplicationInfo>(); + /** Currently visible applications in the grid */ + private ArrayList<ApplicationInfo> mVisibleAppsList = new ArrayList<ApplicationInfo>(); + + public enum AppType { APP, GAME, DOWNLOADED, ALL }; + + private AppType mCurrentFilter = AppType.ALL; + // preserve compatibility with 3D all apps: // 0.0 -> hidden // 1.0 -> shown and opaque @@ -120,30 +126,27 @@ public class AllApps2D setVisibility(View.GONE); setSoundEffectsEnabled(false); - mAppsAdapter = new AppsAdapter(getContext(), mAllAppsList); - mAppsAdapter.setNotifyOnChange(false); + mAppsAdapter = new AppsAdapter(getContext(), mVisibleAppsList); } @Override protected void onFinishInflate() { - setBackgroundColor(Color.BLACK); - try { mGrid = (GridView)findViewWithTag("all_apps_2d_grid"); if (mGrid == null) throw new Resources.NotFoundException(); mGrid.setOnItemClickListener(this); mGrid.setOnItemLongClickListener(this); - mGrid.setBackgroundColor(Color.BLACK); - mGrid.setCacheColorHint(Color.BLACK); + // The home button is optional; some layouts might not use it ImageButton homeButton = (ImageButton) findViewWithTag("all_apps_2d_home"); - if (homeButton == null) throw new Resources.NotFoundException(); - homeButton.setOnClickListener( - new View.OnClickListener() { - public void onClick(View v) { - mLauncher.closeAllApps(true); - } - }); + if (homeButton != null) { + homeButton.setOnClickListener( + new View.OnClickListener() { + public void onClick(View v) { + mLauncher.closeAllApps(true); + } + }); + } } catch (Resources.NotFoundException e) { Log.e(TAG, "Can't find necessary layout elements for AllApps2D"); } @@ -198,10 +201,16 @@ public class AllApps2D } } + @Override public void setDragController(DragController dragger) { mDragController = dragger; } + @Override + public void onDragViewVisible() { + } + + @Override public void onDropCompleted(View target, boolean success) { } @@ -250,19 +259,17 @@ public class AllApps2D return mZoom > 0.001f; } - @Override - public boolean isOpaque() { - return mZoom > 0.999f; + public boolean isAnimating() { + return (getAnimation() != null); } public void setApps(ArrayList<ApplicationInfo> list) { mAllAppsList.clear(); addApps(list); + filterApps(mCurrentFilter); } public void addApps(ArrayList<ApplicationInfo> list) { -// Log.d(TAG, "addApps: " + list.size() + " apps: " + list.toString()); - final int N = list.size(); for (int i=0; i<N; i++) { @@ -274,11 +281,12 @@ public class AllApps2D } mAllAppsList.add(index, item); } - mAppsAdapter.notifyDataSetChanged(); + filterApps(mCurrentFilter); } public void removeApps(ArrayList<ApplicationInfo> list) { final int N = list.size(); + for (int i=0; i<N; i++) { final ApplicationInfo item = list.get(i); int index = findAppByComponent(mAllAppsList, item); @@ -289,7 +297,7 @@ public class AllApps2D // Try to recover. This should keep us from crashing for now. } } - mAppsAdapter.notifyDataSetChanged(); + filterApps(mCurrentFilter); } public void updateApps(ArrayList<ApplicationInfo> list) { @@ -298,6 +306,33 @@ public class AllApps2D addApps(list); } + public void filterApps(AppType appType) { + mCurrentFilter = appType; + + mAppsAdapter.setNotifyOnChange(false); + mVisibleAppsList.clear(); + if (appType == AppType.ALL) { + mVisibleAppsList.addAll(mAllAppsList); + } else { + int searchFlags = 0; + + if (appType == AppType.APP) { + searchFlags = ApplicationInfo.APP_FLAG; + } else if (appType == AppType.GAME) { + searchFlags = ApplicationInfo.GAME_FLAG; + } else if (appType == AppType.DOWNLOADED) { + searchFlags = ApplicationInfo.DOWNLOADED_FLAG; + } + + for (ApplicationInfo info : mAllAppsList) { + if ((info.flags & searchFlags) != 0) { + mVisibleAppsList.add(info); + } + } + } + mAppsAdapter.notifyDataSetChanged(); + } + private static int findAppByComponent(ArrayList<ApplicationInfo> list, ApplicationInfo item) { ComponentName component = item.intent.getComponent(); final int N = list.size(); diff --git a/src/com/android/launcher2/AllApps3D.java b/src/com/android/launcher2/AllApps3D.java index 15b98bf..b75d08a 100644 --- a/src/com/android/launcher2/AllApps3D.java +++ b/src/com/android/launcher2/AllApps3D.java @@ -16,6 +16,12 @@ package com.android.launcher2; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; + +import com.android.launcher.R; + import android.content.ComponentName; import android.content.Context; import android.content.res.Resources; @@ -23,19 +29,7 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.PixelFormat; import android.graphics.Rect; -import android.renderscript.Allocation; -import android.renderscript.Element; -import android.renderscript.ProgramFragment; -import android.renderscript.ProgramStore; -import android.renderscript.ProgramVertex; -import android.renderscript.RSSurfaceView; -import android.renderscript.RenderScriptGL; -import android.renderscript.RenderScript; -import android.renderscript.Sampler; -import android.renderscript.Script; -import android.renderscript.ScriptC; -import android.renderscript.SimpleMesh; -import android.renderscript.Type; +import android.renderscript.*; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; @@ -48,12 +42,6 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.accessibility.AccessibilityEvent; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; - -import com.android.launcher.R; - public class AllApps3D extends RSSurfaceView implements AllAppsView, View.OnClickListener, View.OnLongClickListener, DragSource { private static final String TAG = "Launcher.AllApps3D"; @@ -131,27 +119,16 @@ public class AllApps3D extends RSSurfaceView private boolean mSurrendered; private int mRestoreFocusIndex = -1; - + @SuppressWarnings({"UnusedDeclaration"}) static class Defines { - public static final int ALLOC_PARAMS = 0; - public static final int ALLOC_STATE = 1; - public static final int ALLOC_ICON_IDS = 3; - public static final int ALLOC_LABEL_IDS = 4; - public static final int ALLOC_VP_CONSTANTS = 5; - public static final int COLUMNS_PER_PAGE_PORTRAIT = 4; public static final int ROWS_PER_PAGE_PORTRAIT = 4; public static final int COLUMNS_PER_PAGE_LANDSCAPE = 6; public static final int ROWS_PER_PAGE_LANDSCAPE = 3; - public static final int ICON_WIDTH_PX = 64; - public static final int ICON_TEXTURE_WIDTH_PX = 74; public static final int SELECTION_TEXTURE_WIDTH_PX = 74 + 20; - - public static final int ICON_HEIGHT_PX = 64; - public static final int ICON_TEXTURE_HEIGHT_PX = 74; public static final int SELECTION_TEXTURE_HEIGHT_PX = 74 + 20; } @@ -159,7 +136,6 @@ public class AllApps3D extends RSSurfaceView super(context, attrs); setFocusable(true); setSoundEffectsEnabled(false); - getHolder().setFormat(PixelFormat.TRANSLUCENT); final ViewConfiguration config = ViewConfiguration.get(context); mSlop = config.getScaledTouchSlop(); mMaxFlingVelocity = config.getScaledMaximumFlingVelocity(); @@ -170,7 +146,10 @@ public class AllApps3D extends RSSurfaceView getHolder().setFormat(PixelFormat.TRANSLUCENT); if (sRS == null) { - sRS = createRenderScript(true); + RenderScriptGL.SurfaceConfig sc = new RenderScriptGL.SurfaceConfig(); + sc.setDepth(16, 16); + sc.setAlpha(8, 8); + sRS = createRenderScript(sc); } else { createRenderScript(sRS); } @@ -275,26 +254,11 @@ public class AllApps3D extends RSSurfaceView sRollo.dirtyCheck(); sRollo.resize(w, h); + Log.d(TAG, "sc " + sRS); if (sRS != null) { sRS.mMessageCallback = mMessageProc = new AAMessage(); } - if (sRollo.mUniformAlloc != null) { - float tf[] = new float[] {72.f, 72.f, - 120.f, 120.f, 0.f, 0.f, - 120.f, 680.f, - (2.f / 480.f), 0, -((float)w / 2) - 0.25f, -380.25f}; - if (w > h) { - tf[6] = 40.f; - tf[7] = h - 40.f; - tf[9] = 1.f; - tf[10] = -((float)w / 2) - 0.25f; - tf[11] = -((float)h / 2) - 0.25f; - } - - sRollo.mUniformAlloc.data(tf); - } - //long endTime = SystemClock.uptimeMillis(); //Log.d(TAG, "surfaceChanged took " + (endTime-startTime) + "ms"); } @@ -308,18 +272,17 @@ public class AllApps3D extends RSSurfaceView if (mArrowNavigation) { if (!hasWindowFocus) { // Clear selection when we lose window focus - mLastSelectedIcon = sRollo.mState.selectedIconIndex; + mLastSelectedIcon = sRollo.mScript.get_gSelectedIconIndex(); sRollo.setHomeSelected(SELECTED_NONE); sRollo.clearSelectedIcon(); - sRollo.mState.save(); } else { - if (sRollo.mState.iconCount > 0) { + if (sRollo.mScript.get_gIconCount() > 0) { if (mLastSelection == SELECTION_ICONS) { int selection = mLastSelectedIcon; final int firstIcon = Math.round(sRollo.mScrollPos) * mColumnsPerPage; if (selection < 0 || // No selection selection < firstIcon || // off the top of the screen - selection >= sRollo.mState.iconCount || // past last icon + selection >= sRollo.mScript.get_gIconCount() || // past last icon selection >= firstIcon + // past last icon on screen (mColumnsPerPage * mRowsPerPage)) { selection = firstIcon; @@ -327,10 +290,8 @@ public class AllApps3D extends RSSurfaceView // Select the first icon when we gain window focus sRollo.selectIcon(selection, SELECTED_FOCUSED); - sRollo.mState.save(); } else if (mLastSelection == SELECTION_HOME) { sRollo.setHomeSelected(SELECTED_FOCUSED); - sRollo.mState.save(); } } } @@ -357,7 +318,6 @@ public class AllApps3D extends RSSurfaceView // Clear selection when we lose focus sRollo.clearSelectedIcon(); sRollo.setHomeSelected(SELECTED_NONE); - sRollo.mState.save(); mArrowNavigation = false; } } else { @@ -367,11 +327,10 @@ public class AllApps3D extends RSSurfaceView } private void gainFocus() { - if (!mArrowNavigation && sRollo.mState.iconCount > 0) { + if (!mArrowNavigation && sRollo.mScript.get_gIconCount() > 0) { // Select the first icon when we gain keyboard focus mArrowNavigation = true; sRollo.selectIcon(Math.round(sRollo.mScrollPos) * mColumnsPerPage, SELECTED_FOCUSED); - sRollo.mState.save(); } } @@ -383,7 +342,7 @@ public class AllApps3D extends RSSurfaceView if (!isVisible()) { return false; } - final int iconCount = sRollo.mState.iconCount; + final int iconCount = sRollo.mScript.get_gIconCount(); if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) { if (mArrowNavigation) { @@ -391,7 +350,7 @@ public class AllApps3D extends RSSurfaceView reallyPlaySoundEffect(SoundEffectConstants.CLICK); mLauncher.closeAllApps(true); } else { - int whichApp = sRollo.mState.selectedIconIndex; + int whichApp = sRollo.mScript.get_gSelectedIconIndex(); if (whichApp >= 0) { ApplicationInfo app = mAllAppsList.get(whichApp); mLauncher.startActivitySafely(app.intent, app); @@ -403,10 +362,10 @@ public class AllApps3D extends RSSurfaceView if (iconCount > 0) { final boolean isPortrait = getWidth() < getHeight(); - + mArrowNavigation = true; - int currentSelection = sRollo.mState.selectedIconIndex; + int currentSelection = sRollo.mScript.get_gSelectedIconIndex(); int currentTopRow = Math.round(sRollo.mScrollPos); // The column of the current selection, in the range 0..COLUMNS_PER_PAGE_PORTRAIT-1 @@ -512,7 +471,6 @@ public class AllApps3D extends RSSurfaceView } if (newSelection != currentSelection) { sRollo.selectIcon(newSelection, SELECTED_FOCUSED); - sRollo.mState.save(); } } return handled; @@ -612,7 +570,6 @@ public class AllApps3D extends RSSurfaceView (!isPortrait && x > mTouchXBorders[mTouchXBorders.length-1])) { mTouchTracking = TRACKING_HOME; sRollo.setHomeSelected(SELECTED_PRESSED); - sRollo.mState.save(); mCurrentIconIndex = -1; } else { mTouchTracking = TRACKING_FLING; @@ -620,9 +577,6 @@ public class AllApps3D extends RSSurfaceView mMotionDownRawX = (int)ev.getRawX(); mMotionDownRawY = (int)ev.getRawY(); - sRollo.mState.newPositionX = ev.getRawY() / getHeight(); - sRollo.mState.newTouchDown = 1; - if (!sRollo.checkClickOK()) { sRollo.clearSelectedIcon(); } else { @@ -633,8 +587,7 @@ public class AllApps3D extends RSSurfaceView cancelLongPress(); } } - sRollo.mState.save(); - sRollo.move(); + sRollo.move(ev.getRawY() / getHeight()); mVelocityTracker = VelocityTracker.obtain(); mVelocityTracker.addMovement(ev); mStartedScrolling = false; @@ -647,7 +600,6 @@ public class AllApps3D extends RSSurfaceView y > mTouchYBorders[mTouchYBorders.length-1]) || (!isPortrait && x > mTouchXBorders[mTouchXBorders.length-1]) ? SELECTED_PRESSED : SELECTED_NONE); - sRollo.mState.save(); } else if (mTouchTracking == TRACKING_FLING) { int rawY = (int)ev.getRawY(); int slop; @@ -668,14 +620,11 @@ public class AllApps3D extends RSSurfaceView cancelLongPress(); mCurrentIconIndex = -1; } - sRollo.mState.newPositionX = ev.getRawY() / getHeight(); - sRollo.mState.newTouchDown = 1; - sRollo.move(); + sRollo.move(ev.getRawY() / getHeight()); mStartedScrolling = true; sRollo.clearSelectedIcon(); mVelocityTracker.addMovement(ev); - sRollo.mState.save(); } } break; @@ -689,18 +638,13 @@ public class AllApps3D extends RSSurfaceView mLauncher.closeAllApps(true); } sRollo.setHomeSelected(SELECTED_NONE); - sRollo.mState.save(); } mCurrentIconIndex = -1; } else if (mTouchTracking == TRACKING_FLING) { - sRollo.mState.newTouchDown = 0; - sRollo.mState.newPositionX = ev.getRawY() / getHeight(); - mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, mMaxFlingVelocity); - sRollo.mState.flingVelocity = mVelocityTracker.getYVelocity() / getHeight(); sRollo.clearSelectedIcon(); - sRollo.mState.save(); - sRollo.fling(); + sRollo.fling(ev.getRawY() / getHeight(), + mVelocityTracker.getYVelocity() / getHeight()); if (mVelocityTracker != null) { mVelocityTracker.recycle(); @@ -767,7 +711,7 @@ public class AllApps3D extends RSSurfaceView int pos = -1; switch (mLastSelection) { case SELECTION_ICONS: - index = sRollo.mState.selectedIconIndex; + index = sRollo.mScript.get_gSelectedIconIndex(); if (index >= 0) { ApplicationInfo info = mAllAppsList.get(index); if (info.title != null) { @@ -792,10 +736,16 @@ public class AllApps3D extends RSSurfaceView return false; } + @Override public void setDragController(DragController dragger) { mDragController = dragger; } + @Override + public void onDragViewVisible() { + } + + @Override public void onDropCompleted(View target, boolean success) { } @@ -826,8 +776,8 @@ public class AllApps3D extends RSSurfaceView return sRollo != null && mZoom > 0.001f; } - public boolean isOpaque() { - return mZoom > 0.999f; + public boolean isAnimating() { + return isVisible() && mZoom <= 0.999f; } public void setApps(ArrayList<ApplicationInfo> list) { @@ -859,13 +809,12 @@ public class AllApps3D extends RSSurfaceView if (sRollo != null && reload) { sRollo.setApps(list); } - + if (hasFocus() && mRestoreFocusIndex != -1) { sRollo.selectIcon(mRestoreFocusIndex, SELECTED_FOCUSED); - sRollo.mState.save(); mRestoreFocusIndex = -1; } - + mLocks &= ~LOCK_ICONS_PENDING; } @@ -882,7 +831,7 @@ public class AllApps3D extends RSSurfaceView final int N = list.size(); if (sRollo != null) { sRollo.pause(); - sRollo.reallocAppsList(sRollo.mState.iconCount + N); + sRollo.reallocAppsList(sRollo.mScript.get_gIconCount() + N); } for (int i=0; i<N; i++) { @@ -952,17 +901,6 @@ public class AllApps3D extends RSSurfaceView return -1; } - /* - private static int countPages(int iconCount) { - int iconsPerPage = getColumnsCount() * Defines.ROWS_PER_PAGE_PORTRAIT; - int pages = iconCount / iconsPerPage; - if (pages*iconsPerPage != iconCount) { - pages++; - } - return pages; - } - */ - class AAMessage extends RenderScript.RSMessage { public void run() { sRollo.mScrollPos = ((float)mData[0]) / (1 << 16); @@ -994,23 +932,12 @@ public class AllApps3D extends RSSurfaceView private int mHeight; private Resources mRes; - private Script mScript; - private Script.Invokable mInvokeMove; - private Script.Invokable mInvokeMoveTo; - private Script.Invokable mInvokeFling; - private Script.Invokable mInvokeResetWAR; - private Script.Invokable mInvokeSetZoom; - - private ProgramStore mPSIcons; - private ProgramFragment mPFTexMip; - private ProgramFragment mPFTexMipAlpha; - private ProgramFragment mPFTexNearest; - private ProgramVertex mPV; - private ProgramVertex mPVCurve; - private SimpleMesh mMesh; + ScriptC_allapps mScript; + + private Mesh mMesh; private ProgramVertex.MatrixAllocation mPVA; - private Allocation mUniformAlloc; + private ScriptField_VpConsts mUniformAlloc; private Allocation mHomeButtonNormal; private Allocation mHomeButtonFocused; @@ -1023,28 +950,15 @@ public class AllApps3D extends RSSurfaceView private Allocation[] mLabels; private int[] mLabelIds; private Allocation mAllocLabelIds; - private Allocation mSelectedIcon; private Bitmap mSelectionBitmap; private Canvas mSelectionCanvas; - - private float mScrollPos; - Params mParams; - State mState; + private float mScrollPos; AllApps3D mAllApps; boolean mInitialize; - class BaseAlloc { - Allocation mAlloc; - Type mType; - - void save() { - mAlloc.data(this); - } - } - private boolean checkClickOK() { return (Math.abs(mAllApps.mVelocity) < 0.4f) && (Math.abs(mScrollPos - Math.round(mScrollPos)) < 0.4f); @@ -1062,41 +976,6 @@ public class AllApps3D extends RSSurfaceView } } - class Params extends BaseAlloc { - Params() { - mType = Type.createFromClass(sRS, Params.class, 1, "ParamsClass"); - mAlloc = Allocation.createTyped(sRS, mType); - save(); - } - public int bubbleWidth; - public int bubbleHeight; - public int bubbleBitmapWidth; - public int bubbleBitmapHeight; - - public int homeButtonWidth; - public int homeButtonHeight; - public int homeButtonTextureWidth; - public int homeButtonTextureHeight; - } - - class State extends BaseAlloc { - public float newPositionX; - public int newTouchDown; - public float flingVelocity; - public int iconCount; - public int selectedIconIndex = -1; - public int selectedIconTexture; - public float zoomTarget; - public int homeButtonId; - public float targetPos; - - State() { - mType = Type.createFromClass(sRS, State.class, 1, "StateClass"); - mAlloc = Allocation.createTyped(sRS, mType); - save(); - } - } - public RolloRS(AllApps3D allApps) { mAllApps = allApps; } @@ -1105,16 +984,21 @@ public class AllApps3D extends RSSurfaceView mRes = res; mWidth = width; mHeight = height; + mScript = new ScriptC_allapps(sRS, mRes, R.raw.allapps, true); + initProgramVertex(); initProgramFragment(); initProgramStore(); initGl(); initData(); - initRs(); + + mScript.bind_gIconIDs(mAllocIconIds); + mScript.bind_gLabelIDs(mAllocLabelIds); + sRS.contextBindRootScript(mScript); } public void initMesh() { - SimpleMesh.TriangleMeshBuilder tm = new SimpleMesh.TriangleMeshBuilder(sRS, 2, 0); + Mesh.TriangleMeshBuilder tm = new Mesh.TriangleMeshBuilder(sRS, 2, 0); for (int ct=0; ct < 16; ct++) { float pos = (1.f / (16.f - 1)) * ct; @@ -1125,12 +1009,55 @@ public class AllApps3D extends RSSurfaceView tm.addTriangle(ct, ct+1, ct+2); tm.addTriangle(ct+1, ct+3, ct+2); } - mMesh = tm.create(); - mMesh.setName("SMCell"); + mMesh = tm.create(true); + mScript.set_gSMCell(mMesh); + } + + Matrix4f getProjectionMatrix(int w, int h) { + // range -1,1 in the narrow axis at z = 0. + Matrix4f m1 = new Matrix4f(); + Matrix4f m2 = new Matrix4f(); + + if(w > h) { + float aspect = ((float)w) / h; + m1.loadFrustum(-aspect,aspect, -1,1, 1,100); + } else { + float aspect = ((float)h) / w; + m1.loadFrustum(-1,1, -aspect,aspect, 1,100); + } + + m2.loadRotate(180, 0, 1, 0); + m1.loadMultiply(m1, m2); + + m2.loadScale(-2, 2, 1); + m1.loadMultiply(m1, m2); + + m2.loadTranslate(0, 0, 2); + m1.loadMultiply(m1, m2); + return m1; } void resize(int w, int h) { - mPVA.setupProjectionNormalized(w, h); + Matrix4f proj = getProjectionMatrix(w, h); + mPVA.loadProjection(proj); + + if (mUniformAlloc != null) { + ScriptField_VpConsts.Item i = new ScriptField_VpConsts.Item(); + i.Proj = proj; + i.ScaleOffset.x = (2.f / 480.f); + i.ScaleOffset.y = 0; + i.ScaleOffset.z = -((float)w / 2) - 0.25f; + i.ScaleOffset.w = -380.25f; + i.BendPos.x = 120.f; + i.BendPos.y = 680.f; + if (w > h) { + i.ScaleOffset.z = 40.f; + i.ScaleOffset.w = h - 40.f; + i.BendPos.y = 1.f; + } + mUniformAlloc.set(i, 0, true); + } + mWidth = w; mHeight = h; } @@ -1141,22 +1068,18 @@ public class AllApps3D extends RSSurfaceView ProgramVertex.Builder pvb = new ProgramVertex.Builder(sRS, null, null); pvb.setTextureMatrixEnable(true); - mPV = pvb.create(); - mPV.setName("PV"); - mPV.bindAllocation(mPVA); - - Element.Builder eb = new Element.Builder(sRS); - eb.add(Element.createVector(sRS, Element.DataType.FLOAT_32, 2), "ImgSize"); - eb.add(Element.createVector(sRS, Element.DataType.FLOAT_32, 4), "Position"); - eb.add(Element.createVector(sRS, Element.DataType.FLOAT_32, 2), "BendPos"); - eb.add(Element.createVector(sRS, Element.DataType.FLOAT_32, 4), "ScaleOffset"); - Element e = eb.create(); + ProgramVertex pv = pvb.create(); + pv.bindAllocation(mPVA); + sRS.contextBindProgramVertex(pv); - mUniformAlloc = Allocation.createSized(sRS, e, 1); + mUniformAlloc = new ScriptField_VpConsts(sRS, 1); + mScript.bind_vpConstants(mUniformAlloc); initMesh(); ProgramVertex.ShaderBuilder sb = new ProgramVertex.ShaderBuilder(sRS); - String t = "void main() {\n" + + String t = "varying vec4 varColor;\n" + + "varying vec2 varTex0;\n" + + "void main() {\n" + // Animation " float ani = UNI_Position.z;\n" + @@ -1206,21 +1129,18 @@ public class AllApps3D extends RSSurfaceView " pos.z -= ani * 1.5;\n" + " lum *= 1.0 - ani;\n" + - " gl_Position = UNI_MVP * pos;\n" + + " gl_Position = UNI_Proj * pos;\n" + " varColor.rgba = vec4(lum, lum, lum, 1.0);\n" + " varTex0.xy = ATTRIB_position;\n" + " varTex0.y = 1.0 - varTex0.y;\n" + - " varTex0.zw = vec2(0.0, 0.0);\n" + "}\n"; sb.setShader(t); sb.addConstant(mUniformAlloc.getType()); - sb.addInput(mMesh.getVertexType(0).getElement()); - mPVCurve = sb.create(); - mPVCurve.setName("PVCurve"); - mPVCurve.bindAllocation(mPVA); - mPVCurve.bindConstants(mUniformAlloc, 1); + sb.addInput(mMesh.getVertexAllocation(0).getType().getElement()); + ProgramVertex pvc = sb.create(); + pvc.bindConstants(mUniformAlloc.getAllocation(), 0); - sRS.contextBindProgramVertex(mPV); + mScript.set_gPVCurve(pvc); } private void initProgramFragment() { @@ -1238,20 +1158,23 @@ public class AllApps3D extends RSSurfaceView ProgramFragment.Builder bf = new ProgramFragment.Builder(sRS); bf.setTexture(ProgramFragment.Builder.EnvMode.MODULATE, ProgramFragment.Builder.Format.RGBA, 0); - mPFTexMip = bf.create(); - mPFTexMip.setName("PFTexMip"); - mPFTexMip.bindSampler(linear, 0); + bf.setVaryingColor(true); + ProgramFragment pfTexMip = bf.create(); + pfTexMip.bindSampler(linear, 0); - mPFTexNearest = bf.create(); - mPFTexNearest.setName("PFTexNearest"); - mPFTexNearest.bindSampler(nearest, 0); + bf.setVaryingColor(false); + ProgramFragment pfTexNearest = bf.create(); + pfTexNearest.bindSampler(nearest, 0); bf.setTexture(ProgramFragment.Builder.EnvMode.MODULATE, ProgramFragment.Builder.Format.ALPHA, 0); - mPFTexMipAlpha = bf.create(); - mPFTexMipAlpha.setName("PFTexMipAlpha"); - mPFTexMipAlpha.bindSampler(linear, 0); + bf.setVaryingColor(true); + ProgramFragment pfTexMipAlpha = bf.create(); + pfTexMipAlpha.bindSampler(linear, 0); + mScript.set_gPFTexNearest(pfTexNearest); + mScript.set_gPFTexMip(pfTexMip); + mScript.set_gPFTexMipAlpha(pfTexMipAlpha); } private void initProgramStore() { @@ -1261,23 +1184,17 @@ public class AllApps3D extends RSSurfaceView bs.setDitherEnable(true); bs.setBlendFunc(ProgramStore.BlendSrcFunc.SRC_ALPHA, ProgramStore.BlendDstFunc.ONE_MINUS_SRC_ALPHA); - mPSIcons = bs.create(); - mPSIcons.setName("PSIcons"); + mScript.set_gPS(bs.create()); } private void initGl() { } private void initData() { - mParams = new Params(); - mState = new State(); - - final Utilities.BubbleText bubble = new Utilities.BubbleText(mAllApps.getContext()); - - mParams.bubbleWidth = bubble.getBubbleWidth(); - mParams.bubbleHeight = bubble.getMaxBubbleHeight(); - mParams.bubbleBitmapWidth = bubble.getBitmapWidth(); - mParams.bubbleBitmapHeight = bubble.getBitmapHeight(); + mScript.set_COLUMNS_PER_PAGE_PORTRAIT(Defines.COLUMNS_PER_PAGE_PORTRAIT); + mScript.set_ROWS_PER_PAGE_PORTRAIT(Defines.ROWS_PER_PAGE_PORTRAIT); + mScript.set_COLUMNS_PER_PAGE_LANDSCAPE(Defines.COLUMNS_PER_PAGE_LANDSCAPE); + mScript.set_ROWS_PER_PAGE_LANDSCAPE(Defines.ROWS_PER_PAGE_LANDSCAPE); mHomeButtonNormal = Allocation.createFromBitmapResource(sRS, mRes, R.drawable.home_button_normal, Element.RGBA_8888(sRS), false); @@ -1288,15 +1205,8 @@ public class AllApps3D extends RSSurfaceView mHomeButtonPressed = Allocation.createFromBitmapResource(sRS, mRes, R.drawable.home_button_pressed, Element.RGBA_8888(sRS), false); mHomeButtonPressed.uploadToTexture(0); - mParams.homeButtonWidth = 76; - mParams.homeButtonHeight = 68; - mParams.homeButtonTextureWidth = 128; - mParams.homeButtonTextureHeight = 128; - - mState.homeButtonId = mHomeButtonNormal.getID(); - mParams.save(); - mState.save(); + mScript.set_gHomeButton(mHomeButtonNormal); mSelectionBitmap = Bitmap.createBitmap(Defines.SELECTION_TEXTURE_WIDTH_PX, Defines.SELECTION_TEXTURE_HEIGHT_PX, Bitmap.Config.ARGB_8888); @@ -1305,30 +1215,6 @@ public class AllApps3D extends RSSurfaceView setApps(null); } - private void initRs() { - ScriptC.Builder sb = new ScriptC.Builder(sRS); - sb.setScript(mRes, R.raw.allapps); - sb.setRoot(true); - sb.addDefines(mAllApps.mDefines); - sb.setType(mParams.mType, "params", Defines.ALLOC_PARAMS); - sb.setType(mState.mType, "state", Defines.ALLOC_STATE); - sb.setType(mUniformAlloc.getType(), "vpConstants", Defines.ALLOC_VP_CONSTANTS); - mInvokeMove = sb.addInvokable("move"); - mInvokeFling = sb.addInvokable("fling"); - mInvokeMoveTo = sb.addInvokable("moveTo"); - mInvokeResetWAR = sb.addInvokable("resetHWWar"); - mInvokeSetZoom = sb.addInvokable("setZoom"); - mScript = sb.create(); - mScript.setClearColor(0.0f, 0.0f, 0.0f, 0.0f); - mScript.bindAllocation(mParams.mAlloc, Defines.ALLOC_PARAMS); - mScript.bindAllocation(mState.mAlloc, Defines.ALLOC_STATE); - mScript.bindAllocation(mAllocIconIds, Defines.ALLOC_ICON_IDS); - mScript.bindAllocation(mAllocLabelIds, Defines.ALLOC_LABEL_IDS); - mScript.bindAllocation(mUniformAlloc, Defines.ALLOC_VP_CONSTANTS); - - sRS.contextBindRootScript(mScript); - } - void dirtyCheck() { if (sZoomDirty) { setZoom(mAllApps.sNextZoom, mAllApps.sAnimateNextZoom); @@ -1346,20 +1232,21 @@ public class AllApps3D extends RSSurfaceView mIcons = new Allocation[count]; mIconIds = new int[allocCount]; - mAllocIconIds = Allocation.createSized(sRS, Element.USER_I32(sRS), allocCount); + mAllocIconIds = Allocation.createSized(sRS, Element.I32(sRS), allocCount); mLabels = new Allocation[count]; mLabelIds = new int[allocCount]; - mAllocLabelIds = Allocation.createSized(sRS, Element.USER_I32(sRS), allocCount); + mAllocLabelIds = Allocation.createSized(sRS, Element.I32(sRS), allocCount); - mState.iconCount = count; - for (int i=0; i < mState.iconCount; i++) { + mScript.set_gIconCount(count); + for (int i=0; i < count; i++) { createAppIconAllocations(i, list.get(i)); } - for (int i=0; i < mState.iconCount; i++) { + for (int i=0; i < count; i++) { uploadAppIcon(i, list.get(i)); } saveAppsList(); + android.util.Log.e("rs", "setApps"); sRollo.resume(); } @@ -1368,15 +1255,7 @@ public class AllApps3D extends RSSurfaceView sRollo.clearSelectedIcon(); sRollo.setHomeSelected(SELECTED_NONE); } - if (zoom > 0.001f) { - sRollo.mState.zoomTarget = zoom; - } else { - sRollo.mState.zoomTarget = 0; - } - sRollo.mState.save(); - if (!animate) { - sRollo.mInvokeSetZoom.execute(); - } + sRollo.mScript.invoke_setZoom(zoom, animate ? 1 : 0); } private void createAppIconAllocations(int index, ApplicationInfo item) { @@ -1406,13 +1285,13 @@ public class AllApps3D extends RSSurfaceView private void reallocAppsList(int count) { Allocation[] icons = new Allocation[count]; int[] iconIds = new int[count]; - mAllocIconIds = Allocation.createSized(sRS, Element.USER_I32(sRS), count); + mAllocIconIds = Allocation.createSized(sRS, Element.I32(sRS), count); Allocation[] labels = new Allocation[count]; int[] labelIds = new int[count]; - mAllocLabelIds = Allocation.createSized(sRS, Element.USER_I32(sRS), count); + mAllocLabelIds = Allocation.createSized(sRS, Element.I32(sRS), count); - final int oldCount = sRollo.mState.iconCount; + final int oldCount = sRollo.mScript.get_gIconCount(); System.arraycopy(mIcons, 0, icons, 0, oldCount); System.arraycopy(mIconIds, 0, iconIds, 0, oldCount); @@ -1429,7 +1308,7 @@ public class AllApps3D extends RSSurfaceView * Handle the allocations for the new app. Make sure you call saveAppsList when done. */ private void addApp(int index, ApplicationInfo item) { - final int count = mState.iconCount - index; + final int count = mScript.get_gIconCount() - index; final int dest = index + 1; System.arraycopy(mIcons, index, mIcons, dest, count); @@ -1439,14 +1318,15 @@ public class AllApps3D extends RSSurfaceView createAppIconAllocations(index, item); uploadAppIcon(index, item); - sRollo.mState.iconCount++; + + mScript.set_gIconCount(mScript.get_gIconCount() + 1); } /** * Handle the allocations for the removed app. Make sure you call saveAppsList when done. */ private void removeApp(int index) { - final int count = mState.iconCount - index - 1; + final int count = mScript.get_gIconCount() - index - 1; final int src = index + 1; System.arraycopy(mIcons, src, mIcons, index, count); @@ -1454,8 +1334,8 @@ public class AllApps3D extends RSSurfaceView System.arraycopy(mLabels, src, mLabels, index, count); System.arraycopy(mLabelIds, src, mLabelIds, index, count); - sRollo.mState.iconCount--; - final int last = mState.iconCount; + mScript.set_gIconCount(mScript.get_gIconCount() - 1); + final int last = mScript.get_gIconCount(); mIcons[last] = null; mIconIds[last] = 0; @@ -1472,31 +1352,21 @@ public class AllApps3D extends RSSurfaceView mAllocIconIds.data(mIconIds); mAllocLabelIds.data(mLabelIds); - mScript.bindAllocation(mAllocIconIds, Defines.ALLOC_ICON_IDS); - mScript.bindAllocation(mAllocLabelIds, Defines.ALLOC_LABEL_IDS); - - mState.save(); - - // Note: mScript may be null if we haven't initialized it yet. - // In that case, this is a no-op. - if (mInvokeResetWAR != null) { - mInvokeResetWAR.execute(); - } + mScript.bind_gIconIDs(mAllocIconIds); + mScript.bind_gLabelIDs(mAllocLabelIds); } } - void fling() { - mInvokeFling.execute(); + void fling(float pos, float v) { + mScript.invoke_fling(pos, v); } - void move() { - mInvokeMove.execute(); + void move(float pos) { + mScript.invoke_move(pos); } void moveTo(float row) { - mState.targetPos = row; - mState.save(); - mInvokeMoveTo.execute(); + mScript.invoke_moveTo(row); } /** @@ -1526,7 +1396,7 @@ public class AllApps3D extends RSSurfaceView if (mAllApps != null) { mAllApps.mRestoreFocusIndex = index; } - mState.selectedIconIndex = -1; + mScript.set_gSelectedIconIndex(-1); if (mAllApps.mLastSelection == SELECTION_ICONS) { mAllApps.mLastSelection = SELECTION_NONE; } @@ -1535,8 +1405,8 @@ public class AllApps3D extends RSSurfaceView mAllApps.mLastSelection = SELECTION_ICONS; } - int prev = mState.selectedIconIndex; - mState.selectedIconIndex = index; + int prev = mScript.get_gSelectedIconIndex(); + mScript.set_gSelectedIconIndex(index); ApplicationInfo info = appsList.get(index); Bitmap selectionBitmap = mSelectionBitmap; @@ -1545,10 +1415,10 @@ public class AllApps3D extends RSSurfaceView selectionBitmap.getWidth(), selectionBitmap.getHeight(), pressed == SELECTED_PRESSED, info.iconBitmap); - mSelectedIcon = Allocation.createFromBitmap(sRS, selectionBitmap, + Allocation si = Allocation.createFromBitmap(sRS, selectionBitmap, Element.RGBA_8888(sRS), false); - mSelectedIcon.uploadToTexture(0); - mState.selectedIconTexture = mSelectedIcon.getID(); + si.uploadToTexture(0); + mScript.set_gSelectedIconTexture(si); if (prev != index) { if (info.title != null && info.title.length() > 0) { @@ -1563,24 +1433,24 @@ public class AllApps3D extends RSSurfaceView * You need to call save() on mState on your own after calling this. */ void clearSelectedIcon() { - mState.selectedIconIndex = -1; + mScript.set_gSelectedIconIndex(-1); } void setHomeSelected(int mode) { final int prev = mAllApps.mLastSelection; switch (mode) { case SELECTED_NONE: - mState.homeButtonId = mHomeButtonNormal.getID(); + mScript.set_gHomeButton(mHomeButtonNormal); break; case SELECTED_FOCUSED: mAllApps.mLastSelection = SELECTION_HOME; - mState.homeButtonId = mHomeButtonFocused.getID(); + mScript.set_gHomeButton(mHomeButtonFocused); if (prev != SELECTION_HOME) { mAllApps.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); } break; case SELECTED_PRESSED: - mState.homeButtonId = mHomeButtonPressed.getID(); + mScript.set_gHomeButton(mHomeButtonPressed); break; } } @@ -1600,23 +1470,15 @@ public class AllApps3D extends RSSurfaceView Log.d(TAG, "sRollo.mLabelIds.length=" + mLabelIds.length); } Log.d(TAG, "sRollo.mLabelIds=" + Arrays.toString(mLabelIds)); - Log.d(TAG, "sRollo.mState.newPositionX=" + mState.newPositionX); - Log.d(TAG, "sRollo.mState.newTouchDown=" + mState.newTouchDown); - Log.d(TAG, "sRollo.mState.flingVelocity=" + mState.flingVelocity); - Log.d(TAG, "sRollo.mState.iconCount=" + mState.iconCount); - Log.d(TAG, "sRollo.mState.selectedIconIndex=" + mState.selectedIconIndex); - Log.d(TAG, "sRollo.mState.selectedIconTexture=" + mState.selectedIconTexture); - Log.d(TAG, "sRollo.mState.zoomTarget=" + mState.zoomTarget); - Log.d(TAG, "sRollo.mState.homeButtonId=" + mState.homeButtonId); - Log.d(TAG, "sRollo.mState.targetPos=" + mState.targetPos); - Log.d(TAG, "sRollo.mParams.bubbleWidth=" + mParams.bubbleWidth); - Log.d(TAG, "sRollo.mParams.bubbleHeight=" + mParams.bubbleHeight); - Log.d(TAG, "sRollo.mParams.bubbleBitmapWidth=" + mParams.bubbleBitmapWidth); - Log.d(TAG, "sRollo.mParams.bubbleBitmapHeight=" + mParams.bubbleBitmapHeight); - Log.d(TAG, "sRollo.mParams.homeButtonWidth=" + mParams.homeButtonWidth); - Log.d(TAG, "sRollo.mParams.homeButtonHeight=" + mParams.homeButtonHeight); - Log.d(TAG, "sRollo.mParams.homeButtonTextureWidth=" + mParams.homeButtonTextureWidth); - Log.d(TAG, "sRollo.mParams.homeButtonTextureHeight=" + mParams.homeButtonTextureHeight); + //Log.d(TAG, "sRollo.mState.newPositionX=" + mState.newPositionX); + //Log.d(TAG, "sRollo.mState.newTouchDown=" + mState.newTouchDown); + //Log.d(TAG, "sRollo.mState.flingVelocity=" + mState.flingVelocity); + //Log.d(TAG, "sRollo.mState.iconCount=" + mState.iconCount); + //Log.d(TAG, "sRollo.mState.selectedIconIndex=" + mState.selectedIconIndex); + //Log.d(TAG, "sRollo.mState.selectedIconTexture=" + mState.selectedIconTexture); + //Log.d(TAG, "sRollo.mState.zoomTarget=" + mState.zoomTarget); + //Log.d(TAG, "sRollo.mState.homeButtonId=" + mState.homeButtonId); + //Log.d(TAG, "sRollo.mState.targetPos=" + mState.targetPos); } } @@ -1647,5 +1509,3 @@ public class AllApps3D extends RSSurfaceView } } } - - diff --git a/src/com/android/launcher2/AllAppsList.java b/src/com/android/launcher2/AllAppsList.java index 3a5baea..4c9bc5e 100644 --- a/src/com/android/launcher2/AllAppsList.java +++ b/src/com/android/launcher2/AllAppsList.java @@ -16,17 +16,16 @@ package com.android.launcher2; +import java.util.ArrayList; +import java.util.List; + import android.content.ComponentName; -import android.content.Intent; import android.content.Context; +import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - /** * Stores the list of all applications for the all apps view. @@ -92,7 +91,7 @@ class AllAppsList { if (matches.size() > 0) { for (ResolveInfo info : matches) { - add(new ApplicationInfo(info, mIconCache)); + add(new ApplicationInfo(context.getPackageManager(), info, mIconCache)); } } } @@ -143,7 +142,7 @@ class AllAppsList { info.activityInfo.applicationInfo.packageName, info.activityInfo.name); if (applicationInfo == null) { - add(new ApplicationInfo(info, mIconCache)); + add(new ApplicationInfo(context.getPackageManager(), info, mIconCache)); } else { mIconCache.remove(applicationInfo.componentName); mIconCache.getTitleAndIcon(applicationInfo, info); diff --git a/src/com/android/launcher2/AllAppsPagedView.java b/src/com/android/launcher2/AllAppsPagedView.java new file mode 100644 index 0000000..4ca5b47 --- /dev/null +++ b/src/com/android/launcher2/AllAppsPagedView.java @@ -0,0 +1,578 @@ +/* + * 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 java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; + +import android.content.ComponentName; +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.ActionMode; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.widget.Checkable; +import android.widget.TextView; + +import com.android.launcher.R; + +/** + * An implementation of PagedView that populates the pages of the workspace + * with all of the user's applications. + */ +public class AllAppsPagedView extends PagedView + implements AllAppsView, View.OnClickListener, View.OnLongClickListener, DragSource, + DropTarget, ActionMode.Callback { + + private static final String TAG = "AllAppsPagedView"; + private static final boolean DEBUG = false; + + private static final int MENU_DELETE_APP = 1; + private static final int MENU_APP_INFO = 2; + + private Launcher mLauncher; + private DragController mDragController; + + // preserve compatibility with 3D all apps: + // 0.0 -> hidden + // 1.0 -> shown and opaque + // intermediate values -> partially shown & partially opaque + private float mZoom; + + // set of all applications + private ArrayList<ApplicationInfo> mApps; + private ArrayList<ApplicationInfo> mFilteredApps; + + // the types of applications to filter + static final int ALL_APPS_FLAG = -1; + private int mAppFilter = ALL_APPS_FLAG; + + private final LayoutInflater mInflater; + + private ViewGroup mOrigInfoButtonParent; + private LayoutParams mOrigInfoButtonLayoutParams; + + private ViewGroup mOrigDeleteZoneParent; + private LayoutParams mOrigDeleteZoneLayoutParams; + + public AllAppsPagedView(Context context) { + this(context, null); + } + + public AllAppsPagedView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AllAppsPagedView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PagedView, defStyle, 0); + mCellCountX = a.getInt(R.styleable.PagedView_cellCountX, 6); + mCellCountY = a.getInt(R.styleable.PagedView_cellCountY, 4); + mInflater = LayoutInflater.from(context); + a.recycle(); + setSoundEffectsEnabled(false); + } + + @Override + protected void init() { + super.init(); + mCenterPagesVertically = false; + } + + @Override + public void setLauncher(Launcher launcher) { + mLauncher = launcher; + mLauncher.setAllAppsPagedView(this); + } + + @Override + public void setDragController(DragController dragger) { + mDragController = dragger; + } + + public void setAppFilter(int filterType) { + mAppFilter = filterType; + if (mApps != null) { + mFilteredApps = rebuildFilteredApps(mApps); + setCurrentPage(0); + invalidatePageData(); + } + } + + @Override + public void zoom(float zoom, boolean animate) { + mZoom = zoom; + cancelLongPress(); + + if (isVisible()) { + getParent().bringChildToFront(this); + setVisibility(View.VISIBLE); + if (animate) { + startAnimation(AnimationUtils.loadAnimation(getContext(), + R.anim.all_apps_2d_fade_in)); + } else { + onAnimationEnd(); + } + } else { + if (animate) { + startAnimation(AnimationUtils.loadAnimation(getContext(), + R.anim.all_apps_2d_fade_out)); + } else { + onAnimationEnd(); + } + } + } + + protected void onAnimationEnd() { + if (!isVisible()) { + setVisibility(View.GONE); + mZoom = 0.0f; + + endChoiceMode(); + } else { + mZoom = 1.0f; + } + + if (mLauncher != null) + mLauncher.zoomed(mZoom); + } + + private int getChildIndexForGrandChild(View v) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; ++i) { + final PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i); + if (layout.indexOfChild(v) > -1) { + return i; + } + } + return -1; + } + + @Override + public void onClick(View v) { + // if we are already in a choice mode, then just change the selection + if (v instanceof Checkable) { + if (!isChoiceMode(CHOICE_MODE_NONE)) { + Checkable c = (Checkable) v; + if (isChoiceMode(CHOICE_MODE_SINGLE)) { + // Uncheck all the other grandchildren, and toggle the clicked one + boolean wasChecked = c.isChecked(); + resetCheckedGrandchildren(); + c.setChecked(!wasChecked); + } else { + c.toggle(); + } + if (getCheckedGrandchildren().size() == 0) { + endChoiceMode(); + } + + return; + } + } + + // otherwise continue and launch the application + int childIndex = getChildIndexForGrandChild(v); + if (childIndex == getCurrentPage()) { + final ApplicationInfo app = (ApplicationInfo) v.getTag(); + + // animate some feedback to the click + animateClickFeedback(v, new Runnable() { + @Override + public void run() { + mLauncher.startActivitySafely(app.intent, app); + } + }); + + endChoiceMode(); + } + } + + @Override + public boolean onLongClick(View v) { + if (!v.isInTouchMode()) { + return false; + } + + if (v instanceof Checkable) { + // In preparation for drag, we always reset the checked grand children regardless of + // what choice mode we are in + resetCheckedGrandchildren(); + + // Toggle the selection on the dragged app + Checkable c = (Checkable) v; + c.toggle(); + } + + // Start choice mode AFTER the item is selected + if (isChoiceMode(CHOICE_MODE_NONE)) { + startChoiceMode(CHOICE_MODE_SINGLE, this); + } + + ApplicationInfo app = (ApplicationInfo) v.getTag(); + app = new ApplicationInfo(app); + + mLauncher.getWorkspace().onDragStartedWithItemSpans(1, 1); + mDragController.startDrag(v, this, app, DragController.DRAG_ACTION_COPY); + return true; + } + + @Override + public void onDragViewVisible() { + } + + @Override + public void onDropCompleted(View target, boolean success) { + // close the choice action mode if we have a proper drop + if (target != this) { + endChoiceMode(); + } + mLauncher.getWorkspace().onDragStopped(); + } + + @Override + public boolean isVisible() { + return mZoom > 0.001f; + } + + @Override + public boolean isAnimating() { + return (getAnimation() != null); + } + + private ArrayList<ApplicationInfo> rebuildFilteredApps(ArrayList<ApplicationInfo> apps) { + ArrayList<ApplicationInfo> filteredApps = new ArrayList<ApplicationInfo>(); + if (mAppFilter == ALL_APPS_FLAG) { + return apps; + } else { + final int length = apps.size(); + for (int i = 0; i < length; ++i) { + ApplicationInfo info = apps.get(i); + if ((info.flags & mAppFilter) > 0) { + filteredApps.add(info); + } + } + } + return filteredApps; + } + + @Override + public void setApps(ArrayList<ApplicationInfo> list) { + mApps = list; + Collections.sort(mApps, LauncherModel.APP_NAME_COMPARATOR); + mFilteredApps = rebuildFilteredApps(mApps); + mPageViewIconCache.clear(); + invalidatePageData(); + } + + private void addAppsWithoutInvalidate(ArrayList<ApplicationInfo> list) { + // we add it in place, in alphabetical order + final int count = list.size(); + for (int i = 0; i < count; ++i) { + final ApplicationInfo info = list.get(i); + final int index = Collections.binarySearch(mApps, info, LauncherModel.APP_NAME_COMPARATOR); + if (index < 0) { + mApps.add(-(index + 1), info); + } + } + mFilteredApps = rebuildFilteredApps(mApps); + } + @Override + public void addApps(ArrayList<ApplicationInfo> list) { + addAppsWithoutInvalidate(list); + invalidatePageData(); + } + + private void removeAppsWithoutInvalidate(ArrayList<ApplicationInfo> list) { + // End the choice mode if any of the items in the list that are being removed are + // currently selected + ArrayList<Checkable> checkedList = getCheckedGrandchildren(); + HashSet<ApplicationInfo> checkedAppInfos = new HashSet<ApplicationInfo>(); + for (Checkable checked : checkedList) { + PagedViewIcon icon = (PagedViewIcon) checked; + checkedAppInfos.add((ApplicationInfo) icon.getTag()); + } + for (ApplicationInfo info : list) { + if (checkedAppInfos.contains(info)) { + endChoiceMode(); + break; + } + } + + // Loop through all the apps and remove apps that have the same component + final int length = list.size(); + for (int i = 0; i < length; ++i) { + final ApplicationInfo info = list.get(i); + int removeIndex = findAppByComponent(mApps, info); + if (removeIndex > -1) { + mApps.remove(removeIndex); + mPageViewIconCache.removeOutline(info); + } + } + mFilteredApps = rebuildFilteredApps(mApps); + } + @Override + public void removeApps(ArrayList<ApplicationInfo> list) { + removeAppsWithoutInvalidate(list); + invalidatePageData(); + } + + @Override + public void updateApps(ArrayList<ApplicationInfo> list) { + removeAppsWithoutInvalidate(list); + addAppsWithoutInvalidate(list); + invalidatePageData(); + } + + private int findAppByComponent(ArrayList<ApplicationInfo> list, ApplicationInfo item) { + ComponentName removeComponent = item.intent.getComponent(); + final int length = list.size(); + for (int i = 0; i < length; ++i) { + ApplicationInfo info = list.get(i); + if (info.intent.getComponent().equals(removeComponent)) { + return i; + } + } + return -1; + } + + @Override + public void dumpState() { + ApplicationInfo.dumpApplicationInfoList(TAG, "mApps", mApps); + } + + @Override + public void surrender() { + // do nothing? + } + + @Override + public void syncPages() { + // ensure that we have the right number of pages (min of 1, since we have placeholders) + int numPages = Math.max(1, + (int) Math.ceil((float) mFilteredApps.size() / (mCellCountX * mCellCountY))); + int curNumPages = getChildCount(); + // remove any extra pages after the "last" page + int extraPageDiff = curNumPages - numPages; + for (int i = 0; i < extraPageDiff; ++i) { + removeViewAt(numPages); + } + // add any necessary pages + for (int i = curNumPages; i < numPages; ++i) { + PagedViewCellLayout layout = new PagedViewCellLayout(getContext()); + layout.setCellCount(mCellCountX, mCellCountY); + layout.setPadding(mPageLayoutPaddingLeft, mPageLayoutPaddingTop, + mPageLayoutPaddingRight, mPageLayoutPaddingBottom); + layout.setGap(mPageLayoutWidthGap, mPageLayoutHeightGap); + addView(layout); + } + + // bound the current page + setCurrentPage(Math.max(0, Math.min(numPages - 1, getCurrentPage()))); + } + + @Override + public void syncPageItems(int page) { + // Ensure that we have the right number of items on the pages + final int cellsPerPage = mCellCountX * mCellCountY; + final int startIndex = page * cellsPerPage; + final int endIndex = Math.min(startIndex + cellsPerPage, mFilteredApps.size()); + PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(page); + + if (!mFilteredApps.isEmpty()) { + int curNumPageItems = layout.getChildCount(); + int numPageItems = endIndex - startIndex; + + // If we were previously an empty page, then restart anew + boolean wasEmptyPage = false; + if (curNumPageItems == 1) { + View icon = layout.getChildAt(0); + if (icon.getTag() == null) { + wasEmptyPage = true; + } + } + + if (wasEmptyPage) { + // Remove all the previous items + curNumPageItems = 0; + layout.removeAllViews(); + } else { + // Remove any extra items + int extraPageItemsDiff = curNumPageItems - numPageItems; + for (int i = 0; i < extraPageItemsDiff; ++i) { + layout.removeViewAt(numPageItems); + } + } + + // Add any necessary items + for (int i = curNumPageItems; i < numPageItems; ++i) { + TextView text = (TextView) mInflater.inflate( + R.layout.all_apps_paged_view_application, layout, false); + text.setOnClickListener(this); + text.setOnLongClickListener(this); + + layout.addViewToCellLayout(text, -1, i, + new PagedViewCellLayout.LayoutParams(0, 0, 1, 1)); + } + + // Actually reapply to the existing text views + for (int i = startIndex; i < endIndex; ++i) { + final int index = i - startIndex; + final ApplicationInfo info = mFilteredApps.get(i); + PagedViewIcon icon = (PagedViewIcon) layout.getChildAt(index); + icon.applyFromApplicationInfo(info, mPageViewIconCache, true); + + PagedViewCellLayout.LayoutParams params = + (PagedViewCellLayout.LayoutParams) icon.getLayoutParams(); + params.cellX = index % mCellCountX; + params.cellY = index / mCellCountX; + } + + // Default to left-aligned icons + layout.enableCenteredContent(false); + } else { + // There are no items, so show the user a small message + TextView icon = (TextView) mInflater.inflate( + R.layout.all_apps_no_items_placeholder, layout, false); + switch (mAppFilter) { + case ApplicationInfo.DOWNLOADED_FLAG: + icon.setText(mContext.getString(R.string.all_apps_no_downloads)); + break; + default: break; + } + + // Center-align the message + layout.enableCenteredContent(true); + layout.removeAllViews(); + layout.addViewToCellLayout(icon, -1, 0, + new PagedViewCellLayout.LayoutParams(0, 0, 2, 1)); + } + } + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + mode.setTitle(R.string.cab_app_selection_text); + + // Until the workspace has a selection mode and the CAB supports drag-and-drop, we + // take a hybrid approach: grab the views from the workspace and stuff them into the CAB. + // When the action mode is done, restore the views to their original place in the toolbar. + + ApplicationInfoDropTarget infoButton = + (ApplicationInfoDropTarget) mLauncher.findViewById(R.id.info_button); + mOrigInfoButtonParent = (ViewGroup) infoButton.getParent(); + mOrigInfoButtonLayoutParams = infoButton.getLayoutParams(); + mOrigInfoButtonParent.removeView(infoButton); + infoButton.setManageVisibility(false); + infoButton.setVisibility(View.VISIBLE); + infoButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + final ApplicationInfo appInfo = (ApplicationInfo) getChosenItem(); + mLauncher.startApplicationDetailsActivity(appInfo.componentName); + } + }); + + DeleteZone deleteZone = (DeleteZone) mLauncher.findViewById(R.id.delete_zone); + mOrigDeleteZoneParent = (ViewGroup) deleteZone.getParent(); + mOrigDeleteZoneLayoutParams = deleteZone.getLayoutParams(); + mOrigDeleteZoneParent.removeView(deleteZone); + deleteZone.setManageVisibility(false); + deleteZone.setVisibility(View.VISIBLE); + deleteZone.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + final ApplicationInfo appInfo = (ApplicationInfo) getChosenItem(); + mLauncher.startApplicationUninstallActivity(appInfo); + } + }); + + menu.add(0, MENU_DELETE_APP, 0, R.string.cab_menu_delete_app).setActionView(deleteZone); + menu.add(0, MENU_APP_INFO, 0, R.string.cab_menu_app_info).setActionView(infoButton); + + mLauncher.getWorkspace().shrinkToBottomVisible(); + + return true; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + mDragController.addDropTarget(this); + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + final Menu menu = mode.getMenu(); + + // Re-parent the drop targets into the toolbar, and restore their layout params + + ApplicationInfoDropTarget infoButton = + (ApplicationInfoDropTarget) menu.findItem(MENU_APP_INFO).getActionView(); + ((ViewGroup) infoButton.getParent()).removeView(infoButton); + mOrigInfoButtonParent.addView(infoButton, mOrigInfoButtonLayoutParams); + infoButton.setVisibility(View.GONE); + infoButton.setManageVisibility(true); + + DeleteZone deleteZone = (DeleteZone) menu.findItem(MENU_DELETE_APP).getActionView(); + ((ViewGroup) deleteZone.getParent()).removeView(deleteZone); + mOrigDeleteZoneParent.addView(deleteZone, mOrigDeleteZoneLayoutParams); + deleteZone.setVisibility(View.GONE); + deleteZone.setManageVisibility(true); + + mDragController.removeDropTarget(this); + endChoiceMode(); + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + // This is never called. Because we use setActionView(), we handle our own click events. + return false; + } + + /* + * We don't actually use AllAppsPagedView as a drop target... it's only used to intercept a drop + * to the workspace. + */ + @Override + public boolean acceptDrop(DragSource source, int x, int y, int xOffset, int yOffset, + DragView dragView, Object dragInfo) { + return false; + } + @Override + public DropTarget getDropTargetDelegate(DragSource source, int x, int y, int xOffset, + int yOffset, DragView dragView, Object dragInfo) { + return null; + } + @Override + public void onDragEnter(DragSource source, int x, int y, int xOffset, int yOffset, + DragView dragView, Object dragInfo) {} + @Override + public void onDragExit(DragSource source, int x, int y, int xOffset, int yOffset, + DragView dragView, Object dragInfo) {} + @Override + public void onDragOver(DragSource source, int x, int y, int xOffset, int yOffset, + DragView dragView, Object dragInfo) {} + @Override + public void onDrop(DragSource source, int x, int y, int xOffset, int yOffset, + DragView dragView, Object dragInfo) {} + + public boolean isDropEnabled() { + return true; + } +} diff --git a/src/com/android/launcher2/AllAppsTabbed.java b/src/com/android/launcher2/AllAppsTabbed.java new file mode 100644 index 0000000..0e32461 --- /dev/null +++ b/src/com/android/launcher2/AllAppsTabbed.java @@ -0,0 +1,181 @@ +/* + * 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 com.android.launcher.R; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.widget.TabHost; + +import java.util.ArrayList; + +/** + * Implements a tabbed version of AllApps2D. + */ +public class AllAppsTabbed extends TabHost implements AllAppsView { + + private static final String TAG = "Launcher.AllAppsTabbed"; + + private static final String TAG_ALL = "ALL"; + private static final String TAG_DOWNLOADED = "DOWNLOADED"; + + private AllAppsPagedView mAllApps; + private Context mContext; + + public AllAppsTabbed(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + } + + @Override + protected void onFinishInflate() { + // setup the tab host + setup(); + + try { + mAllApps = (AllAppsPagedView) findViewById(R.id.all_apps_paged_view); + if (mAllApps == null) throw new Resources.NotFoundException(); + } catch (Resources.NotFoundException e) { + Log.e(TAG, "Can't find necessary layout elements for AllAppsTabbed"); + } + + // share the same AllApps workspace across all the tabs + TabContentFactory contentFactory = new TabContentFactory() { + public View createTabContent(String tag) { + return mAllApps; + } + }; + + String label = mContext.getString(R.string.all_apps_tab_all); + addTab(newTabSpec(TAG_ALL).setIndicator(label).setContent(contentFactory)); + + label = mContext.getString(R.string.all_apps_tab_downloaded); + addTab(newTabSpec(TAG_DOWNLOADED).setIndicator(label).setContent(contentFactory)); + + setOnTabChangedListener(new OnTabChangeListener() { + public void onTabChanged(String tabId) { + // animate the changing of the tab content by fading pages in and out + final Resources res = getResources(); + final int duration = res.getInteger(R.integer.config_tabTransitionTime); + final float alpha = mAllApps.getAlpha(); + ValueAnimator alphaAnim = ObjectAnimator.ofFloat(mAllApps, "alpha", alpha, 0.0f). + setDuration(duration); + alphaAnim.addListener(new LauncherAnimatorListenerAdapter() { + @Override + public void onAnimationEndOrCancel(Animator animation) { + String tag = getCurrentTabTag(); + if (tag == TAG_ALL) { + mAllApps.setAppFilter(AllAppsPagedView.ALL_APPS_FLAG); + } else if (tag == TAG_DOWNLOADED) { + mAllApps.setAppFilter(ApplicationInfo.DOWNLOADED_FLAG); + } + + final float alpha = mAllApps.getAlpha(); + ObjectAnimator.ofFloat(mAllApps, "alpha", alpha, 1.0f). + setDuration(duration).start(); + } + }); + alphaAnim.start(); + } + }); + + // It needs to be INVISIBLE so that it will be measured in the layout. + // Otherwise the animations is messed up when we show it for the first time. + setVisibility(INVISIBLE); + } + + @Override + public void setLauncher(Launcher launcher) { + mAllApps.setLauncher(launcher); + } + + @Override + public void setDragController(DragController dragger) { + mAllApps.setDragController(dragger); + } + + @Override + public void zoom(float zoom, boolean animate) { + // NOTE: animate parameter is ignored for the TabHost itself + setVisibility((zoom == 0.0f) ? View.GONE : View.VISIBLE); + mAllApps.zoom(zoom, animate); + } + + @Override + public void setVisibility(int visibility) { + final boolean isVisible = (visibility == View.VISIBLE); + super.setVisibility(visibility); + float zoom = (isVisible ? 1.0f : 0.0f); + mAllApps.zoom(zoom, false); + } + + @Override + public boolean isVisible() { + return mAllApps.isVisible(); + } + + @Override + public boolean isAnimating() { + return (getAnimation() != null); + } + + @Override + public void setApps(ArrayList<ApplicationInfo> list) { + mAllApps.setApps(list); + } + + @Override + public void addApps(ArrayList<ApplicationInfo> list) { + mAllApps.addApps(list); + } + + @Override + public void removeApps(ArrayList<ApplicationInfo> list) { + mAllApps.removeApps(list); + } + + @Override + public void updateApps(ArrayList<ApplicationInfo> list) { + mAllApps.updateApps(list); + } + + @Override + public void dumpState() { + mAllApps.dumpState(); + } + + @Override + public void surrender() { + mAllApps.surrender(); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (ev.getY() > mAllApps.getBottom()) { + return false; + } + return true; + } +} diff --git a/src/com/android/launcher2/AllAppsView.java b/src/com/android/launcher2/AllAppsView.java index 877c075..007ecf8 100644 --- a/src/com/android/launcher2/AllAppsView.java +++ b/src/com/android/launcher2/AllAppsView.java @@ -31,7 +31,7 @@ public interface AllAppsView { public boolean isVisible(); - public boolean isOpaque(); + public boolean isAnimating(); public void setApps(ArrayList<ApplicationInfo> list); diff --git a/src/com/android/launcher2/ApplicationInfo.java b/src/com/android/launcher2/ApplicationInfo.java index 5bb5037..0851cd3 100644 --- a/src/com/android/launcher2/ApplicationInfo.java +++ b/src/com/android/launcher2/ApplicationInfo.java @@ -17,12 +17,11 @@ package com.android.launcher2; import android.content.ComponentName; -import android.content.ContentValues; -import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; import android.util.Log; import java.util.ArrayList; @@ -31,6 +30,7 @@ import java.util.ArrayList; * Represents an app in AllAppsView. */ class ApplicationInfo extends ItemInfo { + private static final String TAG = "Launcher2.ApplicationInfo"; /** * The application name. @@ -54,6 +54,10 @@ class ApplicationInfo extends ItemInfo { ComponentName componentName; + static final int APP_FLAG = 1; + static final int GAME_FLAG = 2; + static final int DOWNLOADED_FLAG = 4; + int flags = 0; ApplicationInfo() { itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT; @@ -62,15 +66,32 @@ class ApplicationInfo extends ItemInfo { /** * Must not hold the Context. */ - public ApplicationInfo(ResolveInfo info, IconCache iconCache) { - this.componentName = new ComponentName( - info.activityInfo.applicationInfo.packageName, - info.activityInfo.name); + public ApplicationInfo(PackageManager pm, ResolveInfo info, IconCache iconCache) { + final String packageName = info.activityInfo.applicationInfo.packageName; + this.componentName = new ComponentName(packageName, info.activityInfo.name); this.container = ItemInfo.NO_ID; this.setActivity(componentName, Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + try { + int appFlags = pm.getApplicationInfo(packageName, 0).flags; + if ((appFlags & android.content.pm.ApplicationInfo.FLAG_SYSTEM) == 0) { + flags |= DOWNLOADED_FLAG; + } + if ((appFlags & android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) { + flags |= DOWNLOADED_FLAG; + } + // TODO: Figure out how to determine what is a game + + // If it's not a game, it's an app + if ((flags & GAME_FLAG) == 0) { + flags |= APP_FLAG; + } + } catch (NameNotFoundException e) { + Log.d(TAG, "PackageManager.getApplicationInfo failed for " + packageName); + } + iconCache.getTitleAndIcon(this, info); } @@ -79,6 +100,7 @@ class ApplicationInfo extends ItemInfo { componentName = info.componentName; title = info.title.toString(); intent = new Intent(info.intent); + flags = info.flags; } /** diff --git a/src/com/android/launcher2/ApplicationInfoDropTarget.java b/src/com/android/launcher2/ApplicationInfoDropTarget.java new file mode 100644 index 0000000..c2922ab --- /dev/null +++ b/src/com/android/launcher2/ApplicationInfoDropTarget.java @@ -0,0 +1,168 @@ +/* + * 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 com.android.launcher.R; + +import android.content.ComponentName; +import android.content.Context; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; + +/** + * Implements a DropTarget which allows applications to be dropped on it, + * in order to launch the application info for that app. + */ +public class ApplicationInfoDropTarget extends ImageView implements DropTarget, DragController.DragListener { + private Launcher mLauncher; + private boolean mActive = false; + + /** + * If true, this View responsible for managing its own visibility, and that of its handle. + * This is generally the case, but it will be set to false when this is part of the + * Contextual Action Bar. + */ + private boolean mManageVisibility = true; + + /** The view that this view should appear in the place of. */ + private View mHandle = null; + + private final Paint mPaint = new Paint(); + + public ApplicationInfoDropTarget(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ApplicationInfoDropTarget(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Set the color that will be used as a filter over objects dragged over this object. + */ + public void setDragColor(int color) { + mPaint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)); + } + + public boolean acceptDrop(DragSource source, int x, int y, int xOffset, int yOffset, + DragView dragView, Object dragInfo) { + + // acceptDrop is called just before onDrop. We do the work here, rather than + // in onDrop, because it allows us to reject the drop (by returning false) + // so that the object being dragged isn't removed from the home screen. + + ComponentName componentName = null; + if (dragInfo instanceof ApplicationInfo) { + componentName = ((ApplicationInfo)dragInfo).componentName; + } else if (dragInfo instanceof ShortcutInfo) { + componentName = ((ShortcutInfo)dragInfo).intent.getComponent(); + } + mLauncher.startApplicationDetailsActivity(componentName); + return false; + } + + public void onDrop(DragSource source, int x, int y, int xOffset, int yOffset, + DragView dragView, Object dragInfo) { + + } + + public void onDragEnter(DragSource source, int x, int y, int xOffset, int yOffset, + DragView dragView, Object dragInfo) { + dragView.setPaint(mPaint); + } + + public void onDragOver(DragSource source, int x, int y, int xOffset, int yOffset, + DragView dragView, Object dragInfo) { + } + + public void onDragExit(DragSource source, int x, int y, int xOffset, int yOffset, + DragView dragView, Object dragInfo) { + dragView.setPaint(null); + } + + public void onDragStart(DragSource source, Object info, int dragAction) { + if (info != null) { + final int itemType = ((ItemInfo)info).itemType; + mActive = (itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION); + if (mManageVisibility) { + // Only show the info icon when an application is selected + if (mActive) { + setVisibility(VISIBLE); + } + mHandle.setVisibility(INVISIBLE); + } + } + } + + public boolean isDropEnabled() { + return mActive; + } + + public void onDragEnd() { + if (mActive) { + mActive = false; + } + if (mManageVisibility) { + setVisibility(GONE); + mHandle.setVisibility(VISIBLE); + } + } + + @Override + public void getHitRect(Rect outRect) { + super.getHitRect(outRect); + if (LauncherApplication.isScreenXLarge()) { + // TODO: This is a temporary hack. mManageVisiblity = false when you're in CAB mode. + // In that case, this icon is more tightly spaced next to the delete icon so we want + // it to have a smaller drag region. When the new drag&drop system comes in, we'll + // dispatch the drag/drop by calculating what target you're overlapping + final int minPadding = R.dimen.delete_zone_min_padding; + final int maxPadding = R.dimen.delete_zone_max_padding; + final int outerDragPadding = + getResources().getDimensionPixelSize(R.dimen.delete_zone_size); + final int innerDragPadding = getResources().getDimensionPixelSize( + mManageVisibility ? maxPadding : minPadding); + outRect.top -= outerDragPadding; + outRect.left -= innerDragPadding; + outRect.bottom += outerDragPadding; + outRect.right += outerDragPadding; + } + } + + void setLauncher(Launcher launcher) { + mLauncher = launcher; + } + + void setHandle(View view) { + mHandle = view; + } + + void setManageVisibility(boolean value) { + mManageVisibility = value; + } + + @Override + public DropTarget getDropTargetDelegate(DragSource source, int x, int y, int xOffset, int yOffset, + DragView dragView, Object dragInfo) { + return null; + } +} diff --git a/src/com/android/launcher2/BubbleTextView.java b/src/com/android/launcher2/BubbleTextView.java index 4a56e1b..f4a3d44 100644 --- a/src/com/android/launcher2/BubbleTextView.java +++ b/src/com/android/launcher2/BubbleTextView.java @@ -39,6 +39,7 @@ public class BubbleTextView extends TextView { private final RectF mRect = new RectF(); private Paint mPaint; + private int mPrevAlpha = -1; private boolean mBackgroundSizeChanged; private Drawable mBackground; @@ -144,4 +145,14 @@ public class BubbleTextView extends TextView { super.onDetachedFromWindow(); mBackground.setCallback(null); } + + @Override + protected boolean onSetAlpha(int alpha) { + if (mPrevAlpha != alpha) { + mPrevAlpha = alpha; + mPaint.setAlpha(alpha); + super.onSetAlpha(alpha); + } + return true; + } } diff --git a/src/com/android/launcher2/CellLayout.java b/src/com/android/launcher2/CellLayout.java index 9d39c2c..023e9f4 100644 --- a/src/com/android/launcher2/CellLayout.java +++ b/src/com/android/launcher2/CellLayout.java @@ -16,54 +16,114 @@ package com.android.launcher2; +import com.android.launcher.R; + +import android.animation.Animator; +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.WallpaperManager; import android.content.Context; -import android.content.res.TypedArray; import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; -import android.graphics.Canvas; +import android.graphics.Region; +import android.graphics.drawable.Drawable; import android.util.AttributeSet; +import android.util.Log; import android.view.ContextMenu; import android.view.MotionEvent; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; -import android.app.WallpaperManager; +import android.view.animation.Animation; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.LayoutAnimationController; -import java.util.ArrayList; +import java.util.Arrays; -import com.android.launcher.R; - -public class CellLayout extends ViewGroup { - private boolean mPortrait; +public class CellLayout extends ViewGroup implements Dimmable { + static final String TAG = "CellLayout"; private int mCellWidth; private int mCellHeight; - - private int mLongAxisStartPadding; - private int mLongAxisEndPadding; - private int mShortAxisStartPadding; - private int mShortAxisEndPadding; + private int mLeftPadding; + private int mRightPadding; + private int mTopPadding; + private int mBottomPadding; - private int mShortAxisCells; - private int mLongAxisCells; + private int mCountX; + private int mCountY; private int mWidthGap; private int mHeightGap; private final Rect mRect = new Rect(); + private final RectF mRectF = new RectF(); private final CellInfo mCellInfo = new CellInfo(); - - int[] mCellXY = new int[2]; + + // These are temporary variables to prevent having to allocate a new object just to + // return an (x, y) value from helper functions. Do NOT use them to maintain other state. + private final int[] mTmpCellXY = new int[2]; + private final int[] mTmpPoint = new int[2]; + private final PointF mTmpPointF = new PointF(); + boolean[][] mOccupied; - private RectF mDragRect = new RectF(); + private OnTouchListener mInterceptTouchListener; + + private float mBackgroundAlpha; + private float mBackgroundAlphaMultiplier = 1.0f; + + private Drawable mBackground; + private Drawable mBackgroundMini; + private Drawable mBackgroundMiniHover; + private Drawable mBackgroundHover; + private Drawable mBackgroundMiniAcceptsDrops; + private Rect mBackgroundRect; + private Rect mHoverRect; + private float mHoverScale; + private float mHoverAlpha; + private boolean mAcceptsDrops; + + // If we're actively dragging something over this screen, mHover is true + private boolean mHover = false; + + private final Point mDragCenter = new Point(); - private boolean mDirtyTag; - private boolean mLastDownOnOccupiedCell = false; - - private final WallpaperManager mWallpaperManager; + // 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[8]; + private float[] mDragOutlineAlphas = new float[mDragOutlines.length]; + private InterruptibleInOutAnimator[] mDragOutlineAnims = + new InterruptibleInOutAnimator[mDragOutlines.length]; + + // Used as an index into the above 3 arrays; indicates which is the most current value. + private int mDragOutlineCurrent = 0; + private final Paint mDragOutlinePaint = new Paint(); + + private Drawable mCrosshairsDrawable = null; + private InterruptibleInOutAnimator mCrosshairsAnimator = null; + private float mCrosshairsVisibility = 0.0f; + + // When a drag operation is in progress, holds the nearest cell to the touch point + private final int[] mDragCell = new int[2]; + + private final WallpaperManager mWallpaperManager; + + private boolean mDragging = false; + + private TimeInterpolator mEaseOutInterpolator; public CellLayout(Context context) { this(context, null); @@ -75,44 +135,309 @@ public class CellLayout extends ViewGroup { public CellLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + + // 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); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CellLayout, defStyle, 0); mCellWidth = a.getDimensionPixelSize(R.styleable.CellLayout_cellWidth, 10); mCellHeight = a.getDimensionPixelSize(R.styleable.CellLayout_cellHeight, 10); - - mLongAxisStartPadding = - a.getDimensionPixelSize(R.styleable.CellLayout_longAxisStartPadding, 10); - mLongAxisEndPadding = - a.getDimensionPixelSize(R.styleable.CellLayout_longAxisEndPadding, 10); - mShortAxisStartPadding = - a.getDimensionPixelSize(R.styleable.CellLayout_shortAxisStartPadding, 10); - mShortAxisEndPadding = - a.getDimensionPixelSize(R.styleable.CellLayout_shortAxisEndPadding, 10); - - mShortAxisCells = a.getInt(R.styleable.CellLayout_shortAxisCells, 4); - mLongAxisCells = a.getInt(R.styleable.CellLayout_longAxisCells, 4); + mWidthGap = a.getDimensionPixelSize(R.styleable.CellLayout_widthGap, -1); + mHeightGap = a.getDimensionPixelSize(R.styleable.CellLayout_heightGap, -1); + + mLeftPadding = + a.getDimensionPixelSize(R.styleable.CellLayout_xAxisStartPadding, 10); + mRightPadding = + a.getDimensionPixelSize(R.styleable.CellLayout_xAxisEndPadding, 10); + mTopPadding = + a.getDimensionPixelSize(R.styleable.CellLayout_yAxisStartPadding, 10); + mBottomPadding = + a.getDimensionPixelSize(R.styleable.CellLayout_yAxisEndPadding, 10); + + mCountX = LauncherModel.getCellCountX(); + mCountY = LauncherModel.getCellCountY(); + mOccupied = new boolean[mCountX][mCountY]; a.recycle(); setAlwaysDrawnWithCacheEnabled(false); - if (mOccupied == null) { - if (mPortrait) { - mOccupied = new boolean[mShortAxisCells][mLongAxisCells]; - } else { - mOccupied = new boolean[mLongAxisCells][mShortAxisCells]; + mWallpaperManager = WallpaperManager.getInstance(context); + + final Resources res = getResources(); + + if (LauncherApplication.isScreenXLarge()) { + mBackgroundMini = res.getDrawable(R.drawable.mini_home_screen_bg); + mBackgroundMini.setFilterBitmap(true); + mBackground = res.getDrawable(R.drawable.home_screen_bg); + mBackground.setFilterBitmap(true); + mBackgroundMiniHover = res.getDrawable(R.drawable.mini_home_screen_bg_hover); + mBackgroundMiniHover.setFilterBitmap(true); + mBackgroundHover = res.getDrawable(R.drawable.home_screen_bg_hover); + mBackgroundHover.setFilterBitmap(true); + mBackgroundMiniAcceptsDrops = res.getDrawable( + R.drawable.mini_home_screen_bg_accepts_drops); + mBackgroundMiniAcceptsDrops.setFilterBitmap(true); + } + + // Initialize the data structures used for the drag visualization. + + mCrosshairsDrawable = res.getDrawable(R.drawable.gardening_crosshairs); + mEaseOutInterpolator = new DecelerateInterpolator(2.5f); // Quint ease out + + // Set up the animation for fading the crosshairs in and out + int animDuration = res.getInteger(R.integer.config_crosshairsFadeInTime); + mCrosshairsAnimator = new InterruptibleInOutAnimator(animDuration, 0.0f, 1.0f); + mCrosshairsAnimator.getAnimator().addUpdateListener(new AnimatorUpdateListener() { + public void onAnimationUpdate(ValueAnimator animation) { + mCrosshairsVisibility = ((Float) animation.getAnimatedValue()).floatValue(); + invalidate(); } + }); + mCrosshairsAnimator.getAnimator().setInterpolator(mEaseOutInterpolator); + + for (int i = 0; i < mDragOutlines.length; i++) { + mDragOutlines[i] = new Point(-1, -1); + } + + // When dragging things around the home screens, we show a green outline of + // where the item will land. The outlines gradually fade out, leaving a trail + // behind the drag path. + // Set up all the animations that are used to implement this fading. + final int duration = res.getInteger(R.integer.config_dragOutlineFadeTime); + final float fromAlphaValue = 0; + final float toAlphaValue = (float)res.getInteger(R.integer.config_dragOutlineMaxAlpha); + + Arrays.fill(mDragOutlineAlphas, fromAlphaValue); + + for (int i = 0; i < mDragOutlineAnims.length; i++) { + final InterruptibleInOutAnimator anim = + new InterruptibleInOutAnimator(duration, fromAlphaValue, toAlphaValue); + anim.getAnimator().setInterpolator(mEaseOutInterpolator); + final int thisIndex = i; + anim.getAnimator().addUpdateListener(new AnimatorUpdateListener() { + public void onAnimationUpdate(ValueAnimator animation) { + final Bitmap outline = (Bitmap)anim.getTag(); + + // If an animation is started and then stopped very quickly, we can still + // get spurious updates we've cleared the tag. Guard against this. + if (outline == null) { + if (false) { + Object val = animation.getAnimatedValue(); + Log.d(TAG, "anim " + thisIndex + " update: " + val + + ", isStopped " + anim.isStopped()); + } + // Try to prevent it from continuing to run + animation.cancel(); + } else { + mDragOutlineAlphas[thisIndex] = (Float) animation.getAnimatedValue(); + final int left = mDragOutlines[thisIndex].x; + final int top = mDragOutlines[thisIndex].y; + CellLayout.this.invalidate(left, top, + left + outline.getWidth(), top + outline.getHeight()); + } + } + }); + // The animation holds a reference to the drag outline bitmap as long is it's + // running. This way the bitmap can be GCed when the animations are complete. + anim.getAnimator().addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if ((Float) ((ValueAnimator) animation).getAnimatedValue() == 0f) { + anim.setTag(null); + } + } + }); + mDragOutlineAnims[i] = anim; } - - mWallpaperManager = WallpaperManager.getInstance(getContext()); + + mBackgroundRect = new Rect(); + mHoverRect = new Rect(); + setHoverScale(1.0f); + setHoverAlpha(1.0f); } - @Override - public void dispatchDraw(Canvas canvas) { + private void updateHoverRect() { + float marginFraction = (mHoverScale - 1.0f) / 2.0f; + int marginX = (int) (marginFraction * (mBackgroundRect.right - mBackgroundRect.left)); + int marginY = (int) (marginFraction * (mBackgroundRect.bottom - mBackgroundRect.top)); + mHoverRect.set(mBackgroundRect.left - marginX, mBackgroundRect.top - marginY, + mBackgroundRect.right + marginX, mBackgroundRect.bottom + marginY); + invalidate(); + } + + public void setHoverScale(float scaleFactor) { + if (scaleFactor != mHoverScale) { + mHoverScale = scaleFactor; + updateHoverRect(); + } + } + + public float getHoverScale() { + return mHoverScale; + } + + public float getHoverAlpha() { + return mHoverAlpha; + } + + public void setHoverAlpha(float alpha) { + mHoverAlpha = alpha; + invalidate(); + } + + void animateDrop() { + if (LauncherApplication.isScreenXLarge()) { + Resources res = getResources(); + float onDropScale = res.getInteger(R.integer.config_screenOnDropScalePercent) / 100.0f; + ObjectAnimator scaleUp = ObjectAnimator.ofFloat(this, "hoverScale", onDropScale); + scaleUp.setDuration(res.getInteger(R.integer.config_screenOnDropScaleUpDuration)); + ObjectAnimator scaleDown = ObjectAnimator.ofFloat(this, "hoverScale", 1.0f); + scaleDown.setDuration(res.getInteger(R.integer.config_screenOnDropScaleDownDuration)); + ObjectAnimator alphaFadeOut = ObjectAnimator.ofFloat(this, "hoverAlpha", 0.0f); + + alphaFadeOut.setStartDelay(res.getInteger(R.integer.config_screenOnDropAlphaFadeDelay)); + alphaFadeOut.setDuration(res.getInteger(R.integer.config_screenOnDropAlphaFadeDelay)); + + AnimatorSet bouncer = new AnimatorSet(); + bouncer.play(scaleUp).before(scaleDown); + bouncer.play(scaleUp).with(alphaFadeOut); + bouncer.addListener(new LauncherAnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + setHover(true); + } + @Override + public void onAnimationEndOrCancel(Animator animation) { + setHover(false); + setHoverScale(1.0f); + setHoverAlpha(1.0f); + } + }); + bouncer.start(); + } + } + + public void setHover(boolean value) { + if (mHover != value) { + mHover = value; + invalidate(); + } + } + + public boolean getHover() { + return mHover; + } + + public void drawChildren(Canvas canvas) { super.dispatchDraw(canvas); } @Override + protected void onDraw(Canvas canvas) { + // When we're large, we are either drawn in a "hover" state (ie when dragging an item to + // a neighboring page) or with just a normal background (if backgroundAlpha > 0.0f) + // When we're small, we are either drawn normally or in the "accepts drops" state (during + // a drag). However, we also drag the mini hover background *over* one of those two + // backgrounds + if (mBackgroundAlpha > 0.0f) { + Drawable bg; + if (getScaleX() < 0.5f) { + bg = mAcceptsDrops ? mBackgroundMiniAcceptsDrops : mBackgroundMini; + } else { + bg = mHover ? mBackgroundHover : mBackground; + } + if (bg != null) { + bg.setAlpha((int) (mBackgroundAlpha * mBackgroundAlphaMultiplier * 255)); + bg.setBounds(mBackgroundRect); + bg.draw(canvas); + } + if (mHover && getScaleX() < 0.5f) { + boolean modifiedClipRect = false; + if (mHoverScale > 1.0f) { + // If the hover background's scale is greater than 1, we'll be drawing outside + // the bounds of this CellLayout. Get around that by temporarily increasing the + // size of the clip rect + float marginFraction = (mHoverScale - 1.0f) / 2.0f; + Rect clipRect = canvas.getClipBounds(); + int marginX = (int) (marginFraction * (clipRect.right - clipRect.left)); + int marginY = (int) (marginFraction * (clipRect.bottom - clipRect.top)); + canvas.save(Canvas.CLIP_SAVE_FLAG); + canvas.clipRect(-marginX, -marginY, + getWidth() + marginX, getHeight() + marginY, Region.Op.REPLACE); + modifiedClipRect = true; + } + + mBackgroundMiniHover.setAlpha((int) (mBackgroundAlpha * mHoverAlpha * 255)); + mBackgroundMiniHover.setBounds(mHoverRect); + mBackgroundMiniHover.draw(canvas); + if (modifiedClipRect) { + canvas.restore(); + } + } + } + + if (mCrosshairsVisibility > 0.0f) { + final int countX = mCountX; + final int countY = mCountY; + + final float MAX_ALPHA = 0.4f; + final int MAX_VISIBLE_DISTANCE = 600; + final float DISTANCE_MULTIPLIER = 0.002f; + + final Drawable d = mCrosshairsDrawable; + final int width = d.getIntrinsicWidth(); + final int height = d.getIntrinsicHeight(); + + int x = getLeftPadding() - (mWidthGap / 2) - (width / 2); + for (int col = 0; col <= countX; col++) { + int y = getTopPadding() - (mHeightGap / 2) - (height / 2); + for (int row = 0; row <= countY; row++) { + mTmpPointF.set(x - mDragCenter.x, y - mDragCenter.y); + float dist = mTmpPointF.length(); + // Crosshairs further from the drag point are more faint + float alpha = Math.min(MAX_ALPHA, + DISTANCE_MULTIPLIER * (MAX_VISIBLE_DISTANCE - dist)); + if (alpha > 0.0f) { + d.setBounds(x, y, x + width, y + height); + d.setAlpha((int) (alpha * 255 * mCrosshairsVisibility)); + d.draw(canvas); + } + y += mCellHeight + mHeightGap; + } + x += mCellWidth + mWidthGap; + } + } + + final Paint paint = mDragOutlinePaint; + for (int i = 0; i < mDragOutlines.length; i++) { + final float alpha = mDragOutlineAlphas[i]; + if (alpha > 0) { + final Point p = mDragOutlines[i]; + final Bitmap b = (Bitmap) mDragOutlineAnims[i].getTag(); + paint.setAlpha((int)(alpha + .5f)); + canvas.drawBitmap(b, p.x, p.y, paint); + } + } + } + + public void setDimmableProgress(float progress) { + for (int i = 0; i < getChildCount(); i++) { + Dimmable d = (Dimmable) getChildAt(i); + d.setDimmableProgress(progress); + } + } + + public float getDimmableProgress() { + if (getChildCount() > 0) { + return ((Dimmable) getChildAt(0)).getDimmableProgress(); + } + return 0.0f; + } + + @Override public void cancelLongPress() { super.cancelLongPress(); @@ -124,22 +449,104 @@ public class CellLayout extends ViewGroup { } } + public void setOnInterceptTouchListener(View.OnTouchListener listener) { + mInterceptTouchListener = listener; + } + int getCountX() { - return mPortrait ? mShortAxisCells : mLongAxisCells; + return mCountX; } int getCountY() { - return mPortrait ? mLongAxisCells : mShortAxisCells; + return mCountY; } - @Override - public void addView(View child, int index, ViewGroup.LayoutParams params) { + public boolean addViewToCellLayout(View child, int index, int childId, LayoutParams params) { + return addViewToCellLayout(child, index, childId, params, true); + } + + public boolean addViewToCellLayout( + View child, int index, int childId, LayoutParams params, boolean markCells) { + final LayoutParams lp = params; + // Generate an id for each view, this assumes we have at most 256x256 cells // per workspace screen - final LayoutParams cellParams = (LayoutParams) params; - cellParams.regenerateId = true; + if (lp.cellX >= 0 && lp.cellX <= mCountX - 1 && lp.cellY >= 0 && lp.cellY <= mCountY - 1) { + // If the horizontal or vertical span is set to -1, it is taken to + // mean that it spans the extent of the CellLayout + if (lp.cellHSpan < 0) lp.cellHSpan = mCountX; + if (lp.cellVSpan < 0) lp.cellVSpan = mCountY; - super.addView(child, index, params); + child.setId(childId); + + // We might be in the middle or end of shrinking/fading to a dimmed view + // Make sure this view's alpha is set the same as all the rest of the views + child.setAlpha(getAlpha()); + addView(child, index, lp); + + if (markCells) markCellsAsOccupiedForView(child); + + return true; + } + return false; + } + public void setAcceptsDrops(boolean acceptsDrops) { + if (mAcceptsDrops != acceptsDrops) { + mAcceptsDrops = acceptsDrops; + invalidate(); + } + } + + public boolean getAcceptsDrops() { + return mAcceptsDrops; + } + + @Override + public void removeAllViews() { + clearOccupiedCells(); + } + + @Override + public void removeAllViewsInLayout() { + clearOccupiedCells(); + } + + public void removeViewWithoutMarkingCells(View view) { + super.removeView(view); + } + + @Override + public void removeView(View view) { + markCellsAsUnoccupiedForView(view); + super.removeView(view); + } + + @Override + public void removeViewAt(int index) { + markCellsAsUnoccupiedForView(getChildAt(index)); + super.removeViewAt(index); + } + + @Override + public void removeViewInLayout(View view) { + markCellsAsUnoccupiedForView(view); + super.removeViewInLayout(view); + } + + @Override + public void removeViews(int start, int count) { + for (int i = start; i < start + count; i++) { + markCellsAsUnoccupiedForView(getChildAt(i)); + } + super.removeViews(start, count); + } + + @Override + public void removeViewsInLayout(int start, int count) { + for (int i = start; i < start + count; i++) { + markCellsAsUnoccupiedForView(getChildAt(i)); + } + super.removeViewsInLayout(start, count); } @Override @@ -158,67 +565,59 @@ public class CellLayout extends ViewGroup { mCellInfo.screen = ((ViewGroup) getParent()).indexOfChild(this); } + public void setTagToCellInfoForPoint(int touchX, int touchY) { + final CellInfo cellInfo = mCellInfo; + final Rect frame = mRect; + final int x = touchX + mScrollX; + final int y = touchY + mScrollY; + final int count = getChildCount(); + + boolean found = false; + for (int i = count - 1; i >= 0; i--) { + final View child = getChildAt(i); + + if ((child.getVisibility()) == VISIBLE || child.getAnimation() != null) { + child.getHitRect(frame); + if (frame.contains(x, y)) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + cellInfo.cell = child; + cellInfo.cellX = lp.cellX; + cellInfo.cellY = lp.cellY; + cellInfo.spanX = lp.cellHSpan; + cellInfo.spanY = lp.cellVSpan; + cellInfo.valid = true; + found = true; + break; + } + } + } + + if (!found) { + final int cellXY[] = mTmpCellXY; + pointToCellExact(x, y, cellXY); + + cellInfo.cell = null; + cellInfo.cellX = cellXY[0]; + cellInfo.cellY = cellXY[1]; + cellInfo.spanX = 1; + cellInfo.spanY = 1; + cellInfo.valid = cellXY[0] >= 0 && cellXY[1] >= 0 && cellXY[0] < mCountX && + cellXY[1] < mCountY && !mOccupied[cellXY[0]][cellXY[1]]; + } + setTag(cellInfo); + } + + @Override public boolean onInterceptTouchEvent(MotionEvent ev) { + if (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev)) { + return true; + } final int action = ev.getAction(); final CellInfo cellInfo = mCellInfo; if (action == MotionEvent.ACTION_DOWN) { - final Rect frame = mRect; - final int x = (int) ev.getX() + mScrollX; - final int y = (int) ev.getY() + mScrollY; - final int count = getChildCount(); - - boolean found = false; - for (int i = count - 1; i >= 0; i--) { - final View child = getChildAt(i); - - if ((child.getVisibility()) == VISIBLE || child.getAnimation() != null) { - child.getHitRect(frame); - if (frame.contains(x, y)) { - final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - cellInfo.cell = child; - cellInfo.cellX = lp.cellX; - cellInfo.cellY = lp.cellY; - cellInfo.spanX = lp.cellHSpan; - cellInfo.spanY = lp.cellVSpan; - cellInfo.valid = true; - found = true; - mDirtyTag = false; - break; - } - } - } - - mLastDownOnOccupiedCell = found; - - if (!found) { - int cellXY[] = mCellXY; - pointToCellExact(x, y, cellXY); - - final boolean portrait = mPortrait; - final int xCount = portrait ? mShortAxisCells : mLongAxisCells; - final int yCount = portrait ? mLongAxisCells : mShortAxisCells; - - final boolean[][] occupied = mOccupied; - findOccupiedCells(xCount, yCount, occupied, null); - - cellInfo.cell = null; - cellInfo.cellX = cellXY[0]; - cellInfo.cellY = cellXY[1]; - cellInfo.spanX = 1; - cellInfo.spanY = 1; - cellInfo.valid = cellXY[0] >= 0 && cellXY[1] >= 0 && cellXY[0] < xCount && - cellXY[1] < yCount && !occupied[cellXY[0]][cellXY[1]]; - - // Instead of finding the interesting vacant cells here, wait until a - // caller invokes getTag() to retrieve the result. Finding the vacant - // cells is a bit expensive and can generate many new objects, it's - // therefore better to defer it until we know we actually need it. - - mDirtyTag = true; - } - setTag(cellInfo); + setTagToCellInfoForPoint((int) ev.getX(), (int) ev.getY()); } else if (action == MotionEvent.ACTION_UP) { cellInfo.cell = null; cellInfo.cellX = -1; @@ -226,7 +625,6 @@ public class CellLayout extends ViewGroup { cellInfo.spanX = 0; cellInfo.spanY = 0; cellInfo.valid = false; - mDirtyTag = false; setTag(cellInfo); } @@ -235,104 +633,12 @@ public class CellLayout extends ViewGroup { @Override public CellInfo getTag() { - final CellInfo info = (CellInfo) super.getTag(); - if (mDirtyTag && info.valid) { - final boolean portrait = mPortrait; - final int xCount = portrait ? mShortAxisCells : mLongAxisCells; - final int yCount = portrait ? mLongAxisCells : mShortAxisCells; - - final boolean[][] occupied = mOccupied; - findOccupiedCells(xCount, yCount, occupied, null); - - findIntersectingVacantCells(info, info.cellX, info.cellY, xCount, yCount, occupied); - - mDirtyTag = false; - } - return info; - } - - private static void findIntersectingVacantCells(CellInfo cellInfo, int x, int y, - int xCount, int yCount, boolean[][] occupied) { - - cellInfo.maxVacantSpanX = Integer.MIN_VALUE; - cellInfo.maxVacantSpanXSpanY = Integer.MIN_VALUE; - cellInfo.maxVacantSpanY = Integer.MIN_VALUE; - cellInfo.maxVacantSpanYSpanX = Integer.MIN_VALUE; - cellInfo.clearVacantCells(); - - if (occupied[x][y]) { - return; - } - - cellInfo.current.set(x, y, x, y); - - findVacantCell(cellInfo.current, xCount, yCount, occupied, cellInfo); - } - - private static void findVacantCell(Rect current, int xCount, int yCount, boolean[][] occupied, - CellInfo cellInfo) { - - addVacantCell(current, cellInfo); - - if (current.left > 0) { - if (isColumnEmpty(current.left - 1, current.top, current.bottom, occupied)) { - current.left--; - findVacantCell(current, xCount, yCount, occupied, cellInfo); - current.left++; - } - } - - if (current.right < xCount - 1) { - if (isColumnEmpty(current.right + 1, current.top, current.bottom, occupied)) { - current.right++; - findVacantCell(current, xCount, yCount, occupied, cellInfo); - current.right--; - } - } - - if (current.top > 0) { - if (isRowEmpty(current.top - 1, current.left, current.right, occupied)) { - current.top--; - findVacantCell(current, xCount, yCount, occupied, cellInfo); - current.top++; - } - } - - if (current.bottom < yCount - 1) { - if (isRowEmpty(current.bottom + 1, current.left, current.right, occupied)) { - current.bottom++; - findVacantCell(current, xCount, yCount, occupied, cellInfo); - current.bottom--; - } - } - } - - private static void addVacantCell(Rect current, CellInfo cellInfo) { - CellInfo.VacantCell cell = CellInfo.VacantCell.acquire(); - cell.cellX = current.left; - cell.cellY = current.top; - cell.spanX = current.right - current.left + 1; - cell.spanY = current.bottom - current.top + 1; - if (cell.spanX > cellInfo.maxVacantSpanX) { - cellInfo.maxVacantSpanX = cell.spanX; - cellInfo.maxVacantSpanXSpanY = cell.spanY; - } - if (cell.spanY > cellInfo.maxVacantSpanY) { - cellInfo.maxVacantSpanY = cell.spanY; - cellInfo.maxVacantSpanYSpanX = cell.spanX; - } - cellInfo.vacantCells.add(cell); - } - - private static boolean isColumnEmpty(int x, int top, int bottom, boolean[][] occupied) { - for (int y = top; y <= bottom; y++) { - if (occupied[x][y]) { - return false; - } - } - return true; + return (CellInfo) super.getTag(); } + /** + * Check if the row 'y' is empty from columns 'left' to 'right', inclusive. + */ private static boolean isRowEmpty(int y, int left, int right, boolean[][] occupied) { for (int x = left; x <= right; x++) { if (occupied[x][y]) { @@ -342,79 +648,28 @@ public class CellLayout extends ViewGroup { return true; } - CellInfo findAllVacantCells(boolean[] occupiedCells, View ignoreView) { - final boolean portrait = mPortrait; - final int xCount = portrait ? mShortAxisCells : mLongAxisCells; - final int yCount = portrait ? mLongAxisCells : mShortAxisCells; - - boolean[][] occupied = mOccupied; - - if (occupiedCells != null) { - for (int y = 0; y < yCount; y++) { - for (int x = 0; x < xCount; x++) { - occupied[x][y] = occupiedCells[y * xCount + x]; - } - } - } else { - findOccupiedCells(xCount, yCount, occupied, ignoreView); - } - - CellInfo cellInfo = new CellInfo(); - - cellInfo.cellX = -1; - cellInfo.cellY = -1; - cellInfo.spanY = 0; - cellInfo.spanX = 0; - cellInfo.maxVacantSpanX = Integer.MIN_VALUE; - cellInfo.maxVacantSpanXSpanY = Integer.MIN_VALUE; - cellInfo.maxVacantSpanY = Integer.MIN_VALUE; - cellInfo.maxVacantSpanYSpanX = Integer.MIN_VALUE; - cellInfo.screen = mCellInfo.screen; - - Rect current = cellInfo.current; - - for (int x = 0; x < xCount; x++) { - for (int y = 0; y < yCount; y++) { - if (!occupied[x][y]) { - current.set(x, y, x, y); - findVacantCell(current, xCount, yCount, occupied, cellInfo); - occupied[x][y] = true; - } - } - } - - cellInfo.valid = cellInfo.vacantCells.size() > 0; - - // Assume the caller will perform their own cell searching, otherwise we - // risk causing an unnecessary rebuild after findCellForSpan() - - return cellInfo; - } - /** - * Given a point, return the cell that strictly encloses that point + * Given a point, return the cell that strictly encloses that point * @param x X coordinate of the point * @param y Y coordinate of the point * @param result Array of 2 ints to hold the x and y coordinate of the cell */ void pointToCellExact(int x, int y, int[] result) { - final boolean portrait = mPortrait; - - final int hStartPadding = portrait ? mShortAxisStartPadding : mLongAxisStartPadding; - final int vStartPadding = portrait ? mLongAxisStartPadding : mShortAxisStartPadding; + final int hStartPadding = getLeftPadding(); + final int vStartPadding = getTopPadding(); result[0] = (x - hStartPadding) / (mCellWidth + mWidthGap); result[1] = (y - vStartPadding) / (mCellHeight + mHeightGap); - final int xAxis = portrait ? mShortAxisCells : mLongAxisCells; - final int yAxis = portrait ? mLongAxisCells : mShortAxisCells; + final int xAxis = mCountX; + final int yAxis = mCountY; if (result[0] < 0) result[0] = 0; if (result[0] >= xAxis) result[0] = xAxis - 1; if (result[1] < 0) result[1] = 0; if (result[1] >= yAxis) result[1] = yAxis - 1; } - + /** * Given a point, return the cell that most closely encloses that point * @param x X coordinate of the point @@ -427,18 +682,15 @@ public class CellLayout extends ViewGroup { /** * Given a cell coordinate, return the point that represents the upper left corner of that cell - * - * @param cellX X coordinate of the cell + * + * @param cellX X coordinate of the cell * @param cellY Y coordinate of the cell - * + * * @param result Array of 2 ints to hold the x and y coordinate of the point */ void cellToPoint(int cellX, int cellY, int[] result) { - final boolean portrait = mPortrait; - - final int hStartPadding = portrait ? mShortAxisStartPadding : mLongAxisStartPadding; - final int vStartPadding = portrait ? mLongAxisStartPadding : mShortAxisStartPadding; - + final int hStartPadding = getLeftPadding(); + final int vStartPadding = getTopPadding(); result[0] = hStartPadding + cellX * (mCellWidth + mWidthGap); result[1] = vStartPadding + cellY * (mCellHeight + mHeightGap); @@ -453,101 +705,76 @@ public class CellLayout extends ViewGroup { } int getLeftPadding() { - return mPortrait ? mShortAxisStartPadding : mLongAxisStartPadding; + return mLeftPadding; } int getTopPadding() { - return mPortrait ? mLongAxisStartPadding : mShortAxisStartPadding; + return mTopPadding; } int getRightPadding() { - return mPortrait ? mShortAxisEndPadding : mLongAxisEndPadding; + return mRightPadding; } int getBottomPadding() { - return mPortrait ? mLongAxisEndPadding : mShortAxisEndPadding; + return mBottomPadding; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // TODO: currently ignoring padding - + int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); - int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); - + int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); - + if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) { throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions"); } - final int shortAxisCells = mShortAxisCells; - final int longAxisCells = mLongAxisCells; - final int longAxisStartPadding = mLongAxisStartPadding; - final int longAxisEndPadding = mLongAxisEndPadding; - final int shortAxisStartPadding = mShortAxisStartPadding; - final int shortAxisEndPadding = mShortAxisEndPadding; final int cellWidth = mCellWidth; final int cellHeight = mCellHeight; - mPortrait = heightSpecSize > widthSpecSize; + int numWidthGaps = mCountX - 1; + int numHeightGaps = mCountY - 1; - int numShortGaps = shortAxisCells - 1; - int numLongGaps = longAxisCells - 1; + if (mWidthGap < 0 || mHeightGap < 0) { + int vSpaceLeft = heightSpecSize - mTopPadding - mBottomPadding - (cellHeight * mCountY); + mHeightGap = vSpaceLeft / numHeightGaps; - if (mPortrait) { - int vSpaceLeft = heightSpecSize - longAxisStartPadding - longAxisEndPadding - - (cellHeight * longAxisCells); - mHeightGap = vSpaceLeft / numLongGaps; + int hSpaceLeft = widthSpecSize - mLeftPadding - mRightPadding - (cellWidth * mCountX); + mWidthGap = hSpaceLeft / numWidthGaps; - int hSpaceLeft = widthSpecSize - shortAxisStartPadding - shortAxisEndPadding - - (cellWidth * shortAxisCells); - if (numShortGaps > 0) { - mWidthGap = hSpaceLeft / numShortGaps; - } else { - mWidthGap = 0; - } - } else { - int hSpaceLeft = widthSpecSize - longAxisStartPadding - longAxisEndPadding - - (cellWidth * longAxisCells); - mWidthGap = hSpaceLeft / numLongGaps; - - int vSpaceLeft = heightSpecSize - shortAxisStartPadding - shortAxisEndPadding - - (cellHeight * shortAxisCells); - if (numShortGaps > 0) { - mHeightGap = vSpaceLeft / numShortGaps; - } else { - mHeightGap = 0; - } + // center it around the min gaps + int minGap = Math.min(mWidthGap, mHeightGap); + mWidthGap = mHeightGap = minGap; } - + int count = getChildCount(); for (int i = 0; i < count; i++) { View child = getChildAt(i); LayoutParams lp = (LayoutParams) child.getLayoutParams(); - - if (mPortrait) { - lp.setup(cellWidth, cellHeight, mWidthGap, mHeightGap, shortAxisStartPadding, - longAxisStartPadding); - } else { - lp.setup(cellWidth, cellHeight, mWidthGap, mHeightGap, longAxisStartPadding, - shortAxisStartPadding); - } - - if (lp.regenerateId) { - child.setId(((getId() & 0xFF) << 16) | (lp.cellX & 0xFF) << 8 | (lp.cellY & 0xFF)); - lp.regenerateId = false; - } + lp.setup(cellWidth, cellHeight, mWidthGap, mHeightGap, + mLeftPadding, mTopPadding); int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); - int childheightMeasureSpec = - MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); + int childheightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, + MeasureSpec.EXACTLY); + child.measure(childWidthMeasureSpec, childheightMeasureSpec); } - - setMeasuredDimension(widthSpecSize, heightSpecSize); + if (widthSpecMode == MeasureSpec.AT_MOST) { + int newWidth = mLeftPadding + mRightPadding + (mCountX * cellWidth) + + ((mCountX - 1) * mWidthGap); + int newHeight = mTopPadding + mBottomPadding + (mCountY * cellHeight) + + ((mCountY - 1) * mHeightGap); + setMeasuredDimension(newWidth, newHeight); + } else if (widthSpecMode == MeasureSpec.EXACTLY) { + setMeasuredDimension(widthSpecSize, heightSpecSize); + } } @Override @@ -555,7 +782,7 @@ public class CellLayout extends ViewGroup { int count = getChildCount(); for (int i = 0; i < count; i++) { - View child = getChildAt(i); + final View child = getChildAt(i); if (child.getVisibility() != GONE) { CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); @@ -567,24 +794,35 @@ public class CellLayout extends ViewGroup { if (lp.dropped) { lp.dropped = false; - final int[] cellXY = mCellXY; + final int[] cellXY = mTmpCellXY; getLocationOnScreen(cellXY); mWallpaperManager.sendWallpaperCommand(getWindowToken(), "android.home.drop", cellXY[0] + childLeft + lp.width / 2, cellXY[1] + childTop + lp.height / 2, 0, null); + + ((Workspace) mParent).animateViewIntoPosition(child); } } } } @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mBackgroundRect.set(0, 0, w, h); + updateHoverRect(); + } + + @Override protected void setChildrenDrawingCacheEnabled(boolean enabled) { final int count = getChildCount(); for (int i = 0; i < count; i++) { final View view = getChildAt(i); view.setDrawingCacheEnabled(enabled); // Update the drawing caches - view.buildDrawingCache(true); + if (!view.isHardwareAccelerated()) { + view.buildDrawingCache(true); + } } } @@ -593,154 +831,475 @@ public class CellLayout extends ViewGroup { super.setChildrenDrawnWithCacheEnabled(enabled); } + public float getBackgroundAlpha() { + return mBackgroundAlpha; + } + + public void setBackgroundAlphaMultiplier(float multiplier) { + mBackgroundAlphaMultiplier = multiplier; + } + + public float getBackgroundAlphaMultiplier() { + return mBackgroundAlphaMultiplier; + } + + 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); + } + + private void setChildrenAlpha(float alpha) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + getChildAt(i).setAlpha(alpha); + } + } + + private boolean isVacantIgnoring( + int originX, int originY, int spanX, int spanY, View ignoreView) { + if (ignoreView != null) { + markCellsAsUnoccupiedForView(ignoreView); + } + boolean isVacant = true; + for (int i = 0; i < spanY; i++) { + if (!isRowEmpty(originY + i, originX, originX + spanX - 1, mOccupied)) { + isVacant = false; + break; + } + } + if (ignoreView != null) { + markCellsAsOccupiedForView(ignoreView); + } + return isVacant; + } + + private boolean isVacant(int originX, int originY, int spanX, int spanY) { + return isVacantIgnoring(originX, originY, spanX, spanY, null); + } + + public View getChildAt(int x, int y) { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if ((lp.cellX <= x) && (x < lp.cellX + lp.cellHSpan) && + (lp.cellY <= y) && (y < lp.cellY + lp.cellHSpan)) { + return child; + } + } + return null; + } + + /** + * Estimate the size that a child with the given dimensions will take in the layout. + */ + void estimateChildSize(int minWidth, int minHeight, int[] result) { + // Assuming it's placed at 0, 0, find where the bottom right cell will land + rectToCell(minWidth, minHeight, result); + + // Then figure out the rect it will occupy + cellToRect(0, 0, result[0], result[1], mRectF); + result[0] = (int)mRectF.width(); + result[1] = (int)mRectF.height(); + } + + /** + * Estimate where the top left cell of the dragged item will land if it is dropped. + * + * @param originX The X value of the top left corner of the item + * @param originY The Y value of the top left corner of the item + * @param spanX The number of horizontal cells that the item spans + * @param spanY The number of vertical cells that the item spans + * @param result The estimated drop cell X and Y. + */ + void estimateDropCell(int originX, int originY, int spanX, int spanY, int[] result) { + final int countX = mCountX; + final int countY = mCountY; + + // pointToCellRounded takes the top left of a cell but will pad that with + // cellWidth/2 and cellHeight/2 when finding the matching cell + pointToCellRounded(originX, originY, result); + + // If the item isn't fully on this screen, snap to the edges + int rightOverhang = result[0] + spanX - countX; + if (rightOverhang > 0) { + result[0] -= rightOverhang; // Snap to right + } + result[0] = Math.max(0, result[0]); // Snap to left + int bottomOverhang = result[1] + spanY - countY; + if (bottomOverhang > 0) { + result[1] -= bottomOverhang; // Snap to bottom + } + result[1] = Math.max(0, result[1]); // Snap to top + } + + void visualizeDropLocation( + View v, Bitmap dragOutline, int originX, int originY, int spanX, int spanY) { + + final int oldDragCellX = mDragCell[0]; + final int oldDragCellY = mDragCell[1]; + final int[] nearest = findNearestVacantArea(originX, originY, spanX, spanY, v, mDragCell); + if (v != null) { + mDragCenter.set(originX + (v.getWidth() / 2), originY + (v.getHeight() / 2)); + } else { + mDragCenter.set(originX, originY); + } + + if (nearest != null && (nearest[0] != oldDragCellX || nearest[1] != oldDragCellY)) { + // Find the top left corner of the rect the object will occupy + final int[] topLeft = mTmpPoint; + cellToPoint(nearest[0], nearest[1], topLeft); + + int left = topLeft[0]; + int top = topLeft[1]; + + if (v != null) { + if (v.getParent() instanceof CellLayout) { + LayoutParams lp = (LayoutParams) v.getLayoutParams(); + left += lp.leftMargin; + top += lp.topMargin; + } + + // Offsets due to the size difference between the View and the dragOutline + left += (v.getWidth() - dragOutline.getWidth()) / 2; + top += (v.getHeight() - dragOutline.getHeight()) / 2; + } + + final int oldIndex = mDragOutlineCurrent; + mDragOutlineAnims[oldIndex].animateOut(); + mDragOutlineCurrent = (oldIndex + 1) % mDragOutlines.length; + + mDragOutlines[mDragOutlineCurrent].set(left, top); + mDragOutlineAnims[mDragOutlineCurrent].setTag(dragOutline); + mDragOutlineAnims[mDragOutlineCurrent].animateIn(); + } + + // If we are drawing crosshairs, the entire CellLayout needs to be invalidated + if (mCrosshairsDrawable != null) { + invalidate(); + } + } + + /** + * Find a vacant area that will fit the given bounds nearest the requested + * cell location. Uses Euclidean distance to score multiple vacant areas. + * + * @param pixelX The X location at which you want to search for a vacant area. + * @param pixelY The Y location at which you want to search for a vacant area. + * @param spanX Horizontal span of the object. + * @param spanY Vertical span of the object. + * @param result Array in which to place the result, or null (in which case a new array will + * be allocated) + * @return The X, Y cell of a vacant area that can contain this object, + * nearest the requested location. + */ + int[] findNearestVacantArea( + int pixelX, int pixelY, int spanX, int spanY, int[] result) { + return findNearestVacantArea(pixelX, pixelY, spanX, spanY, null, result); + } + /** * Find a vacant area that will fit the given bounds nearest the requested * cell location. Uses Euclidean distance to score multiple vacant areas. - * + * * @param pixelX The X location at which you want to search for a vacant area. * @param pixelY The Y location at which you want to search for a vacant area. * @param spanX Horizontal span of the object. * @param spanY Vertical span of the object. - * @param vacantCells Pre-computed set of vacant cells to search. - * @param recycle Previously returned value to possibly recycle. + * @param ignoreView Considers space occupied by this view as unoccupied + * @param result Previously returned value to possibly recycle. * @return The X, Y cell of a vacant area that can contain this object, * nearest the requested location. */ - int[] findNearestVacantArea(int pixelX, int pixelY, int spanX, int spanY, - CellInfo vacantCells, int[] recycle) { - + int[] findNearestVacantArea( + int pixelX, int pixelY, int spanX, int spanY, View ignoreView, int[] result) { + // mark space take by ignoreView as available (method checks if ignoreView is null) + markCellsAsUnoccupiedForView(ignoreView); + // Keep track of best-scoring drop area - final int[] bestXY = recycle != null ? recycle : new int[2]; - final int[] cellXY = mCellXY; + final int[] bestXY = result != null ? result : new int[2]; double bestDistance = Double.MAX_VALUE; - - // Bail early if vacant cells aren't valid - if (!vacantCells.valid) { - return null; - } - // Look across all vacant cells for best fit - final int size = vacantCells.vacantCells.size(); - for (int i = 0; i < size; i++) { - final CellInfo.VacantCell cell = vacantCells.vacantCells.get(i); - - // Reject if vacant cell isn't our exact size - if (cell.spanX != spanX || cell.spanY != spanY) { - continue; - } - - // Score is center distance from requested pixel - cellToPoint(cell.cellX, cell.cellY, cellXY); - - double distance = Math.sqrt(Math.pow(cellXY[0] - pixelX, 2) + - Math.pow(cellXY[1] - pixelY, 2)); - if (distance <= bestDistance) { - bestDistance = distance; - bestXY[0] = cell.cellX; - bestXY[1] = cell.cellY; + final int countX = mCountX; + final int countY = mCountY; + final boolean[][] occupied = mOccupied; + + for (int y = 0; y < countY - (spanY - 1); y++) { + inner: + for (int x = 0; x < countX - (spanX - 1); x++) { + for (int i = 0; i < spanX; i++) { + for (int j = 0; j < spanY; j++) { + if (occupied[x + i][y + j]) { + // small optimization: we can skip to after the column we just found + // an occupied cell + x += i; + continue inner; + } + } + } + final int[] cellXY = mTmpCellXY; + cellToPoint(x, y, cellXY); + + double distance = Math.sqrt(Math.pow(cellXY[0] - pixelX, 2) + + Math.pow(cellXY[1] - pixelY, 2)); + if (distance <= bestDistance) { + bestDistance = distance; + bestXY[0] = x; + bestXY[1] = y; + } } } + // re-mark space taken by ignoreView as occupied + markCellsAsOccupiedForView(ignoreView); - // Return null if no suitable location found + // Return null if no suitable location found if (bestDistance < Double.MAX_VALUE) { return bestXY; } else { return null; } } - + + boolean existsEmptyCell() { + return findCellForSpan(null, 1, 1); + } + + /** + * Finds the upper-left coordinate of the first rectangle in the grid that can + * hold a cell of the specified dimensions. If intersectX and intersectY are not -1, + * then this method will only return coordinates for rectangles that contain the cell + * (intersectX, intersectY) + * + * @param cellXY The array that will contain the position of a vacant cell if such a cell + * can be found. + * @param spanX The horizontal span of the cell we want to find. + * @param spanY The vertical span of the cell we want to find. + * + * @return True if a vacant cell of the specified dimension was found, false otherwise. + */ + boolean findCellForSpan(int[] cellXY, int spanX, int spanY) { + return findCellForSpanThatIntersectsIgnoring(cellXY, spanX, spanY, -1, -1, null); + } + + /** + * Like above, but ignores any cells occupied by the item "ignoreView" + * + * @param cellXY The array that will contain the position of a vacant cell if such a cell + * can be found. + * @param spanX The horizontal span of the cell we want to find. + * @param spanY The vertical span of the cell we want to find. + * @param ignoreView The home screen item we should treat as not occupying any space + * @return + */ + boolean findCellForSpanIgnoring(int[] cellXY, int spanX, int spanY, View ignoreView) { + return findCellForSpanThatIntersectsIgnoring(cellXY, spanX, spanY, -1, -1, ignoreView); + } + + /** + * Like above, but if intersectX and intersectY are not -1, then this method will try to + * return coordinates for rectangles that contain the cell [intersectX, intersectY] + * + * @param spanX The horizontal span of the cell we want to find. + * @param spanY The vertical span of the cell we want to find. + * @param ignoreView The home screen item we should treat as not occupying any space + * @param intersectX The X coordinate of the cell that we should try to overlap + * @param intersectX The Y coordinate of the cell that we should try to overlap + * + * @return True if a vacant cell of the specified dimension was found, false otherwise. + */ + boolean findCellForSpanThatIntersects(int[] cellXY, int spanX, int spanY, + int intersectX, int intersectY) { + return findCellForSpanThatIntersectsIgnoring( + cellXY, spanX, spanY, intersectX, intersectY, null); + } + + /** + * The superset of the above two methods + */ + boolean findCellForSpanThatIntersectsIgnoring(int[] cellXY, int spanX, int spanY, + int intersectX, int intersectY, View ignoreView) { + // mark space take by ignoreView as available (method checks if ignoreView is null) + markCellsAsUnoccupiedForView(ignoreView); + + boolean foundCell = false; + while (true) { + int startX = 0; + if (intersectX >= 0) { + startX = Math.max(startX, intersectX - (spanX - 1)); + } + int endX = mCountX - (spanX - 1); + if (intersectX >= 0) { + endX = Math.min(endX, intersectX + (spanX - 1) + (spanX == 1 ? 1 : 0)); + } + int startY = 0; + if (intersectY >= 0) { + startY = Math.max(startY, intersectY - (spanY - 1)); + } + int endY = mCountY - (spanY - 1); + if (intersectY >= 0) { + endY = Math.min(endY, intersectY + (spanY - 1) + (spanY == 1 ? 1 : 0)); + } + + for (int y = startY; y < endY && !foundCell; y++) { + inner: + for (int x = startX; x < endX; x++) { + for (int i = 0; i < spanX; i++) { + for (int j = 0; j < spanY; j++) { + if (mOccupied[x + i][y + j]) { + // small optimization: we can skip to after the column we just found + // an occupied cell + x += i; + continue inner; + } + } + } + if (cellXY != null) { + cellXY[0] = x; + cellXY[1] = y; + } + foundCell = true; + break; + } + } + if (intersectX == -1 && intersectY == -1) { + break; + } else { + // if we failed to find anything, try again but without any requirements of + // intersecting + intersectX = -1; + intersectY = -1; + continue; + } + } + + // re-mark space taken by ignoreView as occupied + markCellsAsOccupiedForView(ignoreView); + return foundCell; + } + + /** + * Called when drag has left this CellLayout or has been completed (successfully or not) + */ + void onDragExit() { + // This can actually be called when we aren't in a drag, e.g. when adding a new + // item to this layout via the customize drawer. + // Guard against that case. + if (mDragging) { + mDragging = false; + + // Fade out the drag indicators + if (mCrosshairsAnimator != null) { + mCrosshairsAnimator.animateOut(); + } + } + + // Invalidate the drag data + mDragCell[0] = -1; + mDragCell[1] = -1; + mDragOutlineAnims[mDragOutlineCurrent].animateOut(); + mDragOutlineCurrent = (mDragOutlineCurrent + 1) % mDragOutlineAnims.length; + + setHover(false); + } + /** - * Drop a child at the specified position + * Mark a child as having been dropped. + * At the beginning of the drag operation, the child may have been on another + * screen, but it is re-parented before this method is called. * * @param child The child that is being dropped - * @param targetXY Destination area to move to */ - void onDropChild(View child, int[] targetXY) { + void onDropChild(View child) { if (child != null) { LayoutParams lp = (LayoutParams) child.getLayoutParams(); - lp.cellX = targetXY[0]; - lp.cellY = targetXY[1]; lp.isDragging = false; lp.dropped = true; - mDragRect.setEmpty(); + child.setVisibility(View.VISIBLE); child.requestLayout(); - invalidate(); - } - } - - void onDropAborted(View child) { - if (child != null) { - ((LayoutParams) child.getLayoutParams()).isDragging = false; - invalidate(); } - mDragRect.setEmpty(); } /** * Start dragging the specified child - * + * * @param child The child that is being dragged */ void onDragChild(View child) { LayoutParams lp = (LayoutParams) child.getLayoutParams(); lp.isDragging = true; - mDragRect.setEmpty(); } - + /** - * Drag a child over the specified position - * - * @param child The child that is being dropped - * @param cellX The child's new x cell location - * @param cellY The child's new y cell location + * A drag event has begun over this layout. + * It may have begun over this layout (in which case onDragChild is called first), + * or it may have begun on another layout. */ - void onDragOverChild(View child, int cellX, int cellY) { - int[] cellXY = mCellXY; - pointToCellRounded(cellX, cellY, cellXY); - LayoutParams lp = (LayoutParams) child.getLayoutParams(); - cellToRect(cellXY[0], cellXY[1], lp.cellHSpan, lp.cellVSpan, mDragRect); - invalidate(); + void onDragEnter() { + if (!mDragging) { + // Fade in the drag indicators + if (mCrosshairsAnimator != null) { + mCrosshairsAnimator.animateIn(); + } + } + mDragging = true; } - + /** * Computes a bounding rectangle for a range of cells - * + * * @param cellX X coordinate of upper left corner expressed as a cell position * @param cellY Y coordinate of upper left corner expressed as a cell position - * @param cellHSpan Width in cells + * @param cellHSpan Width in cells * @param cellVSpan Height in cells - * @param dragRect Rectnagle into which to put the results + * @param resultRect Rect into which to put the results */ - public void cellToRect(int cellX, int cellY, int cellHSpan, int cellVSpan, RectF dragRect) { - final boolean portrait = mPortrait; + public void cellToRect(int cellX, int cellY, int cellHSpan, int cellVSpan, RectF resultRect) { final int cellWidth = mCellWidth; final int cellHeight = mCellHeight; final int widthGap = mWidthGap; final int heightGap = mHeightGap; - - final int hStartPadding = portrait ? mShortAxisStartPadding : mLongAxisStartPadding; - final int vStartPadding = portrait ? mLongAxisStartPadding : mShortAxisStartPadding; - + + final int hStartPadding = getLeftPadding(); + final int vStartPadding = getTopPadding(); + int width = cellHSpan * cellWidth + ((cellHSpan - 1) * widthGap); int height = cellVSpan * cellHeight + ((cellVSpan - 1) * heightGap); int x = hStartPadding + cellX * (cellWidth + widthGap); int y = vStartPadding + cellY * (cellHeight + heightGap); - - dragRect.set(x, y, x + width, y + height); + + resultRect.set(x, y, x + width, y + height); } - + /** - * Computes the required horizontal and vertical cell spans to always + * Computes the required horizontal and vertical cell spans to always * fit the given rectangle. - * + * * @param width Width in pixels * @param height Height in pixels + * @param result An array of length 2 in which to store the result (may be null). */ - public int[] rectToCell(int width, int height) { + public int[] rectToCell(int width, int height, int[] result) { + return rectToCell(getResources(), width, height, result); + } + + public static int[] rectToCell(Resources resources, int width, int height, int[] result) { // Always assume we're working with the smallest span to make sure we // reserve enough space in both orientations. - final Resources resources = getResources(); int actualWidth = resources.getDimensionPixelSize(R.dimen.workspace_cell_width); int actualHeight = resources.getDimensionPixelSize(R.dimen.workspace_cell_height); int smallerSize = Math.min(actualWidth, actualHeight); @@ -749,7 +1308,12 @@ public class CellLayout extends ViewGroup { int spanX = (width + smallerSize) / smallerSize; int spanY = (height + smallerSize) / smallerSize; - return new int[] { spanX, spanY }; + if (result == null) { + return new int[] { spanX, spanY }; + } + result[0] = spanX; + result[1] = spanY; + return result; } /** @@ -758,18 +1322,12 @@ public class CellLayout extends ViewGroup { * @param vacant Holds the x and y coordinate of the vacant cell * @param spanX Horizontal cell span. * @param spanY Vertical cell span. - * + * * @return True if a vacant cell was found */ public boolean getVacantCell(int[] vacant, int spanX, int spanY) { - final boolean portrait = mPortrait; - final int xCount = portrait ? mShortAxisCells : mLongAxisCells; - final int yCount = portrait ? mLongAxisCells : mShortAxisCells; - final boolean[][] occupied = mOccupied; - - findOccupiedCells(xCount, yCount, occupied, null); - return findVacantCell(vacant, spanX, spanY, xCount, yCount, occupied); + return findVacantCell(vacant, spanX, spanY, mCountX, mCountY, mOccupied); } static boolean findVacantCell(int[] vacant, int spanX, int spanY, @@ -796,43 +1354,36 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { return false; } - boolean[] getOccupiedCells() { - final boolean portrait = mPortrait; - final int xCount = portrait ? mShortAxisCells : mLongAxisCells; - final int yCount = portrait ? mLongAxisCells : mShortAxisCells; - final boolean[][] occupied = mOccupied; - - findOccupiedCells(xCount, yCount, occupied, null); - - final boolean[] flat = new boolean[xCount * yCount]; - for (int y = 0; y < yCount; y++) { - for (int x = 0; x < xCount; x++) { - flat[y * xCount + x] = occupied[x][y]; + private void clearOccupiedCells() { + for (int x = 0; x < mCountX; x++) { + for (int y = 0; y < mCountY; y++) { + mOccupied[x][y] = false; } } + } - return flat; + public void onMove(View view, int newCellX, int newCellY) { + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + markCellsAsUnoccupiedForView(view); + markCellsForView(newCellX, newCellY, lp.cellHSpan, lp.cellVSpan, true); } - private void findOccupiedCells(int xCount, int yCount, boolean[][] occupied, View ignoreView) { - for (int x = 0; x < xCount; x++) { - for (int y = 0; y < yCount; y++) { - occupied[x][y] = false; - } - } + private void markCellsAsOccupiedForView(View view) { + if (view == null || view.getParent() != this) return; + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, true); + } - int count = getChildCount(); - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - if (child instanceof Folder || child.equals(ignoreView)) { - continue; - } - LayoutParams lp = (LayoutParams) child.getLayoutParams(); + private void markCellsAsUnoccupiedForView(View view) { + if (view == null || view.getParent() != this) return; + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, false); + } - for (int x = lp.cellX; x < lp.cellX + lp.cellHSpan && x < xCount; x++) { - for (int y = lp.cellY; y < lp.cellY + lp.cellVSpan && y < yCount; y++) { - occupied[x][y] = true; - } + private void markCellsForView(int cellX, int cellY, int spanX, int spanY, boolean value) { + for (int x = cellX; x < cellX + spanX && x < mCountX; x++) { + for (int y = cellY; y < cellY + spanY && y < mCountY; y++) { + mOccupied[x][y] = value; } } } @@ -852,6 +1403,17 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { return new CellLayout.LayoutParams(p); } + public static class CellLayoutAnimationController extends LayoutAnimationController { + public CellLayoutAnimationController(Animation animation, float delay) { + super(animation, delay); + } + + @Override + protected long getDelayForView(View view) { + return (int) (Math.random() * 150); + } + } + public static class LayoutParams extends ViewGroup.MarginLayoutParams { /** * Horizontal location of the item in the grid. @@ -876,7 +1438,7 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { */ @ViewDebug.ExportedProperty public int cellVSpan; - + /** * Is this item currently being dragged */ @@ -889,8 +1451,18 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { @ViewDebug.ExportedProperty int y; - boolean regenerateId; - + /** + * The old X coordinate of this item, relative to its current parent. + * Used to animate the item into its new position. + */ + int oldX; + + /** + * The old Y coordinate of this item, relative to its current parent. + * Used to animate the item into its new position. + */ + int oldY; + boolean dropped; public LayoutParams(Context c, AttributeSet attrs) { @@ -904,7 +1476,15 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { cellHSpan = 1; cellVSpan = 1; } - + + public LayoutParams(LayoutParams source) { + super(source); + this.cellX = source.cellX; + this.cellY = source.cellY; + this.cellHSpan = source.cellHSpan; + this.cellVSpan = source.cellVSpan; + } + public LayoutParams(int cellX, int cellY, int cellHSpan, int cellVSpan) { super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); this.cellX = cellX; @@ -915,12 +1495,12 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { public void setup(int cellWidth, int cellHeight, int widthGap, int heightGap, int hStartPadding, int vStartPadding) { - + final int myCellHSpan = cellHSpan; final int myCellVSpan = cellVSpan; final int myCellX = cellX; final int myCellY = cellY; - + width = myCellHSpan * cellWidth + ((myCellHSpan - 1) * widthGap) - leftMargin - rightMargin; height = myCellVSpan * cellHeight + ((myCellVSpan - 1) * heightGap) - @@ -929,172 +1509,31 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { x = hStartPadding + myCellX * (cellWidth + widthGap) + leftMargin; y = vStartPadding + myCellY * (cellHeight + heightGap) + topMargin; } - } - - static final class CellInfo implements ContextMenu.ContextMenuInfo { - /** - * See View.AttachInfo.InvalidateInfo for futher explanations about - * the recycling mechanism. In this case, we recycle the vacant cells - * instances because up to several hundreds can be instanciated when - * the user long presses an empty cell. - */ - static final class VacantCell { - int cellX; - int cellY; - int spanX; - int spanY; - - // We can create up to 523 vacant cells on a 4x4 grid, 100 seems - // like a reasonable compromise given the size of a VacantCell and - // the fact that the user is not likely to touch an empty 4x4 grid - // very often - private static final int POOL_LIMIT = 100; - private static final Object sLock = new Object(); - - private static int sAcquiredCount = 0; - private static VacantCell sRoot; - - private VacantCell next; - - static VacantCell acquire() { - synchronized (sLock) { - if (sRoot == null) { - return new VacantCell(); - } - - VacantCell info = sRoot; - sRoot = info.next; - sAcquiredCount--; - - return info; - } - } - void release() { - synchronized (sLock) { - if (sAcquiredCount < POOL_LIMIT) { - sAcquiredCount++; - next = sRoot; - sRoot = this; - } - } - } - - @Override - public String toString() { - return "VacantCell[x=" + cellX + ", y=" + cellY + ", spanX=" + spanX + - ", spanY=" + spanY + "]"; - } + public String toString() { + return "(" + this.cellX + ", " + this.cellY + ")"; } + } + // This class stores info for two purposes: + // 1. When dragging items (mDragInfo in Workspace), we store the View, its cellX & cellY, + // its spanX, spanY, and the screen it is on + // 2. When long clicking on an empty cell in a CellLayout, we save information about the + // cellX and cellY coordinates and which page was clicked. We then set this as a tag on + // the CellLayout that was long clicked + static final class CellInfo implements ContextMenu.ContextMenuInfo { View cell; - int cellX; - int cellY; + int cellX = -1; + int cellY = -1; int spanX; int spanY; int screen; boolean valid; - final ArrayList<VacantCell> vacantCells = new ArrayList<VacantCell>(VacantCell.POOL_LIMIT); - int maxVacantSpanX; - int maxVacantSpanXSpanY; - int maxVacantSpanY; - int maxVacantSpanYSpanX; - final Rect current = new Rect(); - - void clearVacantCells() { - final ArrayList<VacantCell> list = vacantCells; - final int count = list.size(); - - for (int i = 0; i < count; i++) list.get(i).release(); - - list.clear(); - } - - void findVacantCellsFromOccupied(boolean[] occupied, int xCount, int yCount) { - if (cellX < 0 || cellY < 0) { - maxVacantSpanX = maxVacantSpanXSpanY = Integer.MIN_VALUE; - maxVacantSpanY = maxVacantSpanYSpanX = Integer.MIN_VALUE; - clearVacantCells(); - return; - } - - final boolean[][] unflattened = new boolean[xCount][yCount]; - for (int y = 0; y < yCount; y++) { - for (int x = 0; x < xCount; x++) { - unflattened[x][y] = occupied[y * xCount + x]; - } - } - CellLayout.findIntersectingVacantCells(this, cellX, cellY, xCount, yCount, unflattened); - } - - /** - * This method can be called only once! Calling #findVacantCellsFromOccupied will - * restore the ability to call this method. - * - * Finds the upper-left coordinate of the first rectangle in the grid that can - * hold a cell of the specified dimensions. - * - * @param cellXY The array that will contain the position of a vacant cell if such a cell - * can be found. - * @param spanX The horizontal span of the cell we want to find. - * @param spanY The vertical span of the cell we want to find. - * - * @return True if a vacant cell of the specified dimension was found, false otherwise. - */ - boolean findCellForSpan(int[] cellXY, int spanX, int spanY) { - return findCellForSpan(cellXY, spanX, spanY, true); - } - - boolean findCellForSpan(int[] cellXY, int spanX, int spanY, boolean clear) { - final ArrayList<VacantCell> list = vacantCells; - final int count = list.size(); - - boolean found = false; - - if (this.spanX >= spanX && this.spanY >= spanY) { - cellXY[0] = cellX; - cellXY[1] = cellY; - found = true; - } - - // Look for an exact match first - for (int i = 0; i < count; i++) { - VacantCell cell = list.get(i); - if (cell.spanX == spanX && cell.spanY == spanY) { - cellXY[0] = cell.cellX; - cellXY[1] = cell.cellY; - found = true; - break; - } - } - - // Look for the first cell large enough - for (int i = 0; i < count; i++) { - VacantCell cell = list.get(i); - if (cell.spanX >= spanX && cell.spanY >= spanY) { - cellXY[0] = cell.cellX; - cellXY[1] = cell.cellY; - found = true; - break; - } - } - - if (clear) clearVacantCells(); - - return found; - } - @Override public String toString() { - return "Cell[view=" + (cell == null ? "null" : cell.getClass()) + ", x=" + cellX + - ", y=" + cellY + "]"; + return "Cell[view=" + (cell == null ? "null" : cell.getClass()) + + ", x=" + cellX + ", y=" + cellY + "]"; } } - - public boolean lastDownOnOccupiedCell() { - return mLastDownOnOccupiedCell; - } } - - diff --git a/src/com/android/launcher2/CustomizePagedView.java b/src/com/android/launcher2/CustomizePagedView.java new file mode 100644 index 0000000..b48d4ab --- /dev/null +++ b/src/com/android/launcher2/CustomizePagedView.java @@ -0,0 +1,981 @@ +/* + * 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 java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.Region.Op; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ActionMode; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher.R; + +public class CustomizePagedView extends PagedView + implements View.OnLongClickListener, View.OnClickListener, View.OnTouchListener, + DragSource, ActionMode.Callback { + + public enum CustomizationType { + WidgetCustomization, + ShortcutCustomization, + WallpaperCustomization, + ApplicationCustomization + } + + private static final String TAG = "CustomizeWorkspace"; + private static final boolean DEBUG = false; + + private View mLastTouchedItem; + private Launcher mLauncher; + private DragController mDragController; + private PackageManager mPackageManager; + private boolean mIsDragging; + private float mDragSlopeThreshold; + + private CustomizationType mCustomizationType; + + // The layout used to emulate the workspace in resolve the cell dimensions of a widget + private PagedViewCellLayout mWorkspaceWidgetLayout; + + // The mapping between the pages and the widgets that will be laid out on them + private ArrayList<ArrayList<AppWidgetProviderInfo>> mWidgetPages; + + // The max dimensions for the ImageView we use for displaying a widget + private int mMaxWidgetWidth; + + // The max number of widget cells to take a "page" of widgets + private int mMaxWidgetsCellHSpan; + + // The size of the items on the wallpaper tab + private int mWallpaperCellHSpan; + + // The max dimensions for the ImageView we use for displaying a wallpaper + private int mMaxWallpaperWidth; + + // The raw sources of data for each of the different tabs of the customization page + private List<AppWidgetProviderInfo> mWidgetList; + private List<ResolveInfo> mShortcutList; + private List<ResolveInfo> mWallpaperList; + private List<ApplicationInfo> mApps; + + private static final int sMinWidgetCellHSpan = 2; + private static final int sMaxWidgetCellHSpan = 4; + + private int mChoiceModeTitleText; + + // The scale factor for widget previews inside the widget drawer + private static final float sScaleFactor = 0.75f; + + private final Canvas mCanvas = new Canvas(); + private final LayoutInflater mInflater; + + public CustomizePagedView(Context context) { + this(context, null, 0); + } + + public CustomizePagedView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CustomizePagedView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a; + a = context.obtainStyledAttributes(attrs, R.styleable.CustomizePagedView, defStyle, 0); + mWallpaperCellHSpan = a.getInt(R.styleable.CustomizePagedView_wallpaperCellSpanX, 4); + mMaxWidgetsCellHSpan = a.getInt(R.styleable.CustomizePagedView_widgetCellCountX, 8); + a.recycle(); + a = context.obtainStyledAttributes(attrs, R.styleable.PagedView, defStyle, 0); + mCellCountX = a.getInt(R.styleable.PagedView_cellCountX, 7); + mCellCountY = a.getInt(R.styleable.PagedView_cellCountY, 4); + a.recycle(); + + mCustomizationType = CustomizationType.WidgetCustomization; + mWidgetPages = new ArrayList<ArrayList<AppWidgetProviderInfo>>(); + mWorkspaceWidgetLayout = new PagedViewCellLayout(context); + mInflater = LayoutInflater.from(context); + + final Resources r = context.getResources(); + mDragSlopeThreshold = + r.getInteger(R.integer.config_customizationDrawerDragSlopeThreshold) / 100.0f; + + setVisibility(View.GONE); + setSoundEffectsEnabled(false); + setupWorkspaceLayout(); + } + + @Override + protected void init() { + super.init(); + mCenterPagesVertically = false; + } + + public void setLauncher(Launcher launcher) { + Context context = getContext(); + mLauncher = launcher; + mPackageManager = context.getPackageManager(); + } + + /** + * Sets the list of applications that launcher has loaded. + */ + public void setApps(ArrayList<ApplicationInfo> list) { + mApps = list; + Collections.sort(mApps, LauncherModel.APP_NAME_COMPARATOR); + + // Update the widgets/shortcuts to reflect changes in the set of available apps + invalidatePageDataAndIconCache(); + } + + /** + * Convenience function to add new items to the set of applications that were previously loaded. + * Called by both updateApps() and setApps(). + */ + private void addAppsWithoutInvalidate(ArrayList<ApplicationInfo> list) { + // we add it in place, in alphabetical order + final int count = list.size(); + for (int i = 0; i < count; ++i) { + final ApplicationInfo info = list.get(i); + final int index = Collections.binarySearch(mApps, info, LauncherModel.APP_NAME_COMPARATOR); + if (index < 0) { + mApps.add(-(index + 1), info); + } + } + } + + /** + * Adds new applications to the loaded list, and notifies the paged view to update itself. + */ + public void addApps(ArrayList<ApplicationInfo> list) { + addAppsWithoutInvalidate(list); + + // Update the widgets/shortcuts to reflect changes in the set of available apps + invalidatePageDataAndIconCache(); + } + + /** + * Convenience function to remove items to the set of applications that were previously loaded. + * Called by both updateApps() and removeApps(). + */ + private void removeAppsWithoutInvalidate(ArrayList<ApplicationInfo> list) { + // loop through all the apps and remove apps that have the same component + final int length = list.size(); + for (int i = 0; i < length; ++i) { + final ApplicationInfo info = list.get(i); + int removeIndex = findAppByComponent(mApps, info); + if (removeIndex > -1) { + mApps.remove(removeIndex); + mPageViewIconCache.removeOutline(info); + } + } + } + + /** + * Removes applications from the loaded list, and notifies the paged view to update itself. + */ + public void removeApps(ArrayList<ApplicationInfo> list) { + removeAppsWithoutInvalidate(list); + + // Update the widgets/shortcuts to reflect changes in the set of available apps + invalidatePageDataAndIconCache(); + } + + /** + * Updates a set of applications from the loaded list, and notifies the paged view to update + * itself. + */ + public void updateApps(ArrayList<ApplicationInfo> list) { + // We remove and re-add the updated applications list because it's properties may have + // changed (ie. the title), and this will ensure that the items will be in their proper + // place in the list. + removeAppsWithoutInvalidate(list); + addAppsWithoutInvalidate(list); + + // Update the widgets/shortcuts to reflect changes in the set of available apps + invalidatePageDataAndIconCache(); + } + + /** + * Convenience function to find matching ApplicationInfos for removal. + */ + private int findAppByComponent(List<ApplicationInfo> list, ApplicationInfo item) { + ComponentName removeComponent = item.intent.getComponent(); + final int length = list.size(); + for (int i = 0; i < length; ++i) { + ApplicationInfo info = list.get(i); + if (info.intent.getComponent().equals(removeComponent)) { + return i; + } + } + return -1; + } + + public void update() { + // get the list of widgets + mWidgetList = AppWidgetManager.getInstance(mLauncher).getInstalledProviders(); + Collections.sort(mWidgetList, new Comparator<AppWidgetProviderInfo>() { + @Override + public int compare(AppWidgetProviderInfo object1, AppWidgetProviderInfo object2) { + return object1.label.compareTo(object2.label); + } + }); + + Comparator<ResolveInfo> resolveInfoComparator = new Comparator<ResolveInfo>() { + @Override + public int compare(ResolveInfo object1, ResolveInfo object2) { + return object1.loadLabel(mPackageManager).toString().compareTo( + object2.loadLabel(mPackageManager).toString()); + } + }; + + // get the list of shortcuts + Intent shortcutsIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT); + mShortcutList = mPackageManager.queryIntentActivities(shortcutsIntent, 0); + Collections.sort(mShortcutList, resolveInfoComparator); + + // get the list of wallpapers + Intent wallpapersIntent = new Intent(Intent.ACTION_SET_WALLPAPER); + mWallpaperList = mPackageManager.queryIntentActivities(wallpapersIntent, 0); + Collections.sort(mWallpaperList, resolveInfoComparator); + + invalidatePageDataAndIconCache(); + } + + private void invalidatePageDataAndIconCache() { + // Reset the icon cache + mPageViewIconCache.clear(); + + // Refresh all the tabs + invalidatePageData(); + } + + public void setDragController(DragController dragger) { + mDragController = dragger; + } + + public void setCustomizationFilter(CustomizationType filterType) { + mCustomizationType = filterType; + setCurrentPage(0); + updateCurrentPageScroll(); + invalidatePageData(); + + // End the current choice mode so that we don't carry selections across tabs + endChoiceMode(); + // Reset the touch item (if we are mid-dragging) + mLastTouchedItem = null; + } + + @Override + public void onDropCompleted(View target, boolean success) { + mLauncher.getWorkspace().onDragStopped(); + } + + @Override + public void onDragViewVisible() { + } + + @Override + public void onClick(View v) { + // Return early if this is not initiated from a touch + if (!v.isInTouchMode()) return; + // Return early if we are still animating the pages + if (mNextPage != INVALID_PAGE) return; + + // On certain pages, we allow single tap to mark items as selected so that they can be + // dropped onto the mini workspaces + boolean enterChoiceMode = false; + switch (mCustomizationType) { + case WidgetCustomization: + mChoiceModeTitleText = R.string.cab_widget_selection_text; + enterChoiceMode = true; + break; + case ApplicationCustomization: + mChoiceModeTitleText = R.string.cab_app_selection_text; + enterChoiceMode = true; + break; + case ShortcutCustomization: + mChoiceModeTitleText = R.string.cab_shortcut_selection_text; + enterChoiceMode = true; + break; + default: + break; + } + + if (enterChoiceMode) { + final ItemInfo itemInfo = (ItemInfo) v.getTag(); + + Workspace w = mLauncher.getWorkspace(); + int currentWorkspaceScreen = mLauncher.getCurrentWorkspaceScreen(); + final CellLayout cl = (CellLayout)w.getChildAt(currentWorkspaceScreen); + + animateClickFeedback(v, new Runnable() { + @Override + public void run() { + mLauncher.addExternalItemToScreen(itemInfo, cl); + } + }); + return; + } + + // Otherwise, we just handle the single click here + switch (mCustomizationType) { + case WallpaperCustomization: + // animate some feedback to the long press + final View clickView = v; + animateClickFeedback(v, new Runnable() { + @Override + public void run() { + // add the shortcut + ResolveInfo info = (ResolveInfo) clickView.getTag(); + Intent createWallpapersIntent = new Intent(Intent.ACTION_SET_WALLPAPER); + ComponentName name = new ComponentName(info.activityInfo.packageName, + info.activityInfo.name); + createWallpapersIntent.setComponent(name); + mLauncher.processWallpaper(createWallpapersIntent); + } + }); + break; + default: + break; + } + } + + @Override + public boolean onLongClick(View v) { + // Return early if this is not initiated from a touch + if (!v.isInTouchMode()) return false; + // Return early if we are still animating the pages + if (mNextPage != INVALID_PAGE) return false; + return beginDragging(v); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + mLastTouchedItem = v; + return false; + } + + /* + * Determines if we should change the touch state to start scrolling after the + * user moves their touch point too far. + */ + protected void determineScrollingStart(MotionEvent ev) { + if (!mIsDragging) super.determineScrollingStart(ev); + } + + /* + * Determines if we should change the touch state to start dragging after the + * user moves their touch point far enough. + */ + protected void determineDraggingStart(MotionEvent ev) { + /* + * Locally do absolute value. mLastMotionX is set to the y value + * of the down event. + */ + final int pointerIndex = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(pointerIndex); + final float y = ev.getY(pointerIndex); + final int xDiff = (int) Math.abs(x - mLastMotionX); + final int yDiff = (int) Math.abs(y - mLastMotionY); + + final int touchSlop = mTouchSlop; + boolean yMoved = yDiff > touchSlop; + boolean isUpwardMotion = (yDiff / (float) xDiff) > mDragSlopeThreshold; + + if (isUpwardMotion && yMoved && mLastTouchedItem != null) { + // Drag if the user moved far enough along the Y axis + beginDragging(mLastTouchedItem); + + // Cancel any pending longpress + if (mAllowLongPress) { + mAllowLongPress = false; + // Try canceling the long press. It could also have been scheduled + // by a distant descendant, so use the mAllowLongPress flag to block + // everything + final View currentPage = getPageAt(mCurrentPage); + if (currentPage != null) { + currentPage.cancelLongPress(); + } + } + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + final int action = ev.getAction(); + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + mIsDragging = false; + break; + case MotionEvent.ACTION_MOVE: + if (mTouchState != TOUCH_STATE_SCROLLING && !mIsDragging) { + determineDraggingStart(ev); + } + break; + } + return super.onInterceptTouchEvent(ev); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + final int action = ev.getAction(); + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + mIsDragging = false; + break; + case MotionEvent.ACTION_MOVE: + if (mTouchState != TOUCH_STATE_SCROLLING && !mIsDragging) { + determineDraggingStart(ev); + } + break; + } + return super.onTouchEvent(ev); + } + + private boolean beginDragging(View v) { + // End the current choice mode before we start dragging anything + if (isChoiceMode(CHOICE_MODE_SINGLE)) { + endChoiceMode(); + } + mIsDragging = true; + + PendingAddItemInfo createItemInfo; + switch (mCustomizationType) { + case WidgetCustomization: + // Get the icon as the drag representation + final LinearLayout l = (LinearLayout) v; + final Drawable icon = ((ImageView) l.findViewById(R.id.widget_preview)).getDrawable(); + Bitmap b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + icon.draw(c); + PendingAddWidgetInfo createWidgetInfo = (PendingAddWidgetInfo) v.getTag(); + + mLauncher.getWorkspace().onDragStartedWithItemMinSize( + createWidgetInfo.minWidth, createWidgetInfo.minHeight); + mDragController.startDrag(v, b, this, createWidgetInfo, DragController.DRAG_ACTION_COPY, null); + + // Cleanup the icon + b.recycle(); + return true; + case ShortcutCustomization: + createItemInfo = (PendingAddItemInfo) v.getTag(); + mDragController.startDrag(v, this, createItemInfo, DragController.DRAG_ACTION_COPY); + mLauncher.getWorkspace().onDragStartedWithItemSpans(1, 1); + return true; + case ApplicationCustomization: + // Pick up the application for dropping + ApplicationInfo app = (ApplicationInfo) v.getTag(); + app = new ApplicationInfo(app); + + mDragController.startDrag(v, this, app, DragController.DRAG_ACTION_COPY); + mLauncher.getWorkspace().onDragStartedWithItemSpans(1, 1); + return true; + } + return false; + } + + /** + * Pre-processes the layout of the different widget pages. + * @return the number of pages of widgets that we have + */ + private int relayoutWidgets() { + if (mWidgetList.isEmpty()) return 0; + + // create a new page for the first set of widgets + ArrayList<AppWidgetProviderInfo> newPage = new ArrayList<AppWidgetProviderInfo>(); + mWidgetPages.clear(); + mWidgetPages.add(newPage); + + // do this until we have no more widgets to lay out + final int maxNumCellsPerRow = mMaxWidgetsCellHSpan; + final int widgetCount = mWidgetList.size(); + int numCellsInRow = 0; + for (int i = 0; i < widgetCount; ++i) { + final AppWidgetProviderInfo info = mWidgetList.get(i); + + // determine the size of the current widget + int cellSpanX = Math.max(sMinWidgetCellHSpan, Math.min(sMaxWidgetCellHSpan, + mWorkspaceWidgetLayout.estimateCellHSpan(info.minWidth))); + + // create a new page if necessary + if ((numCellsInRow + cellSpanX) > maxNumCellsPerRow) { + numCellsInRow = 0; + newPage = new ArrayList<AppWidgetProviderInfo>(); + mWidgetPages.add(newPage); + } + + // add the item to the current page + newPage.add(info); + numCellsInRow += cellSpanX; + } + + return mWidgetPages.size(); + } + + /** + * Helper function to draw a drawable to the specified canvas with the specified bounds. + */ + private void renderDrawableToBitmap(Drawable d, Bitmap bitmap, int x, int y, int w, int h) { + if (bitmap != null) mCanvas.setBitmap(bitmap); + mCanvas.save(); + d.setBounds(x, y, x+w, y+h); + d.draw(mCanvas); + mCanvas.restore(); + } + + /** + * This method will extract the preview image specified by the wallpaper source provider (if it + * exists) otherwise, it will try to generate a default image preview. + */ + private Drawable getWallpaperPreview(ResolveInfo info) { + // To be implemented later: resolving the up-to-date wallpaper thumbnail + + final int minDim = mWorkspaceWidgetLayout.estimateCellWidth(1); + final int dim = mWorkspaceWidgetLayout.estimateCellWidth(mWallpaperCellHSpan); + Resources resources = mLauncher.getResources(); + + // Create a new bitmap to hold the widget preview + int width = (int) (dim * sScaleFactor); + int height = (int) (dim * sScaleFactor); + final Bitmap bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888); + final Drawable background = resources.getDrawable(R.drawable.default_widget_preview); + renderDrawableToBitmap(background, bitmap, 0, 0, width, height); + + // Draw the icon flush left + try { + final IconCache iconCache = + ((LauncherApplication) mLauncher.getApplication()).getIconCache(); + Drawable icon = new FastBitmapDrawable(Utilities.createIconBitmap( + iconCache.getFullResIcon(info, mPackageManager), mContext)); + + final int iconSize = minDim / 2; + final int offset = iconSize / 4; + renderDrawableToBitmap(icon, null, offset, offset, iconSize, iconSize); + } catch (Resources.NotFoundException e) { + // if we can't find the icon, then just don't draw it + } + + Drawable drawable = new FastBitmapDrawable(bitmap); + drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + return drawable; + } + + /** + * This method will extract the preview image specified by the widget developer (if it exists), + * otherwise, it will try to generate a default image preview with the widget's package icon. + * @return the drawable that will be used and sized in the ImageView to represent the widget + */ + private Drawable getWidgetPreview(AppWidgetProviderInfo info) { + final PackageManager packageManager = mPackageManager; + String packageName = info.provider.getPackageName(); + Drawable drawable = null; + if (info.previewImage != 0) { + drawable = packageManager.getDrawable(packageName, info.previewImage, null); + if (drawable == null) { + Log.w(TAG, "Can't load icon drawable 0x" + Integer.toHexString(info.icon) + + " for provider: " + info.provider); + } + } + + // If we don't have a preview image, create a default one + final int minDim = mWorkspaceWidgetLayout.estimateCellWidth(1); + final int maxDim = mWorkspaceWidgetLayout.estimateCellWidth(3); + if (drawable == null) { + Resources resources = mLauncher.getResources(); + + // Create a new bitmap to hold the widget preview + int width = (int) (Math.max(minDim, Math.min(maxDim, info.minWidth)) * sScaleFactor); + int height = (int) (Math.max(minDim, Math.min(maxDim, info.minHeight)) * sScaleFactor); + final Bitmap bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888); + final Drawable background = resources.getDrawable(R.drawable.default_widget_preview); + renderDrawableToBitmap(background, bitmap, 0, 0, width, height); + + // Draw the icon flush left + try { + Drawable icon = null; + if (info.icon > 0) { + icon = packageManager.getDrawable(packageName, info.icon, null); + } + if (icon == null) { + icon = resources.getDrawable(R.drawable.ic_launcher_application); + } + + final int iconSize = minDim / 2; + final int offset = iconSize / 4; + renderDrawableToBitmap(icon, null, offset, offset, iconSize, iconSize); + } catch (Resources.NotFoundException e) { + // if we can't find the icon, then just don't draw it + } + + drawable = new FastBitmapDrawable(bitmap); + } else { + // Scale down the preview if necessary + final float imageWidth = drawable.getIntrinsicWidth(); + final float imageHeight = drawable.getIntrinsicHeight(); + final float aspect = (float) imageWidth / imageHeight; + final int scaledWidth = + (int) (Math.max(minDim, Math.min(maxDim, imageWidth)) * sScaleFactor); + final int scaledHeight = + (int) (Math.max(minDim, Math.min(maxDim, imageHeight)) * sScaleFactor); + int width; + int height; + if (aspect >= 1.0f) { + width = scaledWidth; + height = (int) (((float) scaledWidth / imageWidth) * imageHeight); + } else { + height = scaledHeight; + width = (int) (((float) scaledHeight / imageHeight) * imageWidth); + } + + final Bitmap bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888); + renderDrawableToBitmap(drawable, bitmap, 0, 0, width, height); + + drawable = new FastBitmapDrawable(bitmap); + } + drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + return drawable; + } + + private void setupPage(PagedViewCellLayout layout) { + layout.setCellCount(mCellCountX, mCellCountY); + layout.setPadding(mPageLayoutPaddingLeft, mPageLayoutPaddingTop, mPageLayoutPaddingRight, + mPageLayoutPaddingBottom); + layout.setGap(mPageLayoutWidthGap, mPageLayoutHeightGap); + } + + private void setupWorkspaceLayout() { + mWorkspaceWidgetLayout.setCellCount(mCellCountX, mCellCountY); + mWorkspaceWidgetLayout.setPadding(20, 10, 20, 0); + + mMaxWidgetWidth = mWorkspaceWidgetLayout.estimateCellWidth(sMaxWidgetCellHSpan); + mMaxWallpaperWidth = mWorkspaceWidgetLayout.estimateCellWidth(mWallpaperCellHSpan); + } + + private void syncWidgetPages() { + if (mWidgetList == null) return; + + // we need to repopulate with the LinearLayout layout for the widget pages + removeAllViews(); + int numPages = relayoutWidgets(); + for (int i = 0; i < numPages; ++i) { + LinearLayout layout = new PagedViewExtendedLayout(getContext()); + layout.setGravity(Gravity.CENTER_HORIZONTAL); + layout.setPadding(mPageLayoutPaddingLeft, mPageLayoutPaddingTop, + mPageLayoutPaddingRight, mPageLayoutPaddingBottom); + + addView(layout, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.MATCH_PARENT)); + } + } + + private void syncWidgetPageItems(int page) { + // ensure that we have the right number of items on the pages + LinearLayout layout = (LinearLayout) getChildAt(page); + final ArrayList<AppWidgetProviderInfo> list = mWidgetPages.get(page); + final int count = list.size(); + layout.removeAllViews(); + for (int i = 0; i < count; ++i) { + final AppWidgetProviderInfo info = (AppWidgetProviderInfo) list.get(i); + final PendingAddWidgetInfo createItemInfo = new PendingAddWidgetInfo(info, null, null); + + LinearLayout l = (LinearLayout) mInflater.inflate( + R.layout.customize_paged_view_widget, layout, false); + l.setTag(createItemInfo); + l.setOnClickListener(this); + l.setOnTouchListener(this); + l.setOnLongClickListener(this); + + final Drawable icon = getWidgetPreview(info); + + int[] spans = CellLayout.rectToCell(getResources(), info.minWidth, info.minHeight, null); + final int hSpan = spans[0]; + final int vSpan = spans[1]; + + ImageView image = (ImageView) l.findViewById(R.id.widget_preview); + image.setMaxWidth(mMaxWidgetWidth); + image.setImageDrawable(icon); + TextView name = (TextView) l.findViewById(R.id.widget_name); + name.setText(info.label); + TextView dims = (TextView) l.findViewById(R.id.widget_dims); + dims.setText(mContext.getString(R.string.widget_dims_format, hSpan, vSpan)); + + layout.addView(l); + } + } + + private void syncWallpaperPages() { + if (mWallpaperList == null) return; + + // We need to repopulate the LinearLayout for the wallpaper pages + removeAllViews(); + int numPages = (int) Math.ceil((float) (mWallpaperList.size() * mWallpaperCellHSpan) / + mMaxWidgetsCellHSpan); + for (int i = 0; i < numPages; ++i) { + LinearLayout layout = new PagedViewExtendedLayout(getContext()); + layout.setGravity(Gravity.CENTER_HORIZONTAL); + layout.setPadding(mPageLayoutPaddingLeft, mPageLayoutPaddingTop, + mPageLayoutPaddingRight, mPageLayoutPaddingBottom); + + addView(layout, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.MATCH_PARENT)); + } + } + + private void syncWallpaperPageItems(int page) { + // Load the items on to the pages + LinearLayout layout = (LinearLayout) getChildAt(page); + layout.removeAllViews(); + final int count = mWallpaperList.size(); + final int numItemsPerPage = mMaxWidgetsCellHSpan / mWallpaperCellHSpan; + final int startIndex = page * numItemsPerPage; + final int endIndex = Math.min(count, startIndex + numItemsPerPage); + for (int i = startIndex; i < endIndex; ++i) { + final ResolveInfo info = mWallpaperList.get(i); + + LinearLayout l = (LinearLayout) mInflater.inflate( + R.layout.customize_paged_view_wallpaper, layout, false); + l.setTag(info); + l.setOnClickListener(this); + + final Drawable icon = getWallpaperPreview(info); + + ImageView image = (ImageView) l.findViewById(R.id.wallpaper_preview); + image.setMaxWidth(mMaxWidgetWidth); + image.setImageDrawable(icon); + TextView name = (TextView) l.findViewById(R.id.wallpaper_name); + name.setText(info.loadLabel(mPackageManager)); + + layout.addView(l); + } + } + + private void syncListPages(List<ResolveInfo> list) { + // we need to repopulate with PagedViewCellLayouts + removeAllViews(); + + // ensure that we have the right number of pages + int numPages = (int) Math.ceil((float) list.size() / (mCellCountX * mCellCountY)); + for (int i = 0; i < numPages; ++i) { + PagedViewCellLayout layout = new PagedViewCellLayout(getContext()); + setupPage(layout); + addView(layout); + } + } + + private void syncListPageItems(int page, List<ResolveInfo> list) { + // ensure that we have the right number of items on the pages + int numCells = mCellCountX * mCellCountY; + int startIndex = page * numCells; + int endIndex = Math.min(startIndex + numCells, list.size()); + PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(page); + // TODO: we can optimize by just re-applying to existing views + layout.removeAllViews(); + for (int i = startIndex; i < endIndex; ++i) { + ResolveInfo info = list.get(i); + PendingAddItemInfo createItemInfo = new PendingAddItemInfo(); + + PagedViewIcon icon = (PagedViewIcon) mInflater.inflate( + R.layout.customize_paged_view_item, layout, false); + icon.applyFromResolveInfo(info, mPackageManager, mPageViewIconCache, + ((LauncherApplication)mLauncher.getApplication()).getIconCache()); + switch (mCustomizationType) { + case WallpaperCustomization: + icon.setOnClickListener(this); + break; + case ShortcutCustomization: + createItemInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; + createItemInfo.componentName = new ComponentName(info.activityInfo.packageName, + info.activityInfo.name); + icon.setTag(createItemInfo); + icon.setOnClickListener(this); + icon.setOnTouchListener(this); + icon.setOnLongClickListener(this); + break; + default: + break; + } + + final int index = i - startIndex; + final int x = index % mCellCountX; + final int y = index / mCellCountX; + setupPage(layout); + layout.addViewToCellLayout(icon, -1, i, new PagedViewCellLayout.LayoutParams(x,y, 1,1)); + } + } + + private void syncAppPages() { + if (mApps == null) return; + + // We need to repopulate with PagedViewCellLayouts + removeAllViews(); + + // Ensure that we have the right number of pages + int numPages = (int) Math.ceil((float) mApps.size() / (mCellCountX * mCellCountY)); + for (int i = 0; i < numPages; ++i) { + PagedViewCellLayout layout = new PagedViewCellLayout(getContext()); + setupPage(layout); + addView(layout); + } + } + + private void syncAppPageItems(int page) { + if (mApps == null) return; + + // ensure that we have the right number of items on the pages + int numCells = mCellCountX * mCellCountY; + int startIndex = page * numCells; + int endIndex = Math.min(startIndex + numCells, mApps.size()); + PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(page); + // TODO: we can optimize by just re-applying to existing views + layout.removeAllViews(); + for (int i = startIndex; i < endIndex; ++i) { + final ApplicationInfo info = mApps.get(i); + PagedViewIcon icon = (PagedViewIcon) mInflater.inflate( + R.layout.all_apps_paged_view_application, layout, false); + icon.applyFromApplicationInfo(info, mPageViewIconCache, true); + icon.setOnClickListener(this); + icon.setOnTouchListener(this); + icon.setOnLongClickListener(this); + + final int index = i - startIndex; + final int x = index % mCellCountX; + final int y = index / mCellCountX; + setupPage(layout); + layout.addViewToCellLayout(icon, -1, i, new PagedViewCellLayout.LayoutParams(x,y, 1,1)); + } + } + + @Override + public void syncPages() { + boolean centerPagedViewCellLayouts = false; + switch (mCustomizationType) { + case WidgetCustomization: + syncWidgetPages(); + break; + case ShortcutCustomization: + syncListPages(mShortcutList); + centerPagedViewCellLayouts = true; + break; + case WallpaperCustomization: + syncWallpaperPages(); + break; + case ApplicationCustomization: + syncAppPages(); + centerPagedViewCellLayouts = false; + break; + default: + removeAllViews(); + setCurrentPage(0); + break; + } + + // only try and center the page if there is one page + final int childCount = getChildCount(); + if (centerPagedViewCellLayouts) { + if (childCount == 1) { + PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(0); + layout.enableCenteredContent(true); + } else { + for (int i = 0; i < childCount; ++i) { + PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i); + layout.enableCenteredContent(false); + } + } + } + + // bound the current page + setCurrentPage(Math.max(0, Math.min(childCount - 1, getCurrentPage()))); + } + + @Override + public void syncPageItems(int page) { + switch (mCustomizationType) { + case WidgetCustomization: + syncWidgetPageItems(page); + break; + case ShortcutCustomization: + syncListPageItems(page, mShortcutList); + break; + case WallpaperCustomization: + syncWallpaperPageItems(page); + break; + case ApplicationCustomization: + syncAppPageItems(page); + break; + } + } + + @Override + protected int getAssociatedLowerPageBound(int page) { + return 0; + } + @Override + protected int getAssociatedUpperPageBound(int page) { + return getChildCount(); + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + mode.setTitle(mChoiceModeTitleText); + return true; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + endChoiceMode(); + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; + } +} diff --git a/src/com/android/launcher2/DeferredHandler.java b/src/com/android/launcher2/DeferredHandler.java index 7801642..0323c7f 100644 --- a/src/com/android/launcher2/DeferredHandler.java +++ b/src/com/android/launcher2/DeferredHandler.java @@ -16,13 +16,12 @@ package com.android.launcher2; +import java.util.LinkedList; + import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; -import android.util.Log; - -import java.util.LinkedList; /** * Queue of things to run on a looper thread. Items posted with {@link #post} will not diff --git a/src/com/android/launcher2/DeleteZone.java b/src/com/android/launcher2/DeleteZone.java index 4e8b204..be651b2 100644 --- a/src/com/android/launcher2/DeleteZone.java +++ b/src/com/android/launcher2/DeleteZone.java @@ -16,24 +16,24 @@ package com.android.launcher2; -import android.widget.ImageView; +import com.android.launcher.R; + import android.content.Context; import android.content.res.TypedArray; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.TransitionDrawable; import android.util.AttributeSet; import android.view.View; -import android.view.animation.TranslateAnimation; -import android.view.animation.Animation; -import android.view.animation.AnimationSet; import android.view.animation.AccelerateInterpolator; import android.view.animation.AlphaAnimation; -import android.graphics.RectF; -import android.graphics.drawable.TransitionDrawable; - -import com.android.launcher.R; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.TranslateAnimation; +import android.widget.ImageView; public class DeleteZone extends ImageView implements DropTarget, DragController.DragListener { private static final int ORIENTATION_HORIZONTAL = 1; @@ -45,6 +45,13 @@ public class DeleteZone extends ImageView implements DropTarget, DragController. private Launcher mLauncher; private boolean mTrashMode; + /** + * If true, this View responsible for managing its own visibility, and that of its handle. + * This is generally the case, but it will be set to false when this is part of the + * Contextual Action Bar. + */ + private boolean mManageVisibility = true; + private AnimationSet mInAnimation; private AnimationSet mOutAnimation; private Animation mHandleInAnimation; @@ -53,11 +60,14 @@ public class DeleteZone extends ImageView implements DropTarget, DragController. private int mOrientation; private DragController mDragController; - private final RectF mRegion = new RectF(); + private final RectF mRegionF = new RectF(); + private final Rect mRegion = new Rect(); private TransitionDrawable mTransition; - private View mHandle; private final Paint mTrashPaint = new Paint(); + /** The View that this view will replace. */ + private View mHandle = null; + public DeleteZone(Context context, AttributeSet attrs) { this(context, attrs, 0); } @@ -71,6 +81,7 @@ public class DeleteZone extends ImageView implements DropTarget, DragController. TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DeleteZone, defStyle, 0); mOrientation = a.getInt(R.styleable.DeleteZone_direction, ORIENTATION_HORIZONTAL); a.recycle(); + } @Override @@ -83,31 +94,30 @@ public class DeleteZone extends ImageView implements DropTarget, DragController. DragView dragView, Object dragInfo) { return true; } - - public Rect estimateDropLocation(DragSource source, int x, int y, int xOffset, int yOffset, - DragView dragView, Object dragInfo, Rect recycle) { - return null; - } public void onDrop(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo) { final ItemInfo item = (ItemInfo) dragInfo; + // On x-large screens, you can uninstall an app by dragging from all apps + if (item instanceof ApplicationInfo && LauncherApplication.isScreenXLarge()) { + mLauncher.startApplicationUninstallActivity((ApplicationInfo) item); + } + if (item.container == -1) return; if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { if (item instanceof LauncherAppWidgetInfo) { mLauncher.removeAppWidget((LauncherAppWidgetInfo) item); } - } else { - if (source instanceof UserFolder) { - final UserFolder userFolder = (UserFolder) source; - final UserFolderInfo userFolderInfo = (UserFolderInfo) userFolder.getInfo(); - // Item must be a ShortcutInfo otherwise it couldn't have been in the folder - // in the first place. - userFolderInfo.remove((ShortcutInfo)item); - } + } else if (source instanceof UserFolder) { + final UserFolder userFolder = (UserFolder) source; + final UserFolderInfo userFolderInfo = (UserFolderInfo) userFolder.getInfo(); + // Item must be a ShortcutInfo otherwise it couldn't have been in the folder + // in the first place. + userFolderInfo.remove((ShortcutInfo)item); } + if (item instanceof UserFolderInfo) { final UserFolderInfo userFolderInfo = (UserFolderInfo)item; LauncherModel.deleteUserFolderContentsFromDatabase(mLauncher, userFolderInfo); @@ -126,6 +136,7 @@ public class DeleteZone extends ImageView implements DropTarget, DragController. }.start(); } } + LauncherModel.deleteItemFromDatabase(mLauncher, item); } @@ -149,16 +160,27 @@ public class DeleteZone extends ImageView implements DropTarget, DragController. final ItemInfo item = (ItemInfo) info; if (item != null) { mTrashMode = true; - createAnimations(); - final int[] location = mLocation; - getLocationOnScreen(location); - mRegion.set(location[0], location[1], location[0] + mRight - mLeft, - location[1] + mBottom - mTop); - mDragController.setDeleteRegion(mRegion); + getHitRect(mRegion); + mRegionF.set(mRegion); + + if (LauncherApplication.isScreenXLarge()) { + // This region will be a "dead zone" for scrolling; make it extend to the edge of + // the screen so users don't accidentally trigger a scroll while deleting items + mRegionF.top = mLauncher.getWorkspace().getTop(); + mRegionF.right = mLauncher.getWorkspace().getRight(); + } + + mDragController.setDeleteRegion(mRegionF); + + // Make sure the icon is set to the default drawable, not the hover drawable mTransition.resetTransition(); - startAnimation(mInAnimation); - mHandle.startAnimation(mHandleOutAnimation); - setVisibility(VISIBLE); + + if (mManageVisibility) { + createAnimations(); + startAnimation(mInAnimation); + mHandle.startAnimation(mHandleOutAnimation); + setVisibility(VISIBLE); + } } } @@ -166,54 +188,94 @@ public class DeleteZone extends ImageView implements DropTarget, DragController. if (mTrashMode) { mTrashMode = false; mDragController.setDeleteRegion(null); - startAnimation(mOutAnimation); - mHandle.startAnimation(mHandleInAnimation); - setVisibility(GONE); + + if (mOutAnimation != null) startAnimation(mOutAnimation); + if (mHandleInAnimation != null) mHandle.startAnimation(mHandleInAnimation); + + if (mManageVisibility) { + setVisibility(GONE); + } } } - private void createAnimations() { - if (mInAnimation == null) { - mInAnimation = new FastAnimationSet(); - final AnimationSet animationSet = mInAnimation; - animationSet.setInterpolator(new AccelerateInterpolator()); - animationSet.addAnimation(new AlphaAnimation(0.0f, 1.0f)); - if (mOrientation == ORIENTATION_HORIZONTAL) { - animationSet.addAnimation(new TranslateAnimation(Animation.ABSOLUTE, 0.0f, - Animation.ABSOLUTE, 0.0f, Animation.RELATIVE_TO_SELF, 1.0f, - Animation.RELATIVE_TO_SELF, 0.0f)); - } else { - animationSet.addAnimation(new TranslateAnimation(Animation.RELATIVE_TO_SELF, - 1.0f, Animation.RELATIVE_TO_SELF, 0.0f, Animation.ABSOLUTE, 0.0f, - Animation.ABSOLUTE, 0.0f)); - } - animationSet.setDuration(ANIMATION_DURATION); + public boolean isDropEnabled() { + return true; + } + + @Override + public void getHitRect(Rect outRect) { + super.getHitRect(outRect); + if (LauncherApplication.isScreenXLarge()) { + // TODO: This is a temporary hack. mManageVisiblity = false when you're in CAB mode. + // In that case, this icon is more tightly spaced next to the delete icon so we want + // it to have a smaller drag region. When the new drag&drop system comes in, we'll + // dispatch the drag/drop by calculating what target you're overlapping + final int minPadding = R.dimen.delete_zone_min_padding; + final int maxPadding = R.dimen.delete_zone_max_padding; + final int outerDragPadding = + getResources().getDimensionPixelSize(R.dimen.delete_zone_size); + final int innerDragPadding = getResources().getDimensionPixelSize( + mManageVisibility ? maxPadding : minPadding); + outRect.top -= outerDragPadding; + outRect.left -= innerDragPadding; + outRect.bottom += outerDragPadding; + outRect.right += innerDragPadding; } + } + + private void createAnimations() { if (mHandleInAnimation == null) { mHandleInAnimation = new AlphaAnimation(0.0f, 1.0f); mHandleInAnimation.setDuration(ANIMATION_DURATION); } - if (mOutAnimation == null) { - mOutAnimation = new FastAnimationSet(); - final AnimationSet animationSet = mOutAnimation; - animationSet.setInterpolator(new AccelerateInterpolator()); - animationSet.addAnimation(new AlphaAnimation(1.0f, 0.0f)); - if (mOrientation == ORIENTATION_HORIZONTAL) { - animationSet.addAnimation(new FastTranslateAnimation(Animation.ABSOLUTE, 0.0f, - Animation.ABSOLUTE, 0.0f, Animation.RELATIVE_TO_SELF, 0.0f, - Animation.RELATIVE_TO_SELF, 1.0f)); + + if (mInAnimation == null) { + mInAnimation = new FastAnimationSet(); + if (!LauncherApplication.isScreenXLarge()) { + final AnimationSet animationSet = mInAnimation; + animationSet.setInterpolator(new AccelerateInterpolator()); + animationSet.addAnimation(new AlphaAnimation(0.0f, 1.0f)); + if (mOrientation == ORIENTATION_HORIZONTAL) { + animationSet.addAnimation(new TranslateAnimation(Animation.ABSOLUTE, 0.0f, + Animation.ABSOLUTE, 0.0f, Animation.RELATIVE_TO_SELF, 1.0f, + Animation.RELATIVE_TO_SELF, 0.0f)); + } else { + animationSet.addAnimation(new TranslateAnimation(Animation.RELATIVE_TO_SELF, + 1.0f, Animation.RELATIVE_TO_SELF, 0.0f, Animation.ABSOLUTE, 0.0f, + Animation.ABSOLUTE, 0.0f)); + } + animationSet.setDuration(ANIMATION_DURATION); } else { - animationSet.addAnimation(new FastTranslateAnimation(Animation.RELATIVE_TO_SELF, - 0.0f, Animation.RELATIVE_TO_SELF, 1.0f, Animation.ABSOLUTE, 0.0f, - Animation.ABSOLUTE, 0.0f)); + mInAnimation.addAnimation(mHandleInAnimation); } - animationSet.setDuration(ANIMATION_DURATION); } + if (mHandleOutAnimation == null) { mHandleOutAnimation = new AlphaAnimation(1.0f, 0.0f); mHandleOutAnimation.setFillAfter(true); mHandleOutAnimation.setDuration(ANIMATION_DURATION); } + + if (mOutAnimation == null) { + mOutAnimation = new FastAnimationSet(); + if (!LauncherApplication.isScreenXLarge()) { + final AnimationSet animationSet = mOutAnimation; + animationSet.setInterpolator(new AccelerateInterpolator()); + animationSet.addAnimation(new AlphaAnimation(1.0f, 0.0f)); + if (mOrientation == ORIENTATION_HORIZONTAL) { + animationSet.addAnimation(new FastTranslateAnimation(Animation.ABSOLUTE, 0.0f, + Animation.ABSOLUTE, 0.0f, Animation.RELATIVE_TO_SELF, 0.0f, + Animation.RELATIVE_TO_SELF, 1.0f)); + } else { + animationSet.addAnimation(new FastTranslateAnimation(Animation.RELATIVE_TO_SELF, + 0.0f, Animation.RELATIVE_TO_SELF, 1.0f, Animation.ABSOLUTE, 0.0f, + Animation.ABSOLUTE, 0.0f)); + } + animationSet.setDuration(ANIMATION_DURATION); + } else { + mOutAnimation.addAnimation(mHandleOutAnimation); + } + } } void setLauncher(Launcher launcher) { @@ -228,6 +290,10 @@ public class DeleteZone extends ImageView implements DropTarget, DragController. mHandle = view; } + void setManageVisibility(boolean value) { + mManageVisibility = value; + } + private static class FastTranslateAnimation extends TranslateAnimation { public FastTranslateAnimation(int fromXType, float fromXValue, int toXType, float toXValue, int fromYType, float fromYValue, int toYType, float toYValue) { @@ -261,4 +327,10 @@ public class DeleteZone extends ImageView implements DropTarget, DragController. return false; } } + + @Override + public DropTarget getDropTargetDelegate(DragSource source, int x, int y, int xOffset, int yOffset, + DragView dragView, Object dragInfo) { + return null; + } } diff --git a/src/com/android/launcher2/Dimmable.java b/src/com/android/launcher2/Dimmable.java new file mode 100644 index 0000000..df43b3c --- /dev/null +++ b/src/com/android/launcher2/Dimmable.java @@ -0,0 +1,6 @@ +package com.android.launcher2; + +public interface Dimmable { + public void setDimmableProgress(float progress); + public float getDimmableProgress(); +} diff --git a/src/com/android/launcher2/DimmableAppWidgetHostView.java b/src/com/android/launcher2/DimmableAppWidgetHostView.java new file mode 100644 index 0000000..1f512a3 --- /dev/null +++ b/src/com/android/launcher2/DimmableAppWidgetHostView.java @@ -0,0 +1,115 @@ +/* + * 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 com.android.launcher.R; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.view.View; + +public class DimmableAppWidgetHostView extends LauncherAppWidgetHostView implements Dimmable { + public DimmableAppWidgetHostView(Context context) { + super(context); + mPaint.setFilterBitmap(true); + } + + private final Paint mPaint = new Paint(); + private Bitmap mDimmedView; + private Canvas mDimmedViewCanvas; + private boolean isDimmedViewUpdatePass; + private float mDimmableProgress; + + private void setChildAlpha(float alpha) { + if (getChildCount() > 0) { + final View child = getChildAt(0); + if (child.getAlpha() != alpha) { + getChildAt(0).setAlpha(alpha); + } + } + } + + private void updateChildAlpha() { + // hacky, but sometimes widgets get their alpha set back to 1.0f, so we call + // this to force them back + setChildAlpha(getAlpha()); + } + + //@Override + public boolean onSetAlpha(int alpha) { + super.onSetAlpha(alpha); + return true; + } + + public void setDimmableProgress(float progress) { + mDimmableProgress = progress; + } + + public float getDimmableProgress() { + return mDimmableProgress; + } + + private void updateDimmedView() { + if (mDimmedView == null) { + mDimmedView = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), + Bitmap.Config.ARGB_8888); + mDimmedViewCanvas = new Canvas(mDimmedView); + } + mDimmedViewCanvas.drawColor(0x00000000); + mDimmedViewCanvas.concat(getMatrix()); + isDimmedViewUpdatePass = true; + draw(mDimmedViewCanvas); + // make the bitmap look "dimmed" + int dimmedColor = getContext().getResources().getColor(R.color.dimmed_view_color); + mDimmedViewCanvas.drawColor(dimmedColor, PorterDuff.Mode.SRC_IN); + isDimmedViewUpdatePass = false; + invalidate(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + if (mDimmedView == null && mDimmableProgress > 0.0f) { + updateDimmedView(); + } + } + + @Override + public void dispatchDraw(Canvas canvas) { + if (isDimmedViewUpdatePass) { + final float alpha = getAlpha(); + canvas.save(); + setAlpha(1.0f); + super.dispatchDraw(canvas); + canvas.restore(); + setAlpha(alpha); + } else { + if (mDimmedView != null && mDimmableProgress > 0) { + // draw the dimmed version of this widget + mPaint.setAlpha((int) (mDimmableProgress * 255)); + canvas.drawBitmap(mDimmedView, 0, 0, mPaint); + } + + updateChildAlpha(); + super.dispatchDraw(canvas); + } + } +} diff --git a/src/com/android/launcher2/DimmableBubbleTextView.java b/src/com/android/launcher2/DimmableBubbleTextView.java new file mode 100644 index 0000000..cb3b8ef --- /dev/null +++ b/src/com/android/launcher2/DimmableBubbleTextView.java @@ -0,0 +1,98 @@ +/* + * 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 com.android.launcher.R; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.util.AttributeSet; + +public class DimmableBubbleTextView extends BubbleTextView implements Dimmable { + private Paint mDimmedPaint = new Paint(); + private int mAlpha; + private Bitmap mDimmedView; + private Canvas mDimmedViewCanvas; + private boolean isDimmedViewUpdatePass; + private float mDimmableProgress; + + public DimmableBubbleTextView(Context context) { + super(context); + mDimmedPaint.setFilterBitmap(true); + } + + public DimmableBubbleTextView(Context context, AttributeSet attrs) { + super(context, attrs); + mDimmedPaint.setFilterBitmap(true); + } + + public DimmableBubbleTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mDimmedPaint.setFilterBitmap(true); + } + + public void setDimmableProgress(float progress) { + mDimmableProgress = progress; + } + + public float getDimmableProgress() { + return mDimmableProgress; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + if (mDimmedView == null) { + isDimmedViewUpdatePass = true; + mDimmedView = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), + Bitmap.Config.ARGB_8888); + mDimmedViewCanvas = new Canvas(mDimmedView); + mDimmedViewCanvas.concat(getMatrix()); + + draw(mDimmedViewCanvas); + + // MAKE THE DIMMED VERSION + int dimmedColor = getContext().getResources().getColor(R.color.dimmed_view_color); + mDimmedViewCanvas.drawColor(dimmedColor, PorterDuff.Mode.SRC_IN); + + isDimmedViewUpdatePass = false; + } + } + + @Override + protected void onDraw(Canvas canvas) { + if (isDimmedViewUpdatePass) { + canvas.save(); + final float alpha = getAlpha(); + super.setAlpha(1.0f); + super.onDraw(canvas); + super.setAlpha(alpha); + canvas.restore(); + } else { + super.onDraw(canvas); + } + + if (mDimmedView != null && mDimmableProgress > 0) { + mDimmedPaint.setAlpha((int) (mDimmableProgress * 255)); + canvas.drawBitmap(mDimmedView, mScrollX, mScrollY, mDimmedPaint); + } + } +}
\ No newline at end of file diff --git a/src/com/android/launcher2/DragController.java b/src/com/android/launcher2/DragController.java index b453061..a0a44a5 100644 --- a/src/com/android/launcher2/DragController.java +++ b/src/com/android/launcher2/DragController.java @@ -16,25 +16,26 @@ package com.android.launcher2; +import com.android.launcher.R; + import android.content.Context; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.RectF; -import android.os.IBinder; import android.os.Handler; +import android.os.IBinder; import android.os.Vibrator; import android.util.DisplayMetrics; import android.util.Log; -import android.view.View; import android.view.KeyEvent; import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import java.util.ArrayList; -import com.android.launcher.R; - /** * Class for initiating a drag within a view or across multiple views. */ @@ -56,8 +57,9 @@ public class DragController { private static final int SCROLL_OUTSIDE_ZONE = 0; private static final int SCROLL_WAITING_IN_ZONE = 1; - private static final int SCROLL_LEFT = 0; - private static final int SCROLL_RIGHT = 1; + static final int SCROLL_NONE = -1; + static final int SCROLL_LEFT = 0; + static final int SCROLL_RIGHT = 1; private Context mContext; private Handler mHandler; @@ -105,7 +107,7 @@ public class DragController { /** Who can receive drop events */ private ArrayList<DropTarget> mDropTargets = new ArrayList<DropTarget>(); - private DragListener mListener; + private ArrayList<DragListener> mListeners = new ArrayList<DragListener>(); /** The window token used as the parent for the DragView. */ private IBinder mWindowToken; @@ -124,6 +126,9 @@ public class DragController { private InputMethodManager mInputMethodManager; + private int mLastTouch[] = new int[2]; + private int mDistanceSinceScroll = 0; + /** * Interface to receive notifications when a drag starts or stops */ @@ -140,7 +145,7 @@ public class DragController { void onDragStart(DragSource source, Object info, int dragAction); /** - * The drag has eneded + * The drag has ended */ void onDragEnd(); } @@ -156,9 +161,13 @@ public class DragController { mScrollZone = context.getResources().getDimensionPixelSize(R.dimen.scroll_zone); } + public boolean dragging() { + return mDragging; + } + /** * Starts a drag. - * + * * @param v The view that is being dragged * @param source An object representing where the drag originated * @param dragInfo The data associated with the object that is being dragged @@ -166,6 +175,22 @@ public class DragController { * {@link #DRAG_ACTION_COPY} */ public void startDrag(View v, DragSource source, Object dragInfo, int dragAction) { + startDrag(v, source, dragInfo, dragAction, null); + } + + /** + * Starts a drag. + * + * @param v The view that is being dragged + * @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(View v, DragSource source, Object dragInfo, int dragAction, + Rect dragRegion) { mOriginator = v; Bitmap b = getViewBitmap(v); @@ -181,7 +206,7 @@ public class DragController { int screenY = loc[1]; startDrag(b, screenX, screenY, 0, 0, b.getWidth(), b.getHeight(), - source, dragInfo, dragAction); + source, dragInfo, dragAction, dragRegion); b.recycle(); @@ -192,7 +217,36 @@ public class DragController { /** * Starts a drag. - * + * + * @param v The view that is being dragged + * @param bmp The bitmap that represents the view being dragged + * @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(View v, Bitmap bmp, DragSource source, Object dragInfo, int dragAction, + Rect dragRegion) { + mOriginator = v; + + int[] loc = mCoordinatesTemp; + v.getLocationOnScreen(loc); + int screenX = loc[0]; + int screenY = loc[1]; + + startDrag(bmp, screenX, screenY, 0, 0, bmp.getWidth(), bmp.getHeight(), + source, dragInfo, dragAction, dragRegion); + + if (dragAction == DRAG_ACTION_MOVE) { + v.setVisibility(View.GONE); + } + } + + /** + * Starts a drag. + * * @param b The bitmap to display as the drag image. It will be re-scaled to the * enlarged size. * @param screenX The x position on screen of the left-top of the bitmap. @@ -209,6 +263,31 @@ public class DragController { public void startDrag(Bitmap b, int screenX, int screenY, int textureLeft, int textureTop, int textureWidth, int textureHeight, DragSource source, Object dragInfo, int dragAction) { + startDrag(b, screenX, screenY, textureLeft, textureTop, textureWidth, textureHeight, + source, dragInfo, dragAction, null); + } + + /** + * Starts a drag. + * + * @param b The bitmap to display as the drag image. It will be re-scaled to the + * enlarged size. + * @param screenX The x position on screen of the left-top of the bitmap. + * @param screenY The y position on screen of the left-top of the bitmap. + * @param textureLeft The left edge of the region inside b to use. + * @param textureTop The top edge of the region inside b to use. + * @param textureWidth The width of the region inside b to use. + * @param textureHeight The height of the region inside b to use. + * @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 screenX, int screenY, + int textureLeft, int textureTop, int textureWidth, int textureHeight, + DragSource source, Object dragInfo, int dragAction, Rect dragRegion) { if (PROFILE_DRAWING_DURING_DRAG) { android.os.Debug.startMethodTracing("Launcher"); } @@ -220,15 +299,17 @@ public class DragController { } mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0); - if (mListener != null) { - mListener.onDragStart(source, dragInfo, dragAction); + for (DragListener listener : mListeners) { + listener.onDragStart(source, dragInfo, dragAction); } int registrationX = ((int)mMotionDownX) - screenX; int registrationY = ((int)mMotionDownY) - screenY; - mTouchOffsetX = mMotionDownX - screenX; - mTouchOffsetY = mMotionDownY - screenY; + final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left; + final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top; + mTouchOffsetX = mMotionDownX - screenX - dragRegionLeft; + mTouchOffsetY = mMotionDownY - screenY - dragRegionTop; mDragging = true; mDragSource = source; @@ -238,7 +319,22 @@ public class DragController { DragView dragView = mDragView = new DragView(mContext, b, registrationX, registrationY, textureLeft, textureTop, textureWidth, textureHeight); + + final DragSource dragSource = source; + dragView.setOnDrawRunnable(new Runnable() { + public void run() { + dragSource.onDragViewVisible(); + }; + }); + + if (dragRegion != null) { + dragView.setDragRegion(dragRegionLeft, dragRegion.top, + dragRegion.right - dragRegionLeft, dragRegion.bottom - dragRegionTop); + } + dragView.show(mWindowToken, (int)mMotionDownX, (int)mMotionDownY); + + handleMoveEvent((int) mMotionDownX, (int) mMotionDownY); } /** @@ -304,8 +400,8 @@ public class DragController { if (mOriginator != null) { mOriginator.setVisibility(View.VISIBLE); } - if (mListener != null) { - mListener.onDragEnd(); + for (DragListener listener : mListeners) { + listener.onDragEnd(); } if (mDragView != null) { mDragView.remove(); @@ -365,12 +461,80 @@ public class DragController { return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction); } + private void handleMoveEvent(int x, int y) { + mDragView.move(x, y); + + // Drop on someone? + final int[] coordinates = mCoordinatesTemp; + DropTarget dropTarget = findDropTarget(x, y, coordinates); + if (dropTarget != null) { + DropTarget delegate = dropTarget.getDropTargetDelegate( + mDragSource, coordinates[0], coordinates[1], + (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); + if (delegate != null) { + dropTarget = delegate; + } + + if (mLastDropTarget != dropTarget) { + if (mLastDropTarget != null) { + mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], + (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); + } + dropTarget.onDragEnter(mDragSource, coordinates[0], coordinates[1], + (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); + } + dropTarget.onDragOver(mDragSource, coordinates[0], coordinates[1], + (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); + } else { + if (mLastDropTarget != null) { + mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], + (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); + } + } + mLastDropTarget = dropTarget; + + // Scroll, maybe, but not if we're in the delete region. + boolean inDeleteRegion = false; + if (mDeleteRegion != null) { + inDeleteRegion = mDeleteRegion.contains(x, y); + } + + // After a scroll, the touch point will still be in the scroll region. + // Rather than scrolling immediately, require a bit of twiddling to scroll again + final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop(); + mDistanceSinceScroll += + Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2)); + mLastTouch[0] = x; + mLastTouch[1] = y; + + if (!inDeleteRegion && x < mScrollZone) { + if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) { + mScrollState = SCROLL_WAITING_IN_ZONE; + mScrollRunnable.setDirection(SCROLL_LEFT); + mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); + mDragScroller.onEnterScrollArea(SCROLL_LEFT); + } + } else if (!inDeleteRegion && x > mScrollView.getWidth() - mScrollZone) { + if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) { + mScrollState = SCROLL_WAITING_IN_ZONE; + mScrollRunnable.setDirection(SCROLL_RIGHT); + mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); + mDragScroller.onEnterScrollArea(SCROLL_RIGHT); + } + } else { + if (mScrollState == SCROLL_WAITING_IN_ZONE) { + mScrollState = SCROLL_OUTSIDE_ZONE; + mScrollRunnable.setDirection(SCROLL_RIGHT); + mHandler.removeCallbacks(mScrollRunnable); + mDragScroller.onExitScrollArea(); + } + } + } + /** * Call this from a drag source view. */ public boolean onTouchEvent(MotionEvent ev) { - View scrollView = mScrollView; - if (!mDragging) { return false; } @@ -385,69 +549,15 @@ public class DragController { mMotionDownX = screenX; mMotionDownY = screenY; - if ((screenX < mScrollZone) || (screenX > scrollView.getWidth() - mScrollZone)) { + if ((screenX < mScrollZone) || (screenX > mScrollView.getWidth() - mScrollZone)) { mScrollState = SCROLL_WAITING_IN_ZONE; mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); } else { mScrollState = SCROLL_OUTSIDE_ZONE; } - break; case MotionEvent.ACTION_MOVE: - // Update the drag view. Don't use the clamped pos here so the dragging looks - // like it goes off screen a little, intead of bumping up against the edge. - mDragView.move((int)ev.getRawX(), (int)ev.getRawY()); - - // Drop on someone? - final int[] coordinates = mCoordinatesTemp; - DropTarget dropTarget = findDropTarget(screenX, screenY, coordinates); - if (dropTarget != null) { - if (mLastDropTarget == dropTarget) { - dropTarget.onDragOver(mDragSource, coordinates[0], coordinates[1], - (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); - } else { - if (mLastDropTarget != null) { - mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], - (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); - } - dropTarget.onDragEnter(mDragSource, coordinates[0], coordinates[1], - (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); - } - } else { - if (mLastDropTarget != null) { - mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], - (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); - } - } - mLastDropTarget = dropTarget; - - // Scroll, maybe, but not if we're in the delete region. - boolean inDeleteRegion = false; - if (mDeleteRegion != null) { - inDeleteRegion = mDeleteRegion.contains(screenX, screenY); - } - //Log.d(Launcher.TAG, "inDeleteRegion=" + inDeleteRegion + " screenX=" + screenX - // + " mScrollZone=" + mScrollZone); - if (!inDeleteRegion && screenX < mScrollZone) { - if (mScrollState == SCROLL_OUTSIDE_ZONE) { - mScrollState = SCROLL_WAITING_IN_ZONE; - mScrollRunnable.setDirection(SCROLL_LEFT); - mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); - } - } else if (!inDeleteRegion && screenX > scrollView.getWidth() - mScrollZone) { - if (mScrollState == SCROLL_OUTSIDE_ZONE) { - mScrollState = SCROLL_WAITING_IN_ZONE; - mScrollRunnable.setDirection(SCROLL_RIGHT); - mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); - } - } else { - if (mScrollState == SCROLL_WAITING_IN_ZONE) { - mScrollState = SCROLL_OUTSIDE_ZONE; - mScrollRunnable.setDirection(SCROLL_RIGHT); - mHandler.removeCallbacks(mScrollRunnable); - } - } - + handleMoveEvent(screenX, screenY); break; case MotionEvent.ACTION_UP: mHandler.removeCallbacks(mScrollRunnable); @@ -481,6 +591,8 @@ public class DragController { mDragSource.onDropCompleted((View) dropTarget, false); return true; } + } else { + mDragSource.onDropCompleted(null, false); } return false; } @@ -491,13 +603,28 @@ public class DragController { final ArrayList<DropTarget> dropTargets = mDropTargets; final int count = dropTargets.size(); for (int i=count-1; i>=0; i--) { - final DropTarget target = dropTargets.get(i); + DropTarget target = dropTargets.get(i); + if (!target.isDropEnabled()) + continue; + target.getHitRect(r); + + // Convert the hit rect to screen coordinates target.getLocationOnScreen(dropCoordinates); r.offset(dropCoordinates[0] - target.getLeft(), dropCoordinates[1] - target.getTop()); + if (r.contains(x, y)) { + DropTarget delegate = target.getDropTargetDelegate(mDragSource, + x, y, (int)mTouchOffsetX, (int)mTouchOffsetY, mDragView, mDragInfo); + if (delegate != null) { + target = delegate; + target.getLocationOnScreen(dropCoordinates); + } + + // Make dropCoordinates relative to the DropTarget dropCoordinates[0] = x - dropCoordinates[0]; dropCoordinates[1] = y - dropCoordinates[1]; + return target; } } @@ -537,15 +664,15 @@ public class DragController { /** * Sets the drag listner which will be notified when a drag starts or ends. */ - public void setDragListener(DragListener l) { - mListener = l; + public void addDragListener(DragListener l) { + mListeners.add(l); } /** * Remove a previously installed drag listener. */ public void removeDragListener(DragListener l) { - mListener = null; + mListeners.remove(l); } /** @@ -592,6 +719,8 @@ public class DragController { mDragScroller.scrollRight(); } mScrollState = SCROLL_OUTSIDE_ZONE; + mDistanceSinceScroll = 0; + mDragScroller.onExitScrollArea(); } } diff --git a/src/com/android/launcher2/DragLayer.java b/src/com/android/launcher2/DragLayer.java index c683207..ab71670 100644 --- a/src/com/android/launcher2/DragLayer.java +++ b/src/com/android/launcher2/DragLayer.java @@ -18,13 +18,13 @@ package com.android.launcher2; import android.content.Context; import android.util.AttributeSet; -import android.view.MotionEvent; import android.view.KeyEvent; +import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; /** - * A ViewGroup that coordinated dragging across its dscendants + * A ViewGroup that coordinates dragging across its descendants */ public class DragLayer extends FrameLayout { DragController mDragController; @@ -33,7 +33,7 @@ public class DragLayer extends FrameLayout { * Used to create a new DragLayer from XML. * * @param context The application's context. - * @param attrs The attribtues set containing the Workspace's customization values. + * @param attrs The attributes set containing the Workspace's customization values. */ public DragLayer(Context context, AttributeSet attrs) { super(context, attrs); diff --git a/src/com/android/launcher2/DragScroller.java b/src/com/android/launcher2/DragScroller.java index c3c251c..6af9c30 100644 --- a/src/com/android/launcher2/DragScroller.java +++ b/src/com/android/launcher2/DragScroller.java @@ -23,4 +23,16 @@ package com.android.launcher2; public interface DragScroller { void scrollLeft(); void scrollRight(); + + /** + * The touch point has entered the scroll area; a scroll is imminent. + * + * @param direction The scroll direction + */ + void onEnterScrollArea(int direction); + + /** + * The touch point has left the scroll area. + */ + void onExitScrollArea(); } diff --git a/src/com/android/launcher2/DragSource.java b/src/com/android/launcher2/DragSource.java index 7c6ca58..11cdcc9 100644 --- a/src/com/android/launcher2/DragSource.java +++ b/src/com/android/launcher2/DragSource.java @@ -24,5 +24,12 @@ import android.view.View; */ public interface DragSource { void setDragController(DragController dragger); + + /** + * Callback from the DragController when it begins drawing the drag view. + * This allows the DragSource to dim or hide the original view. + */ + void onDragViewVisible(); + void onDropCompleted(View target, boolean success); } diff --git a/src/com/android/launcher2/DragView.java b/src/com/android/launcher2/DragView.java index 248712e..947184f 100644 --- a/src/com/android/launcher2/DragView.java +++ b/src/com/android/launcher2/DragView.java @@ -17,41 +17,52 @@ package com.android.launcher2; +import com.android.launcher.R; + +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; import android.content.Context; -import android.content.res.TypedArray; +import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PixelFormat; -import android.graphics.Point; import android.os.IBinder; -import android.util.AttributeSet; -import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; -import android.view.KeyEvent; import android.view.WindowManager; import android.view.WindowManagerImpl; +import android.view.animation.DecelerateInterpolator; -public class DragView extends View implements TweenCallback { - // Number of pixels to add to the dragged item for scaling. Should be even for pixel alignment. - private static final int DRAG_SCALE = 40; - +public class DragView extends View { private Bitmap mBitmap; private Paint mPaint; private int mRegistrationX; private int mRegistrationY; - SymmetricalLinearTween mTween; - private float mScale; - private float mAnimationScale = 1.0f; + private int mDragRegionLeft = 0; + private int mDragRegionTop = 0; + private int mDragRegionWidth; + private int mDragRegionHeight; + + ValueAnimator mAnim; + private float mScale = 1.0f; + private float mOffsetX = 0.0f; + private float mOffsetY = 0.0f; private WindowManager.LayoutParams mLayoutParams; private WindowManager mWindowManager; /** + * A callback to be called the first time this view is drawn. + * This allows the originator of the drag to dim or hide the original view as soon + * as the DragView is drawn. + */ + private Runnable mOnDrawRunnable = null; + + /** * Construct the drag view. * <p> * The registration point is the point inside our view that the touch events should @@ -66,20 +77,95 @@ public class DragView extends View implements TweenCallback { int left, int top, int width, int height) { super(context); + final Resources res = getResources(); + final int dragScale = res.getInteger(R.integer.config_dragViewExtraPixels); + mWindowManager = WindowManagerImpl.getDefault(); - - mTween = new SymmetricalLinearTween(false, 110 /*ms duration*/, this); Matrix scale = new Matrix(); - float scaleFactor = width; - scaleFactor = mScale = (scaleFactor + DRAG_SCALE) / scaleFactor; - scale.setScale(scaleFactor, scaleFactor); + final float scaleFactor = (width + dragScale) / width; + if (scaleFactor != 1.0f) { + scale.setScale(scaleFactor, scaleFactor); + } + + final int offsetX = res.getInteger(R.integer.config_dragViewOffsetX); + final int offsetY = res.getInteger(R.integer.config_dragViewOffsetY); + + // Animate the view into the correct position + mAnim = ValueAnimator.ofFloat(0.0f, 1.0f); + mAnim.setDuration(110); + mAnim.setInterpolator(new DecelerateInterpolator(2.5f)); + mAnim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + final float value = (Float) animation.getAnimatedValue(); + + final int deltaX = (int) ((value * offsetX) - mOffsetX); + final int deltaY = (int) ((value * offsetY) - mOffsetY); + + mOffsetX += deltaX; + mOffsetY += deltaY; + + if (getParent() == null) { + animation.cancel(); + } else { + WindowManager.LayoutParams lp = mLayoutParams; + lp.x += deltaX; + lp.y += deltaY; + mWindowManager.updateViewLayout(DragView.this, lp); + } + } + }); mBitmap = Bitmap.createBitmap(bitmap, left, top, width, height, scale, true); + setDragRegion(0, 0, width, height); // The point in our scaled bitmap that the touch events are located - mRegistrationX = registrationX + (DRAG_SCALE / 2); - mRegistrationY = registrationY + (DRAG_SCALE / 2); + mRegistrationX = registrationX; + mRegistrationY = registrationY; + } + + public void setDragRegion(int left, int top, int width, int height) { + mDragRegionLeft = left; + mDragRegionTop = top; + mDragRegionWidth = width; + mDragRegionHeight = height; + } + + public void setOnDrawRunnable(Runnable r) { + mOnDrawRunnable = r; + } + + public int getScaledDragRegionXOffset() { + return -(int)((mScale - 1.0f) * mDragRegionWidth / 2); + } + + public int getScaledDragRegionWidth() { + return (int)(mScale * mDragRegionWidth); + } + + public int getScaledDragRegionYOffset() { + return -(int)((mScale - 1.0f) * mDragRegionHeight / 2); + } + + public int getScaledDragRegionHeight() { + return (int)(mScale * mDragRegionWidth); + } + + public int getDragRegionLeft() { + return mDragRegionLeft; + } + + public int getDragRegionTop() { + return mDragRegionTop; + } + + public int getDragRegionWidth() { + return mDragRegionWidth; + } + + public int getDragRegionHeight() { + return mDragRegionHeight; } @Override @@ -96,13 +182,15 @@ public class DragView extends View implements TweenCallback { p.setColor(0xaaffffff); canvas.drawRect(0, 0, getWidth(), getHeight(), p); } - float scale = mAnimationScale; - if (scale < 0.999f) { // allow for some float error - float width = mBitmap.getWidth(); - float offset = (width-(width*scale))/2; - canvas.translate(offset, offset); - canvas.scale(scale, scale); + + // Call the callback if we haven't already been detached + if (getParent() != null) { + if (mOnDrawRunnable != null) { + mOnDrawRunnable.run(); + mOnDrawRunnable = null; + } } + canvas.drawBitmap(mBitmap, 0.0f, 0.0f, mPaint); } @@ -112,17 +200,6 @@ public class DragView extends View implements TweenCallback { mBitmap.recycle(); } - public void onTweenValueChanged(float value, float oldValue) { - mAnimationScale = (1.0f+((mScale-1.0f)*value))/mScale; - invalidate(); - } - - public void onTweenStarted() { - } - - public void onTweenFinished() { - } - public void setPaint(Paint paint) { mPaint = paint; invalidate(); @@ -144,7 +221,7 @@ public class DragView extends View implements TweenCallback { lp = new WindowManager.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, - touchX-mRegistrationX, touchY-mRegistrationY, + touchX - mRegistrationX, touchY - mRegistrationY, WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL, WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS @@ -158,8 +235,7 @@ public class DragView extends View implements TweenCallback { mWindowManager.addView(this, lp); - mAnimationScale = 1.0f/mScale; - mTween.start(true); + mAnim.start(); } /** @@ -170,8 +246,8 @@ public class DragView extends View implements TweenCallback { */ void move(int touchX, int touchY) { WindowManager.LayoutParams lp = mLayoutParams; - lp.x = touchX - mRegistrationX; - lp.y = touchY - mRegistrationY; + lp.x = touchX - mRegistrationX + (int) mOffsetX; + lp.y = touchY - mRegistrationY + (int) mOffsetY; mWindowManager.updateViewLayout(this, lp); } diff --git a/src/com/android/launcher2/DropTarget.java b/src/com/android/launcher2/DropTarget.java index 72eb330..308dbbe 100644 --- a/src/com/android/launcher2/DropTarget.java +++ b/src/com/android/launcher2/DropTarget.java @@ -23,6 +23,12 @@ import android.graphics.Rect; * */ public interface DropTarget { + /** + * Used to temporarily disable certain drop targets + * + * @return boolean specifying whether this drop target is currently enabled + */ + boolean isDropEnabled(); /** * Handle an object being dropped on the DropTarget @@ -51,27 +57,28 @@ public interface DropTarget { DragView dragView, Object dragInfo); /** - * Check if a drop action can occur at, or near, the requested location. - * This may be called repeatedly during a drag, so any calls should return - * quickly. - * + * Allows a DropTarget to delegate drag and drop events to another object. + * + * Most subclasses will should just return null from this method. + * * @param source DragSource where the drag started * @param x X coordinate of the drop location * @param y Y coordinate of the drop location - * @param xOffset Horizontal offset with the object being dragged where the - * original touch happened - * @param yOffset Vertical offset with the object being dragged where the - * original touch happened + * @param xOffset Horizontal offset with the object being dragged where the original + * touch happened + * @param yOffset Vertical offset with the object being dragged where the original + * touch happened * @param dragView The DragView that's being dragged around on screen. * @param dragInfo Data associated with the object being dragged - * @return True if the drop will be accepted, false otherwise. + * + * @return The DropTarget to delegate to, or null to not delegate to another object. */ - boolean acceptDrop(DragSource source, int x, int y, int xOffset, int yOffset, + DropTarget getDropTargetDelegate(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo); /** - * Estimate the surface area where this object would land if dropped at the - * given location. + * Check if a drop action can occur at, or near, the requested location. + * This will be called just before onDrop. * * @param source DragSource where the drag started * @param x X coordinate of the drop location @@ -82,13 +89,10 @@ public interface DropTarget { * original touch happened * @param dragView The DragView that's being dragged around on screen. * @param dragInfo Data associated with the object being dragged - * @param recycle {@link Rect} object to be possibly recycled. - * @return Estimated area that would be occupied if object was dropped at - * the given location. Should return null if no estimate is found, - * or if this target doesn't provide estimations. + * @return True if the drop will be accepted, false otherwise. */ - Rect estimateDropLocation(DragSource source, int x, int y, int xOffset, int yOffset, - DragView dragView, Object dragInfo, Rect recycle); + boolean acceptDrop(DragSource source, int x, int y, int xOffset, int yOffset, + DragView dragView, Object dragInfo); // These methods are implemented in Views void getHitRect(Rect outRect); diff --git a/src/com/android/launcher2/FastBitmapDrawable.java b/src/com/android/launcher2/FastBitmapDrawable.java index 226d6d8..1aa8b35 100644 --- a/src/com/android/launcher2/FastBitmapDrawable.java +++ b/src/com/android/launcher2/FastBitmapDrawable.java @@ -16,16 +16,19 @@ package com.android.launcher2; -import android.graphics.drawable.Drawable; -import android.graphics.PixelFormat; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; class FastBitmapDrawable extends Drawable { private Bitmap mBitmap; private int mWidth; private int mHeight; + private final Paint mPaint = new Paint(); FastBitmapDrawable(Bitmap b) { mBitmap = b; @@ -39,7 +42,8 @@ class FastBitmapDrawable extends Drawable { @Override public void draw(Canvas canvas) { - canvas.drawBitmap(mBitmap, 0.0f, 0.0f, null); + final Rect r = getBounds(); + canvas.drawBitmap(mBitmap, r.left, r.top, mPaint); } @Override @@ -49,6 +53,7 @@ class FastBitmapDrawable extends Drawable { @Override public void setAlpha(int alpha) { + mPaint.setAlpha(alpha); } @Override diff --git a/src/com/android/launcher2/Folder.java b/src/com/android/launcher2/Folder.java index 7ff8328..cb450b9 100644 --- a/src/com/android/launcher2/Folder.java +++ b/src/com/android/launcher2/Folder.java @@ -21,11 +21,11 @@ import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; import android.view.View.OnClickListener; +import android.widget.AbsListView; import android.widget.AdapterView; +import android.widget.BaseAdapter; import android.widget.Button; import android.widget.LinearLayout; -import android.widget.AbsListView; -import android.widget.BaseAdapter; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemLongClickListener; @@ -108,13 +108,19 @@ public class Folder extends LinearLayout implements DragSource, OnItemLongClickL return true; } + @Override public void setDragController(DragController dragController) { mDragController = dragController; } + @Override public void onDropCompleted(View target, boolean success) { } + @Override + public void onDragViewVisible() { + } + /** * Sets the adapter used to populate the content area. The adapter must only * contains ShortcutInfo items. @@ -148,7 +154,7 @@ public class Folder extends LinearLayout implements DragSource, OnItemLongClickL void onClose() { final Workspace workspace = mLauncher.getWorkspace(); - workspace.getChildAt(workspace.getCurrentScreen()).requestFocus(); + workspace.getChildAt(workspace.getCurrentPage()).requestFocus(); } void bind(FolderInfo info) { diff --git a/src/com/android/launcher2/FolderIcon.java b/src/com/android/launcher2/FolderIcon.java index 0013644..dae84aa 100644 --- a/src/com/android/launcher2/FolderIcon.java +++ b/src/com/android/launcher2/FolderIcon.java @@ -29,7 +29,7 @@ import com.android.launcher.R; /** * An icon that can appear on in the workspace representing an {@link UserFolder}. */ -public class FolderIcon extends BubbleTextView implements DropTarget { +public class FolderIcon extends DimmableBubbleTextView implements DropTarget { private UserFolderInfo mInfo; private Launcher mLauncher; private Drawable mCloseIcon; @@ -43,15 +43,19 @@ public class FolderIcon extends BubbleTextView implements DropTarget { super(context); } + public boolean isDropEnabled() { + return !((Workspace)getParent().getParent()).isSmall(); + } + static FolderIcon fromXml(int resId, Launcher launcher, ViewGroup group, - UserFolderInfo folderInfo) { + UserFolderInfo folderInfo, IconCache iconCache) { FolderIcon icon = (FolderIcon) LayoutInflater.from(launcher).inflate(resId, group, false); final Resources resources = launcher.getResources(); - Drawable d = resources.getDrawable(R.drawable.ic_launcher_folder); + Drawable d = iconCache.getFullResIcon(resources, R.drawable.ic_launcher_folder); icon.mCloseIcon = d; - icon.mOpenIcon = resources.getDrawable(R.drawable.ic_launcher_folder_open); + icon.mOpenIcon = iconCache.getFullResIcon(resources, R.drawable.ic_launcher_folder_open); icon.setCompoundDrawablesWithIntrinsicBounds(null, d, null, null); icon.setText(folderInfo.title); icon.setTag(folderInfo); @@ -71,11 +75,6 @@ public class FolderIcon extends BubbleTextView implements DropTarget { && item.container != mInfo.id; } - public Rect estimateDropLocation(DragSource source, int x, int y, int xOffset, int yOffset, - DragView dragView, Object dragInfo, Rect recycle) { - return null; - } - public void onDrop(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo) { ShortcutInfo item; @@ -91,7 +90,9 @@ public class FolderIcon extends BubbleTextView implements DropTarget { public void onDragEnter(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo) { - setCompoundDrawablesWithIntrinsicBounds(null, mOpenIcon, null, null); + if (acceptDrop(source, x, y, xOffset, yOffset, dragView, dragInfo)) { + setCompoundDrawablesWithIntrinsicBounds(null, mOpenIcon, null, null); + } } public void onDragOver(DragSource source, int x, int y, int xOffset, int yOffset, @@ -102,4 +103,10 @@ public class FolderIcon extends BubbleTextView implements DropTarget { DragView dragView, Object dragInfo) { setCompoundDrawablesWithIntrinsicBounds(null, mCloseIcon, null, null); } + + @Override + public DropTarget getDropTargetDelegate(DragSource source, int x, int y, int xOffset, int yOffset, + DragView dragView, Object dragInfo) { + return null; + } } diff --git a/src/com/android/launcher2/HolographicOutlineHelper.java b/src/com/android/launcher2/HolographicOutlineHelper.java new file mode 100644 index 0000000..658490a --- /dev/null +++ b/src/com/android/launcher2/HolographicOutlineHelper.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher2; + +import android.graphics.Bitmap; +import android.graphics.BlurMaskFilter; +import android.graphics.Canvas; +import android.graphics.MaskFilter; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.TableMaskFilter; + +public class HolographicOutlineHelper { + private final Paint mHolographicPaint = new Paint(); + private final Paint mBlurPaint = new Paint(); + private final Paint mErasePaint = new Paint(); + + public static final int OUTER_BLUR_RADIUS; + + private static final BlurMaskFilter sThickOuterBlurMaskFilter; + private static final BlurMaskFilter sMediumOuterBlurMaskFilter; + private static final BlurMaskFilter sThinOuterBlurMaskFilter; + private static final BlurMaskFilter sThickInnerBlurMaskFilter; + + static { + final float scale = LauncherApplication.getScreenDensity(); + + OUTER_BLUR_RADIUS = (int) (scale * 6.0f); + + sThickOuterBlurMaskFilter = new BlurMaskFilter(OUTER_BLUR_RADIUS, + BlurMaskFilter.Blur.OUTER); + sMediumOuterBlurMaskFilter = new BlurMaskFilter(scale * 2.0f, BlurMaskFilter.Blur.OUTER); + sThinOuterBlurMaskFilter = new BlurMaskFilter(scale * 1.0f, BlurMaskFilter.Blur.OUTER); + sThickInnerBlurMaskFilter = new BlurMaskFilter(scale * 4.0f, BlurMaskFilter.Blur.NORMAL); + } + + private static final MaskFilter sFineClipTable = TableMaskFilter.CreateClipTable(0, 20); + private static final MaskFilter sCoarseClipTable = TableMaskFilter.CreateClipTable(0, 200); + + private int[] mTempOffset = new int[2]; + + HolographicOutlineHelper() { + mHolographicPaint.setFilterBitmap(true); + mHolographicPaint.setAntiAlias(true); + mBlurPaint.setFilterBitmap(true); + mBlurPaint.setAntiAlias(true); + mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); + mErasePaint.setFilterBitmap(true); + mErasePaint.setAntiAlias(true); + } + + /** + * Returns the interpolated holographic highlight alpha for the effect we want when scrolling + * pages. + */ + public float highlightAlphaInterpolator(float r) { + float maxAlpha = 0.8f; + return (float) Math.pow(maxAlpha * (1.0f - r), 1.5f); + } + + /** + * Returns the interpolated view alpha for the effect we want when scrolling pages. + */ + public float viewAlphaInterpolator(float r) { + final float pivot = 0.95f; + if (r < pivot) { + return (float) Math.pow(r / pivot, 1.5f); + } else { + return 1.0f; + } + } + + /** + * Apply an outer blur to the given bitmap. + * You should use OUTER_BLUR_RADIUS to ensure that the bitmap is big enough to draw + * the blur without clipping. + */ + void applyOuterBlur(Bitmap bitmap, Canvas canvas, int color) { + mBlurPaint.setMaskFilter(sThickOuterBlurMaskFilter); + Bitmap glow = bitmap.extractAlpha(mBlurPaint, mTempOffset); + + // Use the clip table to make the glow heavier closer to the outline + mHolographicPaint.setMaskFilter(sCoarseClipTable); + mHolographicPaint.setAlpha(150); + mHolographicPaint.setColor(color); + canvas.drawBitmap(glow, mTempOffset[0], mTempOffset[1], mHolographicPaint); + glow.recycle(); + } + + /** + * Draws a solid outline around a bitmap, erasing the original pixels. + * + * @param bitmap The bitmap to modify + * @param canvas A canvas on the bitmap + * @param color The color to draw the outline and glow in + * @param removeOrig If true, punch out the original pixels to just leave the outline + */ + void applyExpensiveOuterOutline(Bitmap bitmap, Canvas canvas, int color, boolean removeOrig) { + Bitmap originalImage = null; + if (removeOrig) { + originalImage = bitmap.extractAlpha(); + } + + // Compute an outer blur on the original bitmap + mBlurPaint.setMaskFilter(sMediumOuterBlurMaskFilter); + Bitmap outline = bitmap.extractAlpha(mBlurPaint, mTempOffset); + + // Paint the blurred bitmap back into the canvas. Using the clip table causes any alpha + // pixels above a certain threshold to be rounded up to be fully opaque. This gives the + // effect of a thick outline, with a slight blur on the edge + mHolographicPaint.setColor(color); + mHolographicPaint.setMaskFilter(sFineClipTable); + canvas.drawBitmap(outline, mTempOffset[0], mTempOffset[1], mHolographicPaint); + outline.recycle(); + + if (removeOrig) { + // Finally, punch out the original pixels, leaving just the outline + canvas.drawBitmap(originalImage, 0, 0, mErasePaint); + originalImage.recycle(); + } + } + + /** + * Applies a more expensive and accurate outline to whatever is currently drawn in a specified + * bitmap. + */ + void applyExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color, + int outlineColor) { + // calculate the outer blur first + mBlurPaint.setMaskFilter(sThickOuterBlurMaskFilter); + int[] outerBlurOffset = new int[2]; + Bitmap thickOuterBlur = srcDst.extractAlpha(mBlurPaint, outerBlurOffset); + mBlurPaint.setMaskFilter(sThinOuterBlurMaskFilter); + int[] thinOuterBlurOffset = new int[2]; + Bitmap thinOuterBlur = srcDst.extractAlpha(mBlurPaint, thinOuterBlurOffset); + + // calculate the inner blur + srcDstCanvas.drawColor(0xFF000000, PorterDuff.Mode.SRC_OUT); + mBlurPaint.setMaskFilter(sThickInnerBlurMaskFilter); + int[] thickInnerBlurOffset = new int[2]; + Bitmap thickInnerBlur = srcDst.extractAlpha(mBlurPaint, thickInnerBlurOffset); + + // mask out the inner blur + srcDstCanvas.setBitmap(thickInnerBlur); + srcDstCanvas.drawBitmap(srcDst, -thickInnerBlurOffset[0], + -thickInnerBlurOffset[1], mErasePaint); + srcDstCanvas.drawRect(0, 0, -thickInnerBlurOffset[0], thickInnerBlur.getHeight(), + mErasePaint); + srcDstCanvas.drawRect(0, 0, thickInnerBlur.getWidth(), -thickInnerBlurOffset[1], + mErasePaint); + + // draw the inner and outer blur + srcDstCanvas.setBitmap(srcDst); + srcDstCanvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR); + mHolographicPaint.setColor(color); + srcDstCanvas.drawBitmap(thickInnerBlur, thickInnerBlurOffset[0], thickInnerBlurOffset[1], + mHolographicPaint); + srcDstCanvas.drawBitmap(thickOuterBlur, outerBlurOffset[0], outerBlurOffset[1], + mHolographicPaint); + + // draw the bright outline + mHolographicPaint.setColor(outlineColor); + srcDstCanvas.drawBitmap(thinOuterBlur, thinOuterBlurOffset[0], thinOuterBlurOffset[1], + mHolographicPaint); + + // cleanup + thinOuterBlur.recycle(); + thickOuterBlur.recycle(); + thickInnerBlur.recycle(); + } +} diff --git a/src/com/android/launcher2/IconCache.java b/src/com/android/launcher2/IconCache.java index 81a786c..ae8c98a 100644 --- a/src/com/android/launcher2/IconCache.java +++ b/src/com/android/launcher2/IconCache.java @@ -16,13 +16,17 @@ package com.android.launcher2; +import com.android.launcher.R; + import android.content.ComponentName; import android.content.Intent; 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.drawable.Drawable; +import android.util.DisplayMetrics; import java.util.HashMap; @@ -46,16 +50,49 @@ public class IconCache { private final Utilities.BubbleText mBubble; private final HashMap<ComponentName, CacheEntry> mCache = new HashMap<ComponentName, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY); + private int mIconDpi; public IconCache(LauncherApplication context) { mContext = context; mPackageManager = context.getPackageManager(); mBubble = new Utilities.BubbleText(context); + if (LauncherApplication.isScreenXLarge()) { + mIconDpi = DisplayMetrics.DENSITY_HIGH; + } else { + mIconDpi = context.getResources().getDisplayMetrics().densityDpi; + } + // need to set mIconDpi before getting default icon mDefaultIcon = makeDefaultIcon(); } + public Drawable getFullResDefaultActivityIcon() { + return getFullResIcon(Resources.getSystem(), + com.android.internal.R.drawable.sym_def_app_icon); + } + + public Drawable getFullResIcon(Resources resources, int iconId) { + return resources.getDrawableForDensity(iconId, mIconDpi); + } + + public Drawable getFullResIcon(ResolveInfo info, PackageManager packageManager) { + Resources resources; + try { + resources = packageManager.getResourcesForApplication( + info.activityInfo.applicationInfo); + } catch (PackageManager.NameNotFoundException e) { + resources = null; + } + if (resources != null) { + int iconId = info.activityInfo.getIconResource(); + if (iconId != 0) { + return getFullResIcon(resources, iconId); + } + } + return getFullResDefaultActivityIcon(); + } + private Bitmap makeDefaultIcon() { - Drawable d = mPackageManager.getDefaultActivityIcon(); + Drawable d = getFullResDefaultActivityIcon(); Bitmap b = Bitmap.createBitmap(Math.max(d.getIntrinsicWidth(), 1), Math.max(d.getIntrinsicHeight(), 1), Bitmap.Config.ARGB_8888); @@ -140,7 +177,7 @@ public class IconCache { entry.title = info.activityInfo.name; } entry.icon = Utilities.createIconBitmap( - info.activityInfo.loadIcon(mPackageManager), mContext); + getFullResIcon(info, mPackageManager), mContext); } return entry; } diff --git a/src/com/android/launcher2/InstallShortcutReceiver.java b/src/com/android/launcher2/InstallShortcutReceiver.java index 3fc568b..ac50f66 100644 --- a/src/com/android/launcher2/InstallShortcutReceiver.java +++ b/src/com/android/launcher2/InstallShortcutReceiver.java @@ -16,19 +16,23 @@ package com.android.launcher2; +import java.util.ArrayList; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.ContentResolver; -import android.database.Cursor; import android.widget.Toast; import com.android.launcher.R; public class InstallShortcutReceiver extends BroadcastReceiver { - private static final String ACTION_INSTALL_SHORTCUT = + public static final String ACTION_INSTALL_SHORTCUT = "com.android.launcher.action.INSTALL_SHORTCUT"; + // A mime-type representing shortcut data + public static final String SHORTCUT_MIMETYPE = + "com.android.launcher/shortcut"; + private final int[] mCoordinates = new int[2]; public void onReceive(Context context, Intent data) { @@ -50,11 +54,6 @@ public class InstallShortcutReceiver extends BroadcastReceiver { String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); if (findEmptyCell(context, mCoordinates, screen)) { - CellLayout.CellInfo cell = new CellLayout.CellInfo(); - cell.cellX = mCoordinates[0]; - cell.cellY = mCoordinates[1]; - cell.screen = screen; - Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); if (intent.getAction() == null) { @@ -66,7 +65,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { boolean duplicate = data.getBooleanExtra(Launcher.EXTRA_SHORTCUT_DUPLICATE, true); if (duplicate || !LauncherModel.shortcutExists(context, name, intent)) { ((LauncherApplication)context.getApplicationContext()).getModel() - .addShortcut(context, data, cell, true); + .addShortcut(context, data, screen, mCoordinates[0], mCoordinates[1], true); Toast.makeText(context, context.getString(R.string.shortcut_installed, name), Toast.LENGTH_SHORT).show(); } else { @@ -84,40 +83,26 @@ public class InstallShortcutReceiver extends BroadcastReceiver { } private static boolean findEmptyCell(Context context, int[] xy, int screen) { - final int xCount = Launcher.NUMBER_CELLS_X; - final int yCount = Launcher.NUMBER_CELLS_Y; - + final int xCount = LauncherModel.getCellCountX(); + final int yCount = LauncherModel.getCellCountY(); boolean[][] occupied = new boolean[xCount][yCount]; - final ContentResolver cr = context.getContentResolver(); - Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, - new String[] { LauncherSettings.Favorites.CELLX, LauncherSettings.Favorites.CELLY, - LauncherSettings.Favorites.SPANX, LauncherSettings.Favorites.SPANY }, - LauncherSettings.Favorites.SCREEN + "=?", - new String[] { String.valueOf(screen) }, null); - - final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); - final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); - final int spanXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANX); - final int spanYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANY); - - try { - while (c.moveToNext()) { - int cellX = c.getInt(cellXIndex); - int cellY = c.getInt(cellYIndex); - int spanX = c.getInt(spanXIndex); - int spanY = c.getInt(spanYIndex); - + ArrayList<ItemInfo> items = LauncherModel.getItemsInLocalCoordinates(context); + ItemInfo item = null; + int cellX, cellY, spanX, spanY; + for (int i = 0; i < items.size(); ++i) { + item = items.get(i); + if (item.screen == screen) { + cellX = item.cellX; + cellY = item.cellY; + spanX = item.spanX; + spanY = item.spanY; for (int x = cellX; x < cellX + spanX && x < xCount; x++) { for (int y = cellY; y < cellY + spanY && y < yCount; y++) { occupied[x][y] = true; } } } - } catch (Exception e) { - return false; - } finally { - c.close(); } return CellLayout.findVacantCell(xy, 1, 1, xCount, yCount, occupied); diff --git a/src/com/android/launcher2/InstallWidgetReceiver.java b/src/com/android/launcher2/InstallWidgetReceiver.java new file mode 100644 index 0000000..970c6a5 --- /dev/null +++ b/src/com/android/launcher2/InstallWidgetReceiver.java @@ -0,0 +1,195 @@ +/* + * 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 java.util.List; + +import android.appwidget.AppWidgetProviderInfo; +import android.content.ClipData; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.database.DataSetObserver; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.TextView; + +import com.android.launcher.R; + + +/** + * We will likely flesh this out later, to handle allow external apps to place widgets, but for now, + * we just want to expose the action around for checking elsewhere. + */ +public class InstallWidgetReceiver { + public static final String ACTION_INSTALL_WIDGET = + "com.android.launcher.action.INSTALL_WIDGET"; + public static final String ACTION_SUPPORTS_CLIPDATA_MIMETYPE = + "com.android.launcher.action.SUPPORTS_CLIPDATA_MIMETYPE"; + + // Currently not exposed. Put into Intent when we want to make it public. + // TEMP: Should we call this "EXTRA_APPWIDGET_PROVIDER"? + public static final String EXTRA_APPWIDGET_COMPONENT = + "com.android.launcher.extra.widget.COMPONENT"; + public static final String EXTRA_APPWIDGET_CONFIGURATION_DATA_MIME_TYPE = + "com.android.launcher.extra.widget.CONFIGURATION_DATA_MIME_TYPE"; + public static final String EXTRA_APPWIDGET_CONFIGURATION_DATA = + "com.android.launcher.extra.widget.CONFIGURATION_DATA"; + + /** + * A simple data class that contains per-item information that the adapter below can reference. + */ + public static class WidgetMimeTypeHandlerData { + public ResolveInfo resolveInfo; + public AppWidgetProviderInfo widgetInfo; + + public WidgetMimeTypeHandlerData(ResolveInfo rInfo, AppWidgetProviderInfo wInfo) { + resolveInfo = rInfo; + widgetInfo = wInfo; + } + } + + /** + * The ListAdapter which presents all the valid widgets that can be created for a given drop. + */ + public static class WidgetListAdapter implements ListAdapter, DialogInterface.OnClickListener { + private LayoutInflater mInflater; + private Launcher mLauncher; + private String mMimeType; + private ClipData mClipData; + private List<WidgetMimeTypeHandlerData> mActivities; + private CellLayout mTargetLayout; + private int mTargetLayoutScreen; + private int[] mTargetLayoutPos; + + public WidgetListAdapter(Launcher l, String mimeType, ClipData data, + List<WidgetMimeTypeHandlerData> list, CellLayout target, + int targetScreen, int[] targetPos) { + mLauncher = l; + mMimeType = mimeType; + mClipData = data; + mActivities = list; + mTargetLayout = target; + mTargetLayoutScreen = targetScreen; + mTargetLayoutPos = targetPos; + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + } + + @Override + public int getCount() { + return mActivities.size(); + } + + @Override + public Object getItem(int position) { + return null; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final Context context = parent.getContext(); + final PackageManager packageManager = context.getPackageManager(); + + // Lazy-create inflater + if (mInflater == null) { + mInflater = LayoutInflater.from(context); + } + + // Use the convert-view where possible + if (convertView == null) { + convertView = mInflater.inflate(R.layout.external_widget_drop_list_item, parent, + false); + } + + final WidgetMimeTypeHandlerData data = mActivities.get(position); + final ResolveInfo resolveInfo = data.resolveInfo; + final AppWidgetProviderInfo widgetInfo = data.widgetInfo; + + // Set the icon + Drawable d = resolveInfo.loadIcon(packageManager); + ImageView i = (ImageView) convertView.findViewById(R.id.provider_icon); + i.setImageDrawable(d); + + // Set the text + final CharSequence component = resolveInfo.loadLabel(packageManager); + final int[] widgetSpan = new int[2]; + mTargetLayout.rectToCell(widgetInfo.minWidth, widgetInfo.minHeight, widgetSpan); + TextView t = (TextView) convertView.findViewById(R.id.provider); + t.setText(context.getString(R.string.external_drop_widget_pick_format, + component, widgetSpan[0], widgetSpan[1])); + + return convertView; + } + + @Override + public int getItemViewType(int position) { + return 0; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public boolean isEmpty() { + return mActivities.isEmpty(); + } + + @Override + public boolean areAllItemsEnabled() { + return false; + } + + @Override + public boolean isEnabled(int position) { + return true; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + final LauncherModel model = mLauncher.getModel(); + final AppWidgetProviderInfo widgetInfo = mActivities.get(which).widgetInfo; + + final PendingAddWidgetInfo createInfo = new PendingAddWidgetInfo(widgetInfo, mMimeType, + mClipData); + mLauncher.addAppWidgetFromDrop(createInfo, mTargetLayoutScreen, mTargetLayoutPos); + } + } +} diff --git a/src/com/android/launcher2/InterruptibleInOutAnimator.java b/src/com/android/launcher2/InterruptibleInOutAnimator.java new file mode 100644 index 0000000..570b9e7 --- /dev/null +++ b/src/com/android/launcher2/InterruptibleInOutAnimator.java @@ -0,0 +1,132 @@ +/* + * 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.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.util.Log; + +/** + * A convenience class for two-way animations, e.g. a fadeIn/fadeOut animation. + * With a regular ValueAnimator, if you call reverse to show the 'out' animation, you'll get + * a frame-by-frame mirror of the 'in' animation -- i.e., the interpolated values will + * be exactly reversed. Using this class, both the 'in' and the 'out' animation use the + * interpolator in the same direction. + */ +public class InterruptibleInOutAnimator { + private long mOriginalDuration; + private float mOriginalFromValue; + private float mOriginalToValue; + private ValueAnimator mAnimator; + + private boolean mFirstRun = true; + + private Object mTag = null; + + private static final int STOPPED = 0; + private static final int IN = 1; + private static final int OUT = 2; + + // TODO: This isn't really necessary, but is here to help diagnose a bug in the drag viz + private int mDirection = STOPPED; + + public InterruptibleInOutAnimator(long duration, float fromValue, float toValue) { + mAnimator = ValueAnimator.ofFloat(fromValue, toValue).setDuration(duration); + mOriginalDuration = duration; + mOriginalFromValue = fromValue; + mOriginalToValue = toValue; + + mAnimator.addListener(new LauncherAnimatorListenerAdapter() { + @Override + public void onAnimationEndOrCancel(Animator animation) { + mDirection = STOPPED; + } + }); + } + + private void animate(int direction) { + final long currentPlayTime = mAnimator.getCurrentPlayTime(); + final float toValue = (direction == IN) ? mOriginalToValue : mOriginalFromValue; + final float startValue = mFirstRun ? mOriginalFromValue : + ((Float) mAnimator.getAnimatedValue()).floatValue(); + + // Make sure it's stopped before we modify any values + cancel(); + + // TODO: We don't really need to do the animation if startValue == toValue, but + // somehow that doesn't seem to work, possibly a quirk of the animation framework + mDirection = direction; + + // Ensure we don't calculate a non-sensical duration + long duration = mOriginalDuration - currentPlayTime; + mAnimator.setDuration(Math.max(0, Math.min(duration, mOriginalDuration))); + + mAnimator.setFloatValues(startValue, toValue); + mAnimator.start(); + mFirstRun = false; + } + + public void cancel() { + mAnimator.cancel(); + mDirection = STOPPED; + } + + public void end() { + mAnimator.end(); + mDirection = STOPPED; + } + + /** + * Return true when the animation is not running and it hasn't even been started. + */ + public boolean isStopped() { + return mDirection == STOPPED; + } + + /** + * This is the equivalent of calling Animator.start(), except that it can be called when + * the animation is running in the opposite direction, in which case we reverse + * direction and animate for a correspondingly shorter duration. + */ + public void animateIn() { + animate(IN); + } + + /** + * This is the roughly the equivalent of calling Animator.reverse(), except that it uses the + * same interpolation curve as animateIn(), rather than mirroring it. Also, like animateIn(), + * if the animation is currently running in the opposite direction, we reverse + * direction and animate for a correspondingly shorter duration. + */ + public void animateOut() { + animate(OUT); + } + + public void setTag(Object tag) { + mTag = tag; + } + + public Object getTag() { + return mTag; + } + + public ValueAnimator getAnimator() { + return mAnimator; + } +} diff --git a/src/com/android/launcher2/ItemInfo.java b/src/com/android/launcher2/ItemInfo.java index a96d9ae..dc45750 100644 --- a/src/com/android/launcher2/ItemInfo.java +++ b/src/com/android/launcher2/ItemInfo.java @@ -112,6 +112,11 @@ class ItemInfo { } } + void updateValuesWithCoordinates(ContentValues values, int cellX, int cellY) { + values.put(LauncherSettings.Favorites.CELLX, cellX); + values.put(LauncherSettings.Favorites.CELLY, cellY); + } + static byte[] flattenBitmap(Bitmap bitmap) { // Try go guesstimate how much space the icon will take when serialized // to avoid unnecessary allocations/copies during the write. diff --git a/src/com/android/launcher2/Launcher.java b/src/com/android/launcher2/Launcher.java index b361ab5..2a2d364 100644 --- a/src/com/android/launcher2/Launcher.java +++ b/src/com/android/launcher2/Launcher.java @@ -1,3 +1,4 @@ + /* * Copyright (C) 2008 The Android Open Source Project * @@ -16,44 +17,63 @@ package com.android.launcher2; -import com.android.common.Search; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.SearchManager; import android.app.StatusBarManager; import android.app.WallpaperManager; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; +import android.content.ClipData; +import android.content.ClipDescription; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.Intent.ShortcutIconResource; import android.content.IntentFilter; +import android.content.Intent.ShortcutIconResource; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.database.ContentObserver; import android.graphics.Bitmap; -import android.graphics.Rect; import android.graphics.Canvas; -import android.graphics.drawable.Drawable; +import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.os.Handler; +import android.os.Message; import android.os.Parcelable; import android.os.SystemClock; import android.os.SystemProperties; import android.provider.LiveFolders; +import android.provider.Settings; +import android.speech.RecognizerIntent; import android.text.Selection; import android.text.SpannableStringBuilder; import android.text.TextUtils; @@ -65,34 +85,35 @@ import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.WindowManager; import android.view.View.OnLongClickListener; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; import android.view.inputmethod.InputMethodManager; +import android.widget.Advanceable; import android.widget.EditText; -import android.widget.TextView; -import android.widget.Toast; import android.widget.ImageView; -import android.widget.PopupWindow; import android.widget.LinearLayout; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProviderInfo; - -import java.util.ArrayList; -import java.util.List; -import java.util.HashMap; -import java.io.DataOutputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.DataInputStream; +import android.widget.PopupWindow; +import android.widget.TabHost; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.TabHost.OnTabChangeListener; +import android.widget.TabHost.TabContentFactory; +import com.android.common.Search; import com.android.launcher.R; + /** * Default launcher application. */ public final class Launcher extends Activity - implements View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks, AllAppsView.Watcher { + implements View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks, + AllAppsView.Watcher, View.OnTouchListener { static final String TAG = "Launcher"; static final boolean LOGD = false; @@ -100,8 +121,6 @@ public final class Launcher extends Activity static final boolean DEBUG_WIDGETS = false; static final boolean DEBUG_USER_INTERFACE = false; - private static final int WALLPAPER_SCREENS_SPAN = 2; - private static final int MENU_GROUP_ADD = 1; private static final int MENU_GROUP_WALLPAPER = MENU_GROUP_ADD + 1; @@ -125,8 +144,6 @@ public final class Launcher extends Activity static final int SCREEN_COUNT = 5; static final int DEFAULT_SCREEN = 2; - static final int NUMBER_CELLS_X = 4; - static final int NUMBER_CELLS_Y = 4; static final int DIALOG_CREATE_SHORTCUT = 1; static final int DIALOG_RENAME_FOLDER = 2; @@ -135,8 +152,8 @@ public final class Launcher extends Activity // Type: int private static final String RUNTIME_STATE_CURRENT_SCREEN = "launcher.current_screen"; - // Type: boolean - private static final String RUNTIME_STATE_ALL_APPS_FOLDER = "launcher.all_apps_folder"; + // Type: int + private static final String RUNTIME_STATE = "launcher.state"; // Type: long private static final String RUNTIME_STATE_USER_FOLDERS = "launcher.user_folder"; // Type: int @@ -145,21 +162,24 @@ public final class Launcher extends Activity private static final String RUNTIME_STATE_PENDING_ADD_CELL_X = "launcher.add_cellX"; // Type: int private static final String RUNTIME_STATE_PENDING_ADD_CELL_Y = "launcher.add_cellY"; - // Type: int - private static final String RUNTIME_STATE_PENDING_ADD_SPAN_X = "launcher.add_spanX"; - // Type: int - private static final String RUNTIME_STATE_PENDING_ADD_SPAN_Y = "launcher.add_spanY"; - // Type: int - private static final String RUNTIME_STATE_PENDING_ADD_COUNT_X = "launcher.add_countX"; - // Type: int - private static final String RUNTIME_STATE_PENDING_ADD_COUNT_Y = "launcher.add_countY"; - // Type: int[] - private static final String RUNTIME_STATE_PENDING_ADD_OCCUPIED_CELLS = "launcher.add_occupied_cells"; // Type: boolean private static final String RUNTIME_STATE_PENDING_FOLDER_RENAME = "launcher.rename_folder"; // Type: long private static final String RUNTIME_STATE_PENDING_FOLDER_RENAME_ID = "launcher.rename_folder_id"; + // tags for the customization tabs + private static final String WIDGETS_TAG = "widgets"; + private static final String APPLICATIONS_TAG = "applications"; + private static final String SHORTCUTS_TAG = "shortcuts"; + private static final String WALLPAPERS_TAG = "wallpapers"; + + private static final String TOOLBAR_ICON_METADATA_NAME = "com.android.launcher.toolbar_icon"; + + /** The different states that Launcher can be in. */ + private enum State { WORKSPACE, ALL_APPS, CUSTOMIZE, OVERVIEW }; + private State mState = State.WORKSPACE; + private AnimatorSet mStateAnimation; + static final int APPWIDGET_HOST_ID = 1024; private static final Object sLock = new Object(); @@ -177,14 +197,22 @@ public final class Launcher extends Activity private AppWidgetManager mAppWidgetManager; private LauncherAppWidgetHost mAppWidgetHost; - private CellLayout.CellInfo mAddItemCellInfo; - private CellLayout.CellInfo mMenuAddInfo; - private final int[] mCellCoordinates = new int[2]; + private int mAddScreen = -1; + private int mAddIntersectCellX = -1; + private int mAddIntersectCellY = -1; + private int[] mAddDropPosition; + private int[] mTmpAddItemCellCoordinates = new int[2]; + private FolderInfo mFolderInfo; private DeleteZone mDeleteZone; private HandleView mHandleView; private AllAppsView mAllAppsGrid; + private TabHost mHomeCustomizationDrawer; + private boolean mAutoAdvanceRunning = false; + + private PagedView mAllAppsPagedView = null; + private CustomizePagedView mCustomizePagedView = null; private Bundle mSavedState; @@ -201,26 +229,46 @@ public final class Launcher extends Activity private LauncherModel mModel; private IconCache mIconCache; + private boolean mUserPresent = true; + private boolean mVisible = false; + private boolean mAttached = false; private static LocaleConfiguration sLocaleConfiguration = null; private ArrayList<ItemInfo> mDesktopItems = new ArrayList<ItemInfo>(); + private static HashMap<Long, FolderInfo> sFolders = new HashMap<Long, FolderInfo>(); private ImageView mPreviousView; private ImageView mNextView; // Hotseats (quick-launch icons next to AllApps) - private static final int NUM_HOTSEATS = 2; private String[] mHotseatConfig = null; private Intent[] mHotseats = null; private Drawable[] mHotseatIcons = null; private CharSequence[] mHotseatLabels = null; + private Intent mAppMarketIntent = null; + + // Related to the auto-advancing of widgets + private final int ADVANCE_MSG = 1; + private final int mAdvanceInterval = 20000; + private final int mAdvanceStagger = 250; + private long mAutoAdvanceSentTime; + private long mAutoAdvanceTimeLeft = -1; + private HashMap<View, AppWidgetProviderInfo> mWidgetsToAdvance = + new HashMap<View, AppWidgetProviderInfo>(); + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + if (LauncherApplication.isInPlaceRotationEnabled()) { + // hide the status bar (temporary until we get the status bar design figured out) + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); + this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR); + } + LauncherApplication app = ((LauncherApplication)getApplication()); mModel = app.setLauncher(this); mIconCache = app.getIconCache(); @@ -239,8 +287,70 @@ public final class Launcher extends Activity loadHotseats(); checkForLocaleChange(); setWallpaperDimension(); - setContentView(R.layout.launcher); + mHomeCustomizationDrawer = (TabHost) findViewById(com.android.internal.R.id.tabhost); + if (mHomeCustomizationDrawer != null) { + mHomeCustomizationDrawer.setup(); + + // share the same customization workspace across all the tabs + mCustomizePagedView = (CustomizePagedView) mInflater.inflate( + R.layout.customization_drawer, mHomeCustomizationDrawer, false); + TabContentFactory contentFactory = new TabContentFactory() { + public View createTabContent(String tag) { + return mCustomizePagedView; + } + }; + + String widgetsLabel = getString(R.string.widgets_tab_label); + mHomeCustomizationDrawer.addTab(mHomeCustomizationDrawer.newTabSpec(WIDGETS_TAG) + .setIndicator(widgetsLabel).setContent(contentFactory)); + String applicationsLabel = getString(R.string.applications_tab_label); + mHomeCustomizationDrawer.addTab(mHomeCustomizationDrawer.newTabSpec(APPLICATIONS_TAG) + .setIndicator(applicationsLabel).setContent(contentFactory)); + String wallpapersLabel = getString(R.string.wallpapers_tab_label); + mHomeCustomizationDrawer.addTab(mHomeCustomizationDrawer.newTabSpec(WALLPAPERS_TAG) + .setIndicator(wallpapersLabel).setContent(contentFactory)); + String shortcutsLabel = getString(R.string.shortcuts_tab_label); + mHomeCustomizationDrawer.addTab(mHomeCustomizationDrawer.newTabSpec(SHORTCUTS_TAG) + .setIndicator(shortcutsLabel).setContent(contentFactory)); + mHomeCustomizationDrawer.setOnTabChangedListener(new OnTabChangeListener() { + public void onTabChanged(String tabId) { + // animate the changing of the tab content by fading pages in and out + final Resources res = getResources(); + final int duration = res.getInteger(R.integer.config_tabTransitionTime); + final float alpha = mCustomizePagedView.getAlpha(); + ValueAnimator alphaAnim = ObjectAnimator.ofFloat(mCustomizePagedView, + "alpha", alpha, 0.0f); + alphaAnim.setDuration(duration); + alphaAnim.addListener(new LauncherAnimatorListenerAdapter() { + @Override + public void onAnimationEndOrCancel(Animator animation) { + String tag = mHomeCustomizationDrawer.getCurrentTabTag(); + if (tag == WIDGETS_TAG) { + mCustomizePagedView.setCustomizationFilter( + CustomizePagedView.CustomizationType.WidgetCustomization); + } else if (tag == APPLICATIONS_TAG) { + mCustomizePagedView.setCustomizationFilter( + CustomizePagedView.CustomizationType.ApplicationCustomization); + } else if (tag == WALLPAPERS_TAG) { + mCustomizePagedView.setCustomizationFilter( + CustomizePagedView.CustomizationType.WallpaperCustomization); + } else if (tag == SHORTCUTS_TAG) { + mCustomizePagedView.setCustomizationFilter( + CustomizePagedView.CustomizationType.ShortcutCustomization); + } + + final float alpha = mCustomizePagedView.getAlpha(); + ValueAnimator alphaAnim = ObjectAnimator.ofFloat( + mCustomizePagedView, "alpha", alpha, 1.0f); + alphaAnim.setDuration(duration); + alphaAnim.start(); + } + }); + alphaAnim.start(); + } + }); + } setupViews(); registerContentObservers(); @@ -308,6 +418,7 @@ public final class Launcher extends Activity final LocaleConfiguration localeConfiguration = sLocaleConfiguration; new Thread("WriteLocaleConfiguration") { + @Override public void run() { writeConfiguration(Launcher.this, localeConfiguration); } @@ -383,11 +494,12 @@ public final class Launcher extends Activity WallpaperManager wpm = (WallpaperManager)getSystemService(WALLPAPER_SERVICE); Display display = getWindowManager().getDefaultDisplay(); - boolean isPortrait = display.getWidth() < display.getHeight(); - - final int width = isPortrait ? display.getWidth() : display.getHeight(); - final int height = isPortrait ? display.getHeight() : display.getWidth(); - wpm.suggestDesiredDimensions(width * WALLPAPER_SCREENS_SPAN, height); + // TODO: Put back when we decide about scrolling the wallpaper + // boolean isPortrait = display.getWidth() < display.getHeight(); + // final int width = isPortrait ? display.getWidth() : display.getHeight(); + // final int height = isPortrait ? display.getHeight() : display.getWidth(); + wpm.suggestDesiredDimensions(Math.max(display.getWidth(), display.getHeight()), + Math.max(display.getWidth(), display.getHeight())); } // Note: This doesn't do all the client-id magic that BrowserProvider does @@ -446,7 +558,7 @@ public final class Launcher extends Activity // note: if the user launches this without a default set, she // will always be taken to the default URL above; this is // unavoidable as we must specify a valid URL in order for the - // chooser to appear, and once the user selects something, that + // chooser to appear, and once the user selects something, that // URL is unavoidably sent to the chosen app. } else { try { @@ -456,7 +568,7 @@ public final class Launcher extends Activity // bogus; leave intent=null } } - + if (intent == null) { mHotseats[i] = null; mHotseatLabels[i] = getText(R.string.activity_not_found); @@ -464,15 +576,15 @@ public final class Launcher extends Activity } if (LOGD) { - Log.d(TAG, "loadHotseats: hotseat " + i - + " initial intent=[" + Log.d(TAG, "loadHotseats: hotseat " + i + + " initial intent=[" + intent.toUri(Intent.URI_INTENT_SCHEME) + "]"); } ResolveInfo bestMatch = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); List<ResolveInfo> allMatches = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); - if (LOGD) { + if (LOGD) { Log.d(TAG, "Best match for intent: " + bestMatch); Log.d(TAG, "All matches: "); for (ResolveInfo ri : allMatches) { @@ -481,8 +593,8 @@ public final class Launcher extends Activity } // did this resolve to a single app, or the resolver? if (allMatches.size() == 0 || bestMatch == null) { - // can't find any activity to handle this. let's leave the - // intent as-is and let Launcher show a toast when it fails + // can't find any activity to handle this. let's leave the + // intent as-is and let Launcher show a toast when it fails // to launch. mHotseats[i] = intent; @@ -498,7 +610,7 @@ public final class Launcher extends Activity break; } } - + if (!found) { if (LOGD) Log.d(TAG, "Multiple options, no default yet"); // the bestMatch is probably the ResolveActivity, meaning the @@ -523,8 +635,8 @@ public final class Launcher extends Activity } if (LOGD) { - Log.d(TAG, "loadHotseats: hotseat " + i - + " final intent=[" + Log.d(TAG, "loadHotseats: hotseat " + i + + " final intent=[" + ((mHotseats[i] == null) ? "null" : mHotseats[i].toUri(Intent.URI_INTENT_SCHEME)) @@ -545,28 +657,30 @@ public final class Launcher extends Activity // For example, the user would PICK_SHORTCUT for "Music playlist", and we // launch over to the Music app to actually CREATE_SHORTCUT. - if (resultCode == RESULT_OK && mAddItemCellInfo != null) { + if (resultCode == RESULT_OK && mAddScreen != -1) { switch (requestCode) { case REQUEST_PICK_APPLICATION: - completeAddApplication(this, data, mAddItemCellInfo); + completeAddApplication( + this, data, mAddScreen, mAddIntersectCellX, mAddIntersectCellY); break; case REQUEST_PICK_SHORTCUT: processShortcut(data); break; case REQUEST_CREATE_SHORTCUT: - completeAddShortcut(data, mAddItemCellInfo); + completeAddShortcut(data, mAddScreen, mAddIntersectCellX, mAddIntersectCellY); break; case REQUEST_PICK_LIVE_FOLDER: addLiveFolder(data); break; case REQUEST_CREATE_LIVE_FOLDER: - completeAddLiveFolder(data, mAddItemCellInfo); + completeAddLiveFolder(data, mAddScreen, mAddIntersectCellX, mAddIntersectCellY); break; case REQUEST_PICK_APPWIDGET: - addAppWidget(data); + addAppWidgetFromPick(data); break; case REQUEST_CREATE_APPWIDGET: - completeAddAppWidget(data, mAddItemCellInfo); + int appWidgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); + completeAddAppWidget(appWidgetId, mAddScreen); break; case REQUEST_PICK_WALLPAPER: // We just wanted the activity result here so we can clear mWaitingForResult @@ -593,14 +707,22 @@ public final class Launcher extends Activity mRestoring = false; mOnResumeNeedsLoad = false; } + // When we resume Launcher, a different Activity might be responsible for the app + // market intent, so refresh the icon + updateAppMarketIcon(); } @Override protected void onPause() { super.onPause(); + // Some launcher layouts don't have a previous and next view + if (mPreviousView != null) { + dismissPreview(mPreviousView); + } + if (mNextView != null) { + dismissPreview(mNextView); + } mPaused = true; - dismissPreview(mPreviousView); - dismissPreview(mNextView); mDragController.cancelDrag(); } @@ -675,6 +797,22 @@ public final class Launcher extends Activity } /** + * Given the integer (ordinal) value of a State enum instance, convert it to a variable of type + * State + */ + private static State intToState(int stateOrdinal) { + State state = State.WORKSPACE; + final State[] stateValues = State.values(); + for (int i = 0; i < stateValues.length; i++) { + if (stateValues[i].ordinal() == stateOrdinal) { + state = stateValues[i]; + break; + } + } + return state; + } + + /** * Restores the previous state, if it exists. * * @param savedState The previous state. @@ -684,30 +822,25 @@ public final class Launcher extends Activity return; } - final boolean allApps = savedState.getBoolean(RUNTIME_STATE_ALL_APPS_FOLDER, false); - if (allApps) { + State state = intToState(savedState.getInt(RUNTIME_STATE, State.WORKSPACE.ordinal())); + + if (state == State.ALL_APPS) { showAllApps(false); + } else if (state == State.CUSTOMIZE) { + showCustomizationDrawer(false); } final int currentScreen = savedState.getInt(RUNTIME_STATE_CURRENT_SCREEN, -1); if (currentScreen > -1) { - mWorkspace.setCurrentScreen(currentScreen); + mWorkspace.setCurrentPage(currentScreen); } final int addScreen = savedState.getInt(RUNTIME_STATE_PENDING_ADD_SCREEN, -1); + if (addScreen > -1) { - mAddItemCellInfo = new CellLayout.CellInfo(); - final CellLayout.CellInfo addItemCellInfo = mAddItemCellInfo; - addItemCellInfo.valid = true; - addItemCellInfo.screen = addScreen; - addItemCellInfo.cellX = savedState.getInt(RUNTIME_STATE_PENDING_ADD_CELL_X); - addItemCellInfo.cellY = savedState.getInt(RUNTIME_STATE_PENDING_ADD_CELL_Y); - addItemCellInfo.spanX = savedState.getInt(RUNTIME_STATE_PENDING_ADD_SPAN_X); - addItemCellInfo.spanY = savedState.getInt(RUNTIME_STATE_PENDING_ADD_SPAN_Y); - addItemCellInfo.findVacantCellsFromOccupied( - savedState.getBooleanArray(RUNTIME_STATE_PENDING_ADD_OCCUPIED_CELLS), - savedState.getInt(RUNTIME_STATE_PENDING_ADD_COUNT_X), - savedState.getInt(RUNTIME_STATE_PENDING_ADD_COUNT_Y)); + mAddScreen = addScreen; + mAddIntersectCellX = savedState.getInt(RUNTIME_STATE_PENDING_ADD_CELL_X); + mAddIntersectCellY = savedState.getInt(RUNTIME_STATE_PENDING_ADD_CELL_Y); mRestoring = true; } @@ -723,7 +856,7 @@ public final class Launcher extends Activity * Finds all the views we need and configure them properly. */ private void setupViews() { - DragController dragController = mDragController; + final DragController dragController = mDragController; DragLayer dragLayer = (DragLayer) findViewById(R.id.drag_layer); dragLayer.setDragController(dragController); @@ -732,39 +865,57 @@ public final class Launcher extends Activity mAllAppsGrid.setLauncher(this); mAllAppsGrid.setDragController(dragController); ((View) mAllAppsGrid).setWillNotDraw(false); // We don't want a hole punched in our window. - // Manage focusability manually since this thing is always visible - ((View) mAllAppsGrid).setFocusable(false); + // Manage focusability manually since this thing is always visible (in non-xlarge) + ((View) mAllAppsGrid).setFocusable(false); + + if (LauncherApplication.isScreenXLarge()) { + // They need to be INVISIBLE initially so that they will be measured in the layout. + // Otherwise the animations are messed up when we show them for the first time. + ((View) mAllAppsGrid).setVisibility(View.INVISIBLE); + mHomeCustomizationDrawer.setVisibility(View.INVISIBLE); + } mWorkspace = (Workspace) dragLayer.findViewById(R.id.workspace); + final Workspace workspace = mWorkspace; workspace.setHapticFeedbackEnabled(false); DeleteZone deleteZone = (DeleteZone) dragLayer.findViewById(R.id.delete_zone); mDeleteZone = deleteZone; - mHandleView = (HandleView) findViewById(R.id.all_apps_button); - mHandleView.setLauncher(this); - mHandleView.setOnClickListener(this); - mHandleView.setOnLongClickListener(this); + View handleView = findViewById(R.id.all_apps_button); + if (handleView != null && handleView instanceof HandleView) { + // we don't use handle view in xlarge mode + mHandleView = (HandleView)handleView; + mHandleView.setLauncher(this); + mHandleView.setOnClickListener(this); + mHandleView.setOnLongClickListener(this); + } - ImageView hotseatLeft = (ImageView) findViewById(R.id.hotseat_left); - hotseatLeft.setContentDescription(mHotseatLabels[0]); - hotseatLeft.setImageDrawable(mHotseatIcons[0]); - ImageView hotseatRight = (ImageView) findViewById(R.id.hotseat_right); - hotseatRight.setContentDescription(mHotseatLabels[1]); - hotseatRight.setImageDrawable(mHotseatIcons[1]); + if (mCustomizePagedView != null) { + mCustomizePagedView.setLauncher(this); + mCustomizePagedView.setDragController(dragController); + mCustomizePagedView.update(); + } else { + ImageView hotseatLeft = (ImageView) findViewById(R.id.hotseat_left); + hotseatLeft.setContentDescription(mHotseatLabels[0]); + hotseatLeft.setImageDrawable(mHotseatIcons[0]); + ImageView hotseatRight = (ImageView) findViewById(R.id.hotseat_right); + hotseatRight.setContentDescription(mHotseatLabels[1]); + hotseatRight.setImageDrawable(mHotseatIcons[1]); - mPreviousView = (ImageView) dragLayer.findViewById(R.id.previous_screen); - mNextView = (ImageView) dragLayer.findViewById(R.id.next_screen); + mPreviousView = (ImageView) dragLayer.findViewById(R.id.previous_screen); + mNextView = (ImageView) dragLayer.findViewById(R.id.next_screen); - Drawable previous = mPreviousView.getDrawable(); - Drawable next = mNextView.getDrawable(); - mWorkspace.setIndicators(previous, next); + Drawable previous = mPreviousView.getDrawable(); + Drawable next = mNextView.getDrawable(); + mWorkspace.setIndicators(previous, next); - mPreviousView.setHapticFeedbackEnabled(false); - mPreviousView.setOnLongClickListener(this); - mNextView.setHapticFeedbackEnabled(false); - mNextView.setOnLongClickListener(this); + mPreviousView.setHapticFeedbackEnabled(false); + mPreviousView.setOnLongClickListener(this); + mNextView.setHapticFeedbackEnabled(false); + mNextView.setOnLongClickListener(this); + } workspace.setOnLongClickListener(this); workspace.setDragController(dragController); @@ -772,35 +923,52 @@ public final class Launcher extends Activity deleteZone.setLauncher(this); deleteZone.setDragController(dragController); - deleteZone.setHandle(findViewById(R.id.all_apps_button_cluster)); + int deleteZoneHandleId; + if (LauncherApplication.isScreenXLarge()) { + deleteZoneHandleId = R.id.all_apps_button; + } else { + deleteZoneHandleId = R.id.all_apps_button_cluster; + } + deleteZone.setHandle(findViewById(deleteZoneHandleId)); + dragController.addDragListener(deleteZone); + + ApplicationInfoDropTarget infoButton = (ApplicationInfoDropTarget)findViewById(R.id.info_button); + if (infoButton != null) { + infoButton.setLauncher(this); + infoButton.setHandle(findViewById(R.id.configure_button)); + infoButton.setDragColor(getResources().getColor(R.color.app_info_filter)); + dragController.addDragListener(infoButton); + } dragController.setDragScoller(workspace); - dragController.setDragListener(deleteZone); dragController.setScrollView(dragLayer); dragController.setMoveTarget(workspace); // The order here is bottom to top. dragController.addDropTarget(workspace); dragController.addDropTarget(deleteZone); + if (infoButton != null) { + dragController.addDropTarget(infoButton); + } } @SuppressWarnings({"UnusedDeclaration"}) public void previousScreen(View v) { - if (!isAllAppsVisible()) { + if (mState != State.ALL_APPS) { mWorkspace.scrollLeft(); } } @SuppressWarnings({"UnusedDeclaration"}) public void nextScreen(View v) { - if (!isAllAppsVisible()) { + if (mState != State.ALL_APPS) { mWorkspace.scrollRight(); } } @SuppressWarnings({"UnusedDeclaration"}) public void launchHotSeat(View v) { - if (isAllAppsVisible()) return; + if (mState == State.ALL_APPS) return; int index = -1; if (v.getId() == R.id.hotseat_left) { @@ -819,7 +987,7 @@ public final class Launcher extends Activity ); } } - + /** * Creates a view representing a shortcut. * @@ -829,7 +997,7 @@ public final class Launcher extends Activity */ View createShortcut(ShortcutInfo info) { return createShortcut(R.layout.application, - (ViewGroup) mWorkspace.getChildAt(mWorkspace.getCurrentScreen()), info); + (ViewGroup) mWorkspace.getChildAt(mWorkspace.getCurrentPage()), info); } /** @@ -844,8 +1012,10 @@ public final class Launcher extends Activity View createShortcut(int layoutResId, ViewGroup parent, ShortcutInfo info) { TextView favorite = (TextView) mInflater.inflate(layoutResId, parent, false); + Bitmap b = info.getIcon(mIconCache); + favorite.setCompoundDrawablesWithIntrinsicBounds(null, - new FastBitmapDrawable(info.getIcon(mIconCache)), + new FastBitmapDrawable(b), null, null); favorite.setText(info.title); favorite.setTag(info); @@ -860,9 +1030,15 @@ public final class Launcher extends Activity * @param data The intent describing the application. * @param cellInfo The position on screen where to create the shortcut. */ - void completeAddApplication(Context context, Intent data, CellLayout.CellInfo cellInfo) { - cellInfo.screen = mWorkspace.getCurrentScreen(); - if (!findSingleSlot(cellInfo)) return; + void completeAddApplication(Context context, Intent data, int screen, + int intersectCellX, int intersectCellY) { + final int[] cellXY = mTmpAddItemCellCoordinates; + final CellLayout layout = (CellLayout) mWorkspace.getChildAt(screen); + + if (!layout.findCellForSpanThatIntersects(cellXY, 1, 1, intersectCellX, intersectCellY)) { + showOutOfSpaceMessage(); + return; + } final ShortcutInfo info = mModel.getShortcutInfo(context.getPackageManager(), data, context); @@ -871,7 +1047,8 @@ public final class Launcher extends Activity info.setActivity(data.getComponent(), Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); info.container = ItemInfo.NO_ID; - mWorkspace.addApplicationShortcut(info, cellInfo, isWorkspaceLocked()); + mWorkspace.addApplicationShortcut(info, screen, cellXY[0], cellXY[1], + isWorkspaceLocked(), mAddIntersectCellX, mAddIntersectCellY); } else { Log.e(TAG, "Couldn't find ActivityInfo for selected application: " + data); } @@ -883,53 +1060,82 @@ public final class Launcher extends Activity * @param data The intent describing the shortcut. * @param cellInfo The position on screen where to create the shortcut. */ - private void completeAddShortcut(Intent data, CellLayout.CellInfo cellInfo) { - cellInfo.screen = mWorkspace.getCurrentScreen(); - if (!findSingleSlot(cellInfo)) return; + private void completeAddShortcut(Intent data, int screen, + int intersectCellX, int intersectCellY) { + final int[] cellXY = mTmpAddItemCellCoordinates; + final CellLayout layout = (CellLayout) mWorkspace.getChildAt(screen); + + if (!layout.findCellForSpanThatIntersects(cellXY, 1, 1, intersectCellX, intersectCellY)) { + showOutOfSpaceMessage(); + return; + } - final ShortcutInfo info = mModel.addShortcut(this, data, cellInfo, false); + final ShortcutInfo info = mModel.addShortcut( + this, data, screen, cellXY[0], cellXY[1], false); if (!mRestoring) { final View view = createShortcut(info); - mWorkspace.addInCurrentScreen(view, cellInfo.cellX, cellInfo.cellY, 1, 1, - isWorkspaceLocked()); + mWorkspace.addInScreen(view, screen, cellXY[0], cellXY[1], 1, 1, isWorkspaceLocked()); } } - /** * Add a widget to the workspace. * - * @param data The intent describing the appWidgetId. + * @param appWidgetId The app widget id * @param cellInfo The position on screen where to create the widget. */ - private void completeAddAppWidget(Intent data, CellLayout.CellInfo cellInfo) { - Bundle extras = data.getExtras(); - int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); - - if (LOGD) Log.d(TAG, "dumping extras content=" + extras.toString()); - + private void completeAddAppWidget(int appWidgetId, int screen) { AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId); // Calculate the grid spans needed to fit this widget - CellLayout layout = (CellLayout) mWorkspace.getChildAt(cellInfo.screen); - int[] spans = layout.rectToCell(appWidgetInfo.minWidth, appWidgetInfo.minHeight); + CellLayout layout = (CellLayout) mWorkspace.getChildAt(screen); + int[] spanXY = layout.rectToCell(appWidgetInfo.minWidth, appWidgetInfo.minHeight, null); // Try finding open space on Launcher screen - final int[] xy = mCellCoordinates; - if (!findSlot(cellInfo, xy, spans[0], spans[1])) { + // We have saved the position to which the widget was dragged-- this really only matters + // if we are placing widgets on a "spring-loaded" screen + final int[] cellXY = mTmpAddItemCellCoordinates; + + // For now, we don't save the coordinate where we dropped the icon because we're not + // supporting spring-loaded mini-screens; however, leaving the ability to directly place + // a widget on the home screen in case we want to add it in the future + int[] touchXY = null; + if (mAddDropPosition != null && mAddDropPosition[0] > -1 && mAddDropPosition[1] > -1) { + touchXY = mAddDropPosition; + } + boolean findNearestVacantAreaFailed = false; + boolean foundCellSpan = false; + if (touchXY != null) { + // when dragging and dropping, just find the closest free spot + CellLayout screenLayout = (CellLayout) mWorkspace.getChildAt(screen); + int[] result = screenLayout.findNearestVacantArea( + touchXY[0], touchXY[1], spanXY[0], spanXY[1], cellXY); + findNearestVacantAreaFailed = (result == null); + foundCellSpan = !findNearestVacantAreaFailed; + } else { + // if we long pressed on an empty cell to bring up a menu, + // make sure we intersect the empty cell + // if mAddIntersectCellX/Y are -1 (e.g. we used menu -> add) then + // findCellForSpanThatIntersects will just ignore them + foundCellSpan = layout.findCellForSpanThatIntersects(cellXY, spanXY[0], spanXY[1], + mAddIntersectCellX, mAddIntersectCellY); + } + + if (!foundCellSpan) { if (appWidgetId != -1) mAppWidgetHost.deleteAppWidgetId(appWidgetId); + showOutOfSpaceMessage(); return; } // Build Launcher-specific widget info and save to database LauncherAppWidgetInfo launcherInfo = new LauncherAppWidgetInfo(appWidgetId); - launcherInfo.spanX = spans[0]; - launcherInfo.spanY = spans[1]; + launcherInfo.spanX = spanXY[0]; + launcherInfo.spanY = spanXY[1]; LauncherModel.addItemToDatabase(this, launcherInfo, LauncherSettings.Favorites.CONTAINER_DESKTOP, - mWorkspace.getCurrentScreen(), xy[0], xy[1], false); + screen, cellXY[0], cellXY[1], false); if (!mRestoring) { mDesktopItems.add(launcherInfo); @@ -940,20 +1146,139 @@ public final class Launcher extends Activity launcherInfo.hostView.setAppWidget(appWidgetId, appWidgetInfo); launcherInfo.hostView.setTag(launcherInfo); - mWorkspace.addInCurrentScreen(launcherInfo.hostView, xy[0], xy[1], + mWorkspace.addInScreen(launcherInfo.hostView, screen, cellXY[0], cellXY[1], launcherInfo.spanX, launcherInfo.spanY, isWorkspaceLocked()); + + addWidgetToAutoAdvanceIfNeeded(launcherInfo.hostView, appWidgetInfo); + } + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_SCREEN_OFF.equals(action)) { + mUserPresent = false; + updateRunning(); + } else if (Intent.ACTION_USER_PRESENT.equals(action)) { + mUserPresent = true; + updateRunning(); + } + } + }; + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + // Listen for broadcasts related to user-presence + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(Intent.ACTION_USER_PRESENT); + registerReceiver(mReceiver, filter); + + mAttached = true; + mVisible = true; + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mVisible = false; + + if (mAttached) { + unregisterReceiver(mReceiver); + mAttached = false; + } + updateRunning(); + } + + public void onWindowVisibilityChanged(int visibility) { + mVisible = visibility == View.VISIBLE; + updateRunning(); + } + + private void sendAdvanceMessage(long delay) { + mHandler.removeMessages(ADVANCE_MSG); + Message msg = mHandler.obtainMessage(ADVANCE_MSG); + mHandler.sendMessageDelayed(msg, delay); + mAutoAdvanceSentTime = System.currentTimeMillis(); + } + + private void updateRunning() { + boolean autoAdvanceRunning = mVisible && mUserPresent && !mWidgetsToAdvance.isEmpty(); + if (autoAdvanceRunning != mAutoAdvanceRunning) { + mAutoAdvanceRunning = autoAdvanceRunning; + if (autoAdvanceRunning) { + long delay = mAutoAdvanceTimeLeft == -1 ? mAdvanceInterval : mAutoAdvanceTimeLeft; + sendAdvanceMessage(delay); + } else { + if (!mWidgetsToAdvance.isEmpty()) { + mAutoAdvanceTimeLeft = Math.max(0, mAdvanceInterval - + (System.currentTimeMillis() - mAutoAdvanceSentTime)); + } + mHandler.removeMessages(ADVANCE_MSG); + } + } + } + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (msg.what == ADVANCE_MSG) { + int i = 0; + for (View key: mWidgetsToAdvance.keySet()) { + final View v = key.findViewById(mWidgetsToAdvance.get(key).autoAdvanceViewId); + final int delay = mAdvanceStagger * i; + if (v instanceof Advanceable) { + postDelayed(new Runnable() { + public void run() { + ((Advanceable) v).advance(); + } + }, delay); + } + i++; + } + sendAdvanceMessage(mAdvanceInterval); + } + } + }; + + void addWidgetToAutoAdvanceIfNeeded(View hostView, AppWidgetProviderInfo appWidgetInfo) { + if (appWidgetInfo.autoAdvanceViewId == -1) return; + View v = hostView.findViewById(appWidgetInfo.autoAdvanceViewId); + if (v instanceof Advanceable) { + mWidgetsToAdvance.put(hostView, appWidgetInfo); + ((Advanceable) v).willBeAdvancedByHost(); + updateRunning(); + } + } + + void removeWidgetToAutoAdvance(View hostView) { + if (mWidgetsToAdvance.containsKey(hostView)) { + mWidgetsToAdvance.remove(hostView); + updateRunning(); } } public void removeAppWidget(LauncherAppWidgetInfo launcherInfo) { mDesktopItems.remove(launcherInfo); + removeWidgetToAutoAdvance(launcherInfo.hostView); launcherInfo.hostView = null; } + void showOutOfSpaceMessage() { + Toast.makeText(this, getString(R.string.out_of_space), Toast.LENGTH_SHORT).show(); + } + public LauncherAppWidgetHost getAppWidgetHost() { return mAppWidgetHost; } + public LauncherModel getModel() { + return mModel; + } + void closeSystemDialogs() { getWindow().closeAllPanels(); @@ -986,11 +1311,17 @@ public final class Launcher extends Activity boolean alreadyOnHome = ((intent.getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT); - boolean allAppsVisible = isAllAppsVisible(); - if (!mWorkspace.isDefaultScreenShowing()) { - mWorkspace.moveToDefaultScreen(alreadyOnHome && !allAppsVisible); + + // in all these cases, only animate if we're already on home + if (LauncherApplication.isScreenXLarge()) { + mWorkspace.unshrink(alreadyOnHome); + } + if (!mWorkspace.isDefaultPageShowing()) { + // on the phone, we don't animate the change to the workspace if all apps is visible + mWorkspace.moveToDefaultScreen(alreadyOnHome && + (LauncherApplication.isScreenXLarge() || mState != State.ALL_APPS)); } - closeAllApps(alreadyOnHome && allAppsVisible); + showWorkspace(alreadyOnHome); final View v = getWindow().peekDecorView(); if (v != null && v.getWindowToken() != null) { @@ -1005,11 +1336,18 @@ public final class Launcher extends Activity protected void onRestoreInstanceState(Bundle savedInstanceState) { // Do not call super here mSavedInstanceState = savedInstanceState; + + if (mHomeCustomizationDrawer != null) { + String cur = savedInstanceState.getString("currentTab"); + if (cur != null) { + mHomeCustomizationDrawer.setCurrentTabByTag(cur); + } + } } @Override protected void onSaveInstanceState(Bundle outState) { - outState.putInt(RUNTIME_STATE_CURRENT_SCREEN, mWorkspace.getCurrentScreen()); + outState.putInt(RUNTIME_STATE_CURRENT_SCREEN, mWorkspace.getCurrentPage()); final ArrayList<Folder> folders = mWorkspace.getOpenFolders(); if (folders.size() > 0) { @@ -1024,30 +1362,25 @@ public final class Launcher extends Activity super.onSaveInstanceState(outState); } - // TODO should not do this if the drawer is currently closing. - if (isAllAppsVisible()) { - outState.putBoolean(RUNTIME_STATE_ALL_APPS_FOLDER, true); - } + outState.putInt(RUNTIME_STATE, mState.ordinal()); - if (mAddItemCellInfo != null && mAddItemCellInfo.valid && mWaitingForResult) { - final CellLayout.CellInfo addItemCellInfo = mAddItemCellInfo; - final CellLayout layout = (CellLayout) mWorkspace.getChildAt(addItemCellInfo.screen); - - outState.putInt(RUNTIME_STATE_PENDING_ADD_SCREEN, addItemCellInfo.screen); - outState.putInt(RUNTIME_STATE_PENDING_ADD_CELL_X, addItemCellInfo.cellX); - outState.putInt(RUNTIME_STATE_PENDING_ADD_CELL_Y, addItemCellInfo.cellY); - outState.putInt(RUNTIME_STATE_PENDING_ADD_SPAN_X, addItemCellInfo.spanX); - outState.putInt(RUNTIME_STATE_PENDING_ADD_SPAN_Y, addItemCellInfo.spanY); - outState.putInt(RUNTIME_STATE_PENDING_ADD_COUNT_X, layout.getCountX()); - outState.putInt(RUNTIME_STATE_PENDING_ADD_COUNT_Y, layout.getCountY()); - outState.putBooleanArray(RUNTIME_STATE_PENDING_ADD_OCCUPIED_CELLS, - layout.getOccupiedCells()); + if (mAddScreen > -1 && mWaitingForResult) { + outState.putInt(RUNTIME_STATE_PENDING_ADD_SCREEN, mAddScreen); + outState.putInt(RUNTIME_STATE_PENDING_ADD_CELL_X, mAddIntersectCellX); + outState.putInt(RUNTIME_STATE_PENDING_ADD_CELL_Y, mAddIntersectCellY); } if (mFolderInfo != null && mWaitingForResult) { outState.putBoolean(RUNTIME_STATE_PENDING_FOLDER_RENAME, true); outState.putLong(RUNTIME_STATE_PENDING_FOLDER_RENAME_ID, mFolderInfo.id); } + + if (mHomeCustomizationDrawer != null) { + String currentTabTag = mHomeCustomizationDrawer.getCurrentTabTag(); + if (currentTabTag != null) { + outState.putString("currentTab", currentTabTag); + } + } } @Override @@ -1067,9 +1400,14 @@ public final class Launcher extends Activity unbindDesktopItems(); getContentResolver().unregisterContentObserver(mWidgetObserver); - - dismissPreview(mPreviousView); - dismissPreview(mNextView); + + // Some launcher layouts don't have a previous and next view + if (mPreviousView != null) { + dismissPreview(mPreviousView); + } + if (mNextView != null) { + dismissPreview(mNextView); + } unregisterReceiver(mCloseSystemDialogsReceiver); } @@ -1084,7 +1422,7 @@ public final class Launcher extends Activity public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, boolean globalSearch) { - closeAllApps(true); + showWorkspace(true); if (initialQuery == null) { // Use any text typed in the launcher as the initial query @@ -1143,19 +1481,19 @@ public final class Launcher extends Activity // If all apps is animating, don't show the menu, because we don't know // which one to show. - if (mAllAppsGrid.isVisible() && !mAllAppsGrid.isOpaque()) { + if (mAllAppsGrid.isAnimating()) { return false; } // Only show the add and wallpaper options when we're not in all apps. - boolean visible = !mAllAppsGrid.isOpaque(); + boolean visible = !mAllAppsGrid.isVisible(); menu.setGroupVisible(MENU_GROUP_ADD, visible); menu.setGroupVisible(MENU_GROUP_WALLPAPER, visible); // Disable add if the workspace is full. if (visible) { - mMenuAddInfo = mWorkspace.findAllVacantCells(null); - menu.setGroupEnabled(MENU_GROUP_ADD, mMenuAddInfo != null && mMenuAddInfo.valid); + CellLayout layout = (CellLayout) mWorkspace.getChildAt(mWorkspace.getCurrentPage()); + menu.setGroupEnabled(MENU_GROUP_ADD, layout.existsEmptyCell()); } return true; @@ -1200,17 +1538,49 @@ public final class Launcher extends Activity } private void addItems() { - closeAllApps(true); - showAddDialog(mMenuAddInfo); + if (LauncherApplication.isScreenXLarge()) { + // Animate the widget chooser up from the bottom of the screen + if (mState != State.CUSTOMIZE) { + showCustomizationDrawer(true); + } + } else { + showWorkspace(true); + showAddDialog(-1, -1); + } + } + + private void resetAddInfo() { + mAddScreen = -1; + mAddIntersectCellX = -1; + mAddIntersectCellY = -1; + mAddDropPosition = null; + } + + void addAppWidgetFromDrop(PendingAddWidgetInfo info, int screen, int[] position) { + resetAddInfo(); + mAddScreen = screen; + + // only set mAddDropPosition if we dropped on home screen in "spring-loaded" manner + mAddDropPosition = position; + + int appWidgetId = getAppWidgetHost().allocateAppWidgetId(); + AppWidgetManager.getInstance(this).bindAppWidgetId(appWidgetId, info.componentName); + addAppWidgetImpl(appWidgetId, info); } private void manageApps() { startActivity(new Intent(android.provider.Settings.ACTION_MANAGE_ALL_APPLICATIONS_SETTINGS)); } - void addAppWidget(Intent data) { + void addAppWidgetFromPick(Intent data) { // TODO: catch bad widget exception when sent int appWidgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); + // TODO: Is this log message meaningful? + if (LOGD) Log.d(TAG, "dumping extras content=" + data.getExtras()); + addAppWidgetImpl(appWidgetId, null); + } + + void addAppWidgetImpl(int appWidgetId, PendingAddWidgetInfo info) { AppWidgetProviderInfo appWidget = mAppWidgetManager.getAppWidgetInfo(appWidgetId); if (appWidget.configure != null) { @@ -1218,14 +1588,53 @@ public final class Launcher extends Activity Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE); intent.setComponent(appWidget.configure); 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); + + final String mimeType = info.mimeType; + final ClipData clipData = (ClipData) info.configurationData; + final ClipDescription clipDesc = clipData.getDescription(); + for (int i = 0; i < clipDesc.getMimeTypeCount(); ++i) { + if (clipDesc.getMimeType(i).equals(mimeType)) { + final ClipData.Item item = clipData.getItem(i); + final CharSequence stringData = item.getText(); + final Uri uriData = item.getUri(); + final Intent intentData = item.getIntent(); + final String key = + InstallWidgetReceiver.EXTRA_APPWIDGET_CONFIGURATION_DATA; + if (uriData != null) { + intent.putExtra(key, uriData); + } else if (intentData != null) { + intent.putExtra(key, intentData); + } else if (stringData != null) { + intent.putExtra(key, stringData); + } + break; + } + } + } + } startActivityForResultSafely(intent, REQUEST_CREATE_APPWIDGET); } else { // Otherwise just add it - onActivityResult(REQUEST_CREATE_APPWIDGET, Activity.RESULT_OK, data); + completeAddAppWidget(appWidgetId, mAddScreen); } } + void processShortcutFromDrop(ComponentName componentName, int screen, int[] position) { + resetAddInfo(); + mAddScreen = screen; + mAddDropPosition = position; + + Intent createShortcutIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT); + createShortcutIntent.setComponent(componentName); + processShortcut(createShortcutIntent); + } + void processShortcut(Intent intent) { // Handle case where user selected "Applications" String applicationName = getResources().getString(R.string.group_applications); @@ -1244,59 +1653,81 @@ public final class Launcher extends Activity } } - void addLiveFolder(Intent intent) { + void processWallpaper(Intent intent) { + startActivityForResult(intent, REQUEST_PICK_WALLPAPER); + } + + void addLiveFolderFromDrop(ComponentName componentName, int screen, int[] position) { + resetAddInfo(); + mAddScreen = screen; + mAddDropPosition = position; + + Intent createFolderIntent = new Intent(LiveFolders.ACTION_CREATE_LIVE_FOLDER); + createFolderIntent.setComponent(componentName); + + addLiveFolder(createFolderIntent); + } + + void addLiveFolder(Intent intent) { // YYY add screen intersect etc. parameters here // Handle case where user selected "Folder" String folderName = getResources().getString(R.string.group_folder); String shortcutName = intent.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); if (folderName != null && folderName.equals(shortcutName)) { - addFolder(); + addFolder(mAddScreen, mAddIntersectCellX, mAddIntersectCellY); } else { startActivityForResultSafely(intent, REQUEST_CREATE_LIVE_FOLDER); } } - void addFolder() { + void addFolder(int screen, int intersectCellX, int intersectCellY) { UserFolderInfo folderInfo = new UserFolderInfo(); folderInfo.title = getText(R.string.folder_name); - CellLayout.CellInfo cellInfo = mAddItemCellInfo; - cellInfo.screen = mWorkspace.getCurrentScreen(); - if (!findSingleSlot(cellInfo)) return; + final CellLayout layout = (CellLayout) mWorkspace.getChildAt(screen); + final int[] cellXY = mTmpAddItemCellCoordinates; + if (!layout.findCellForSpanThatIntersects(cellXY, 1, 1, intersectCellX, intersectCellY)) { + showOutOfSpaceMessage(); + return; + } // Update the model LauncherModel.addItemToDatabase(this, folderInfo, LauncherSettings.Favorites.CONTAINER_DESKTOP, - mWorkspace.getCurrentScreen(), cellInfo.cellX, cellInfo.cellY, false); + screen, cellXY[0], cellXY[1], false); sFolders.put(folderInfo.id, folderInfo); // Create the view FolderIcon newFolder = FolderIcon.fromXml(R.layout.folder_icon, this, - (ViewGroup) mWorkspace.getChildAt(mWorkspace.getCurrentScreen()), folderInfo); - mWorkspace.addInCurrentScreen(newFolder, - cellInfo.cellX, cellInfo.cellY, 1, 1, isWorkspaceLocked()); + (ViewGroup) mWorkspace.getChildAt(mWorkspace.getCurrentPage()), + folderInfo, mIconCache); + mWorkspace.addInScreen(newFolder, screen, cellXY[0], cellXY[1], 1, 1, isWorkspaceLocked()); } void removeFolder(FolderInfo folder) { sFolders.remove(folder.id); } - private void completeAddLiveFolder(Intent data, CellLayout.CellInfo cellInfo) { - cellInfo.screen = mWorkspace.getCurrentScreen(); - if (!findSingleSlot(cellInfo)) return; + private void completeAddLiveFolder( + Intent data, int screen, int intersectCellX, int intersectCellY) { + final CellLayout layout = (CellLayout) mWorkspace.getChildAt(screen); + final int[] cellXY = mTmpAddItemCellCoordinates; + if (!layout.findCellForSpanThatIntersects(cellXY, 1, 1, intersectCellX, intersectCellY)) { + showOutOfSpaceMessage(); + return; + } - final LiveFolderInfo info = addLiveFolder(this, data, cellInfo, false); + final LiveFolderInfo info = addLiveFolder(this, data, screen, cellXY[0], cellXY[1], false); if (!mRestoring) { final View view = LiveFolderIcon.fromXml(R.layout.live_folder_icon, this, - (ViewGroup) mWorkspace.getChildAt(mWorkspace.getCurrentScreen()), info); - mWorkspace.addInCurrentScreen(view, cellInfo.cellX, cellInfo.cellY, 1, 1, - isWorkspaceLocked()); + (ViewGroup) mWorkspace.getChildAt(mWorkspace.getCurrentPage()), info); + mWorkspace.addInScreen(view, screen, cellXY[0], cellXY[1], 1, 1, isWorkspaceLocked()); } } static LiveFolderInfo addLiveFolder(Context context, Intent data, - CellLayout.CellInfo cellInfo, boolean notify) { + int screen, int cellX, int cellY, boolean notify) { Intent baseIntent = data.getParcelableExtra(LiveFolders.EXTRA_LIVE_FOLDER_BASE_INTENT); String name = data.getStringExtra(LiveFolders.EXTRA_LIVE_FOLDER_NAME); @@ -1332,35 +1763,12 @@ public final class Launcher extends Activity LiveFolders.DISPLAY_MODE_GRID); LauncherModel.addItemToDatabase(context, info, LauncherSettings.Favorites.CONTAINER_DESKTOP, - cellInfo.screen, cellInfo.cellX, cellInfo.cellY, notify); + screen, cellX, cellY, notify); sFolders.put(info.id, info); return info; } - private boolean findSingleSlot(CellLayout.CellInfo cellInfo) { - final int[] xy = new int[2]; - if (findSlot(cellInfo, xy, 1, 1)) { - cellInfo.cellX = xy[0]; - cellInfo.cellY = xy[1]; - return true; - } - return false; - } - - private boolean findSlot(CellLayout.CellInfo cellInfo, int[] xy, int spanX, int spanY) { - if (!cellInfo.findCellForSpan(xy, spanX, spanY)) { - boolean[] occupied = mSavedState != null ? - mSavedState.getBooleanArray(RUNTIME_STATE_PENDING_ADD_OCCUPIED_CELLS) : null; - cellInfo = mWorkspace.findAllVacantCells(occupied); - if (!cellInfo.findCellForSpan(xy, spanX, spanY)) { - Toast.makeText(this, getString(R.string.out_of_space), Toast.LENGTH_SHORT).show(); - return false; - } - } - return true; - } - private void showNotifications() { final StatusBarManager statusBar = (StatusBarManager) getSystemService(STATUS_BAR_SERVICE); if (statusBar != null) { @@ -1369,7 +1777,7 @@ public final class Launcher extends Activity } private void startWallpaper() { - closeAllApps(true); + showWorkspace(true); final Intent pickWallpaper = new Intent(Intent.ACTION_SET_WALLPAPER); Intent chooser = Intent.createChooser(pickWallpaper, getText(R.string.chooser_wallpaper)); @@ -1422,13 +1830,16 @@ public final class Launcher extends Activity @Override public void onBackPressed() { - if (isAllAppsVisible()) { - closeAllApps(true); + if (mState == State.ALL_APPS || mState == State.CUSTOMIZE) { + showWorkspace(true); } else { closeFolder(); } - dismissPreview(mPreviousView); - dismissPreview(mNextView); + // Some launcher layouts don't have a previous and next view + if (mPreviousView != null) { + dismissPreview(mPreviousView); + dismissPreview(mNextView); + } } private void closeFolder() { @@ -1442,7 +1853,8 @@ public final class Launcher extends Activity folder.getInfo().opened = false; ViewGroup parent = (ViewGroup) folder.getParent(); if (parent != null) { - parent.removeView(folder); + CellLayout cl = (CellLayout) parent; + cl.removeViewWithoutMarkingCells(folder); if (folder instanceof DropTarget) { // Live folders aren't DropTargets. mDragController.removeDropTarget((DropTarget)folder); @@ -1486,14 +1898,95 @@ public final class Launcher extends Activity } else if (tag instanceof FolderInfo) { handleFolderClick((FolderInfo) tag); } else if (v == mHandleView) { - if (isAllAppsVisible()) { - closeAllApps(true); + if (mState == State.ALL_APPS) { + showWorkspace(true); } else { showAllApps(true); } } } + public boolean onTouch(View v, MotionEvent event) { + // this is an intercepted event being forwarded from mWorkspace; + // clicking anywhere on the workspace causes the customization drawer to slide down + showWorkspace(true); + return false; + } + + /** + * Event handler for the search button + * + * @param v The view that was clicked. + */ + public void onClickSearchButton(View v) { + startSearch(null, false, null, true); + // Use a custom animation for launching search + overridePendingTransition(R.anim.fade_in_fast, R.anim.fade_out_fast); + } + + /** + * Event handler for the voice button + * + * @param v The view that was clicked. + */ + public void onClickVoiceButton(View v) { + startVoiceSearch(); + } + + private void startVoiceSearch() { + Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + + /** + * Event handler for the "gear" button that appears on the home screen, which + * enters home screen customization mode. + * + * @param v The view that was clicked. + */ + public void onClickConfigureButton(View v) { + addItems(); + } + + /** + * Event handler for the "grid" button that appears on the home screen, which + * enters all apps mode. + * + * @param v The view that was clicked. + */ + public void onClickAllAppsButton(View v) { + showAllApps(true); + } + + public void onClickAppMarketButton(View v) { + if (mAppMarketIntent != null) { + startActivitySafely(mAppMarketIntent, "app market"); + } + } + + void startApplicationDetailsActivity(ComponentName componentName) { + String packageName = componentName.getPackageName(); + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", packageName, null)); + startActivity(intent); + } + + void startApplicationUninstallActivity(ApplicationInfo appInfo) { + if ((appInfo.flags & ApplicationInfo.DOWNLOADED_FLAG) == 0) { + // System applications cannot be installed. For now, show a toast explaining that. + // We may give them the option of disabling apps this way. + int messageId = R.string.uninstall_system_app_text; + Toast.makeText(this, messageId, Toast.LENGTH_SHORT).show(); + } else { + String packageName = appInfo.componentName.getPackageName(); + String className = appInfo.componentName.getClassName(); + Intent intent = new Intent( + Intent.ACTION_DELETE, Uri.fromParts("package", packageName, className)); + startActivity(intent); + } + } + void startActivitySafely(Intent intent, Object tag) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { @@ -1509,7 +2002,7 @@ public final class Launcher extends Activity + "tag="+ tag + " intent=" + intent, e); } } - + void startActivityForResultSafely(Intent intent, int requestCode) { try { startActivityForResult(intent, requestCode); @@ -1534,10 +2027,10 @@ public final class Launcher extends Activity Folder openFolder = mWorkspace.getFolderForTag(folderInfo); int folderScreen; if (openFolder != null) { - folderScreen = mWorkspace.getScreenForView(openFolder); + folderScreen = mWorkspace.getPageForView(openFolder); // .. and close it closeFolder(openFolder); - if (folderScreen != mWorkspace.getCurrentScreen()) { + if (folderScreen != mWorkspace.getCurrentPage()) { // Close any folder open on the current screen closeFolder(); // Pull the folder onto this screen @@ -1548,13 +2041,13 @@ public final class Launcher extends Activity } /** - * Opens the user fodler described by the specified tag. The opening of the folder + * Opens the user folder described by the specified tag. The opening of the folder * is animated relative to the specified View. If the View is null, no animation * is played. * * @param folderInfo The FolderInfo describing the folder to open. */ - private void openFolder(FolderInfo folderInfo) { + public void openFolder(FolderInfo folderInfo) { Folder openFolder; if (folderInfo instanceof UserFolderInfo) { @@ -1571,28 +2064,29 @@ public final class Launcher extends Activity openFolder.bind(folderInfo); folderInfo.opened = true; - mWorkspace.addInScreen(openFolder, folderInfo.screen, 0, 0, 4, 4); + mWorkspace.addInFullScreen(openFolder, folderInfo.screen); + openFolder.onOpen(); } public boolean onLongClick(View v) { switch (v.getId()) { case R.id.previous_screen: - if (!isAllAppsVisible()) { + if (mState != State.ALL_APPS) { mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); showPreviews(v); } return true; case R.id.next_screen: - if (!isAllAppsVisible()) { + if (mState != State.ALL_APPS) { mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); showPreviews(v); } return true; case R.id.all_apps_button: - if (!isAllAppsVisible()) { + if (mState != State.ALL_APPS) { mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); showPreviews(v); @@ -1608,28 +2102,33 @@ public final class Launcher extends Activity v = (View) v.getParent(); } - CellLayout.CellInfo cellInfo = (CellLayout.CellInfo) v.getTag(); + resetAddInfo(); + CellLayout.CellInfo longClickCellInfo = (CellLayout.CellInfo) v.getTag(); // This happens when long clicking an item with the dpad/trackball - if (cellInfo == null) { + if (longClickCellInfo == null || !longClickCellInfo.valid) { return true; } + final View itemUnderLongClick = longClickCellInfo.cell; + if (mWorkspace.allowLongPress()) { - if (cellInfo.cell == null) { - if (cellInfo.valid) { - // User long pressed on empty space - mWorkspace.setAllowLongPress(false); - mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, - HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); - showAddDialog(cellInfo); + if (itemUnderLongClick == null) { + // User long pressed on empty space + mWorkspace.setAllowLongPress(false); + mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + if (!LauncherApplication.isScreenXLarge()) { + showAddDialog(longClickCellInfo.cellX, longClickCellInfo.cellY); } } else { - if (!(cellInfo.cell instanceof Folder)) { + if (!(itemUnderLongClick instanceof Folder)) { // User long pressed on an item mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); - mWorkspace.startDrag(cellInfo); + mAddIntersectCellX = longClickCellInfo.cellX; + mAddIntersectCellY = longClickCellInfo.cellY; + mWorkspace.startDrag(longClickCellInfo); } } } @@ -1669,9 +2168,9 @@ public final class Launcher extends Activity final Workspace workspace = mWorkspace; CellLayout cell = ((CellLayout) workspace.getChildAt(start)); - + float max = workspace.getChildCount(); - + final Rect r = new Rect(); resources.getDrawable(R.drawable.preview_background).getPadding(r); int extraW = (int) ((r.left + r.right) * max); @@ -1709,7 +2208,7 @@ public final class Launcher extends Activity final Canvas c = new Canvas(bitmap); c.scale(scale, scale); c.translate(-cell.getLeftPadding(), -cell.getTopPadding()); - cell.dispatchDraw(c); + cell.drawChildren(c); image.setBackgroundDrawable(resources.getDrawable(R.drawable.preview_background)); image.setImageBitmap(bitmap); @@ -1717,12 +2216,12 @@ public final class Launcher extends Activity image.setOnClickListener(handler); image.setOnFocusChangeListener(handler); image.setFocusable(true); - if (i == mWorkspace.getCurrentScreen()) image.requestFocus(); + if (i == mWorkspace.getCurrentPage()) image.requestFocus(); preview.addView(image, LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); - bitmaps.add(bitmap); + bitmaps.add(bitmap); } final PopupWindow p = new PopupWindow(this); @@ -1743,7 +2242,7 @@ public final class Launcher extends Activity anchor.setTag(p); anchor.setTag(R.id.workspace, preview); - anchor.setTag(R.id.icon, bitmaps); + anchor.setTag(R.id.icon, bitmaps); } class PreviewTouchHandler implements View.OnClickListener, Runnable, View.OnFocusChangeListener { @@ -1754,17 +2253,17 @@ public final class Launcher extends Activity } public void onClick(View v) { - mWorkspace.snapToScreen((Integer) v.getTag()); + mWorkspace.snapToPage((Integer) v.getTag()); v.post(this); } public void run() { - dismissPreview(mAnchor); + dismissPreview(mAnchor); } public void onFocusChange(View v, boolean hasFocus) { if (hasFocus) { - mWorkspace.snapToScreen((Integer) v.getTag()); + mWorkspace.snapToPage((Integer) v.getTag()); } } } @@ -1807,13 +2306,17 @@ public final class Launcher extends Activity showDialog(DIALOG_RENAME_FOLDER); } - private void showAddDialog(CellLayout.CellInfo cellInfo) { - mAddItemCellInfo = cellInfo; + private void showAddDialog(int intersectX, int intersectY) { + resetAddInfo(); + mAddIntersectCellX = intersectX; + mAddIntersectCellY = intersectY; + mAddScreen = mWorkspace.getCurrentPage(); mWaitingForResult = true; showDialog(DIALOG_CREATE_SHORTCUT); } private void pickShortcut() { + // Insert extra item to handle picking application Bundle bundle = new Bundle(); ArrayList<String> shortcutNames = new ArrayList<String>(); @@ -1915,24 +2418,378 @@ public final class Launcher extends Activity // Now a part of LauncherModel.Callbacks. Used to reorder loading steps. public boolean isAllAppsVisible() { - return (mAllAppsGrid != null) ? mAllAppsGrid.isVisible() : false; + return mState == State.ALL_APPS; } // AllAppsView.Watcher public void zoomed(float zoom) { - if (zoom == 1.0f) { + // In XLarge view, we zoom down the workspace below all apps so it's still visible + if (zoom == 1.0f && !LauncherApplication.isScreenXLarge()) { mWorkspace.setVisibility(View.GONE); } } + + private void showToolbarButton(View button) { + button.setAlpha(1.0f); + button.setVisibility(View.VISIBLE); + button.setFocusable(true); + button.setClickable(true); + } + + private void hideToolbarButton(View button) { + button.setAlpha(0.0f); + // We can't set it to GONE, otherwise the RelativeLayout gets screwed up + button.setVisibility(View.INVISIBLE); + button.setFocusable(false); + button.setClickable(false); + } + + /** + * Helper function for showing or hiding a toolbar button, possibly animated. + * + * @param show If true, create an animation to the show the item. Otherwise, hide it. + * @param view The toolbar button to be animated + * @param seq A AnimatorSet that will be used to animate the transition. If null, the + * transition will not be animated. + */ + private void hideOrShowToolbarButton(boolean show, final View view, AnimatorSet seq) { + final boolean showing = show; + final boolean hiding = !show; + + final int duration = show ? + getResources().getInteger(R.integer.config_toolbarButtonFadeInTime) : + getResources().getInteger(R.integer.config_toolbarButtonFadeOutTime); + + if (seq != null) { + Animator anim = ObjectAnimator.ofFloat(view, "alpha", show ? 1.0f : 0.0f); + anim.setDuration(duration); + anim.addListener(new LauncherAnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + if (showing) showToolbarButton(view); + } + @Override + public void onAnimationEndOrCancel(Animator animation) { + if (hiding) hideToolbarButton(view); + } + }); + seq.play(anim); + } else { + if (showing) { + showToolbarButton(view); + } else { + hideToolbarButton(view); + } + } + } + + /** + * Show/hide the appropriate toolbar buttons for newState. + * If showSeq or hideSeq is null, the transition will be done immediately (not animated). + * + * @param newState The state that is being switched to + * @param showSeq AnimatorSet in which to put "show" animations, or null. + * @param hideSeq AnimatorSet in which to put "hide" animations, or null. + */ + private void hideAndShowToolbarButtons(State newState, AnimatorSet showSeq, AnimatorSet hideSeq) { + final View searchButton = findViewById(R.id.search_button_cluster); + final View allAppsButton = findViewById(R.id.all_apps_button); + final View configureButton = findViewById(R.id.configure_button); + + switch (newState) { + case WORKSPACE: + hideOrShowToolbarButton(true, searchButton, showSeq); + hideOrShowToolbarButton(true, allAppsButton, showSeq); + hideOrShowToolbarButton(true, configureButton, showSeq); + mDeleteZone.setHandle(allAppsButton); + break; + case ALL_APPS: + hideOrShowToolbarButton(false, configureButton, hideSeq); + hideOrShowToolbarButton(false, searchButton, hideSeq); + hideOrShowToolbarButton(false, allAppsButton, hideSeq); + break; + case CUSTOMIZE: + hideOrShowToolbarButton(false, allAppsButton, hideSeq); + hideOrShowToolbarButton(false, searchButton, hideSeq); + hideOrShowToolbarButton(false, configureButton, hideSeq); + mDeleteZone.setHandle(allAppsButton); + break; + } + } + + /** + * Helper method for the cameraZoomIn/cameraZoomOut animations + * @param view The view being animated + * @param state The state that we are moving in or out of -- either ALL_APPS or CUSTOMIZE + * @param scaleFactor The scale factor used for the zoom + */ + private void setPivotsForZoom(View view, State state, float scaleFactor) { + final int height = view.getHeight(); + + view.setPivotX(view.getWidth() / 2.0f); + // Set pivotY so that at the starting zoom factor, the view is partially + // visible. Modifying initialHeightFactor changes how much of the view is + // initially showing, and hence the perceived angle from which the view enters. + final float initialHeightFactor = 0.2f; + if (state == State.ALL_APPS) { + view.setPivotY((1 + initialHeightFactor) * height); + } else { + view.setPivotY(-initialHeightFactor * height); + } + } + + /** + * Zoom the camera out from the workspace to reveal 'toView'. + * Assumes that the view to show is anchored at either the very top or very bottom + * of the screen. + * @param toState The state to zoom out to. Must be ALL_APPS or CUSTOMIZE. + */ + private void cameraZoomOut(State toState, boolean animated) { + final Resources res = getResources(); + final int duration = res.getInteger(R.integer.config_allAppsZoomInTime); + final float scale = (float) res.getInteger(R.integer.config_allAppsZoomScaleFactor); + final boolean toAllApps = (toState == State.ALL_APPS); + final View toView = toAllApps ? (View) mAllAppsGrid : mHomeCustomizationDrawer; + + setPivotsForZoom(toView, toState, scale); + + if (toAllApps) { + mWorkspace.shrinkToBottomHidden(animated); + } else { + mWorkspace.shrinkToTop(animated); + } + + if (animated) { + ValueAnimator scaleAnim = ObjectAnimator.ofPropertyValuesHolder(toView, + PropertyValuesHolder.ofFloat("scaleX", scale, 1.0f), + PropertyValuesHolder.ofFloat("scaleY", scale, 1.0f)); + scaleAnim.setDuration(duration); + + scaleAnim.setInterpolator(new Workspace.ZoomOutInterpolator()); + scaleAnim.addListener(new LauncherAnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + // Prepare the position + toView.setTranslationX(0.0f); + toView.setTranslationY(0.0f); + toView.setVisibility(View.VISIBLE); + toView.setAlpha(1.0f); + } + @Override + public void onAnimationEndOrCancel(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); + } + }); + + AnimatorSet toolbarHideAnim = new AnimatorSet(); + AnimatorSet toolbarShowAnim = new AnimatorSet(); + hideAndShowToolbarButtons(toState, toolbarShowAnim, toolbarHideAnim); + + // toView should appear right at the end of the workspace shrink animation + final int startDelay = res.getInteger(R.integer.config_workspaceShrinkTime) - duration; + + if (mStateAnimation != null) mStateAnimation.cancel(); + mStateAnimation = new AnimatorSet(); + mStateAnimation.playTogether(scaleAnim, toolbarHideAnim); + mStateAnimation.play(scaleAnim).after(startDelay); + + // Show the new toolbar buttons just as the main animation is ending + final int fadeInTime = res.getInteger(R.integer.config_toolbarButtonFadeInTime); + mStateAnimation.play(toolbarShowAnim).after(duration + startDelay - fadeInTime); + mStateAnimation.start(); + } else { + toView.setTranslationX(0.0f); + toView.setTranslationY(0.0f); + toView.setScaleX(1.0f); + toView.setScaleY(1.0f); + toView.setVisibility(View.VISIBLE); + hideAndShowToolbarButtons(toState, null, null); + } + } + + /** + * Zoom the camera back into the workspace, hiding 'fromView'. + * This is the opposite of cameraZoomOut. + * @param fromState The current state (must be ALL_APPS or CUSTOMIZE). + * @param animated If true, the transition will be animated. + */ + private void cameraZoomIn(State fromState, boolean animated) { + Resources res = getResources(); + int duration = res.getInteger(R.integer.config_allAppsZoomOutTime); + float scaleFactor = (float) res.getInteger(R.integer.config_allAppsZoomScaleFactor); + final View fromView = + (fromState == State.ALL_APPS) ? (View) mAllAppsGrid : mHomeCustomizationDrawer; + + mCustomizePagedView.endChoiceMode(); + mAllAppsPagedView.endChoiceMode(); + + setPivotsForZoom(fromView, fromState, scaleFactor); + + mWorkspace.unshrink(animated); + + if (animated) { + if (mStateAnimation != null) mStateAnimation.cancel(); + mStateAnimation = new AnimatorSet(); + ValueAnimator scaleAnim = ObjectAnimator.ofPropertyValuesHolder(fromView, + PropertyValuesHolder.ofFloat("scaleX", scaleFactor), + PropertyValuesHolder.ofFloat("scaleY", scaleFactor)); + scaleAnim.setDuration(duration); + scaleAnim.setInterpolator(new Workspace.ZoomInInterpolator()); + + ValueAnimator alphaAnim = ObjectAnimator.ofPropertyValuesHolder(fromView, + PropertyValuesHolder.ofFloat("alpha", 1.0f, 0.0f)); + alphaAnim.setDuration(res.getInteger(R.integer.config_allAppsFadeOutTime)); + alphaAnim.addListener(new LauncherAnimatorListenerAdapter() { + @Override + public void onAnimationEndOrCancel(Animator animation) { + fromView.setVisibility(View.GONE); + } + }); + + AnimatorSet toolbarHideAnim = new AnimatorSet(); + AnimatorSet toolbarShowAnim = new AnimatorSet(); + hideAndShowToolbarButtons(State.WORKSPACE, toolbarShowAnim, toolbarHideAnim); + + mStateAnimation.playTogether(scaleAnim, toolbarHideAnim, alphaAnim); + + // Show the new toolbar buttons at the very end of the whole animation + final int fadeInTime = res.getInteger(R.integer.config_toolbarButtonFadeInTime); + final int unshrinkTime = res.getInteger(R.integer.config_workspaceUnshrinkTime); + mStateAnimation.play(toolbarShowAnim).after(unshrinkTime - fadeInTime); + mStateAnimation.start(); + } else { + fromView.setVisibility(View.GONE); + hideAndShowToolbarButtons(State.WORKSPACE, null, null); + } + } + + /** + * Pan the camera in the vertical plane between 'fromView' and 'toView'. + * This is the transition used on xlarge screens to go between all apps and + * the home customization drawer. + * @param fromState The view to pan away from. Must be ALL_APPS or CUSTOMIZE. + * @param toState The view to pan into the frame. Must be ALL_APPS or CUSTOMIZE. + * @param animated If true, the transition will be animated. + */ + private void cameraPan(State fromState, State toState, boolean animated) { + final Resources res = getResources(); + final int duration = res.getInteger(R.integer.config_allAppsCameraPanTime); + final int workspaceHeight = mWorkspace.getHeight(); + + final boolean fromAllApps = (fromState == State.ALL_APPS); + final View fromView = fromAllApps ? (View) mAllAppsGrid : mHomeCustomizationDrawer; + final View toView = fromAllApps ? mHomeCustomizationDrawer : (View) mAllAppsGrid; + + final float fromViewStartY = fromAllApps ? 0.0f : fromView.getY(); + final float fromViewEndY = fromAllApps ? -fromView.getHeight() * 2 : workspaceHeight * 2; + final float toViewStartY = fromAllApps ? workspaceHeight * 2 : -toView.getHeight() * 2; + final float toViewEndY = fromAllApps ? workspaceHeight - toView.getHeight() : 0.0f; + + mCustomizePagedView.endChoiceMode(); + mAllAppsPagedView.endChoiceMode(); + + if (toState == State.ALL_APPS) { + mWorkspace.shrinkToBottomHidden(animated); + } else { + mWorkspace.shrinkToTop(animated); + } + + if (animated) { + if (mStateAnimation != null) mStateAnimation.cancel(); + mStateAnimation = new AnimatorSet(); + mStateAnimation.addListener(new LauncherAnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + toView.setVisibility(View.VISIBLE); + toView.setY(toViewStartY); + toView.setAlpha(1.0f); + } + @Override + public void onAnimationEndOrCancel(Animator animation) { + fromView.setVisibility(View.GONE); + } + }); + + AnimatorSet toolbarHideAnim = new AnimatorSet(); + AnimatorSet toolbarShowAnim = new AnimatorSet(); + hideAndShowToolbarButtons(toState, toolbarShowAnim, toolbarHideAnim); + + ObjectAnimator fromAnim = ObjectAnimator.ofFloat(fromView, "y", + fromViewStartY, fromViewEndY); + fromAnim.setDuration(duration); + ObjectAnimator toAnim = ObjectAnimator.ofPropertyValuesHolder(toView, + PropertyValuesHolder.ofFloat("y", toViewStartY, toViewEndY), + PropertyValuesHolder.ofFloat("scaleX", toView.getScaleX(), 1.0f), + PropertyValuesHolder.ofFloat("scaleY", toView.getScaleY(), 1.0f) + ); + fromAnim.setDuration(duration); + mStateAnimation.playTogether(toolbarHideAnim, fromAnim, toAnim); + + // Show the new toolbar buttons just as the main animation is ending + final int fadeInTime = res.getInteger(R.integer.config_toolbarButtonFadeInTime); + mStateAnimation.play(toolbarShowAnim).after(duration - fadeInTime); + mStateAnimation.start(); + } else { + fromView.setY(fromViewEndY); + fromView.setVisibility(View.GONE); + toView.setY(toViewEndY); + toView.setScaleX(1.0f); + toView.setScaleY(1.0f); + toView.setVisibility(View.VISIBLE); + hideAndShowToolbarButtons(toState, null, null); + } + } void showAllApps(boolean animated) { - mAllAppsGrid.zoom(1.0f, animated); + if (mState == State.ALL_APPS) { + return; + } + + if (LauncherApplication.isScreenXLarge()) { + if (mState == State.CUSTOMIZE) { + cameraPan(State.CUSTOMIZE, State.ALL_APPS, animated); + } else { + cameraZoomOut(State.ALL_APPS, animated); + } + } else { + mAllAppsGrid.zoom(1.0f, animated); + } ((View) mAllAppsGrid).setFocusable(true); ((View) mAllAppsGrid).requestFocus(); - + // TODO: fade these two too mDeleteZone.setVisibility(View.GONE); + + // Change the state *after* we've called all the transition code + mState = State.ALL_APPS; + } + + + void showWorkspace(boolean animated) { + showWorkspace(animated, null); + } + + void showWorkspace(boolean animated, CellLayout layout) { + if (layout != null) { + // always animated, but that's ok since we never specify a layout and + // want no animation + mWorkspace.unshrink(layout); + } else { + mWorkspace.unshrink(animated); + } + if (mState == State.ALL_APPS) { + closeAllApps(animated); + } else if (mState == State.CUSTOMIZE) { + hideCustomizationDrawer(animated); + } + + // Change the state *after* we've called all the transition code + mState = State.WORKSPACE; } /** @@ -1975,11 +2832,15 @@ public final class Launcher extends Activity * - From another workspace */ void closeAllApps(boolean animated) { - if (mAllAppsGrid.isVisible()) { + if (mState == State.ALL_APPS) { mWorkspace.setVisibility(View.VISIBLE); - mAllAppsGrid.zoom(0.0f, animated); + if (LauncherApplication.isScreenXLarge()) { + cameraZoomIn(State.ALL_APPS, animated); + } else { + mAllAppsGrid.zoom(0.0f, animated); + } ((View)mAllAppsGrid).setFocusable(false); - mWorkspace.getChildAt(mWorkspace.getCurrentScreen()).requestFocus(); + mWorkspace.getChildAt(mWorkspace.getCurrentPage()).requestFocus(); } } @@ -1991,6 +2852,105 @@ public final class Launcher extends Activity // TODO } + // Show the customization drawer (only exists in x-large configuration) + private void showCustomizationDrawer(boolean animated) { + if (mState == State.ALL_APPS) { + cameraPan(State.ALL_APPS, State.CUSTOMIZE, animated); + } else { + cameraZoomOut(State.CUSTOMIZE, animated); + } + // Change the state *after* we've called all the transition code + mState = State.CUSTOMIZE; + } + + // Hide the customization drawer (only exists in x-large configuration) + void hideCustomizationDrawer(boolean animated) { + if (mState == State.CUSTOMIZE) { + cameraZoomIn(State.CUSTOMIZE, animated); + } + } + + void addExternalItemToScreen(ItemInfo itemInfo, CellLayout layout) { + if (!mWorkspace.addExternalItemToScreen(itemInfo, layout)) { + showOutOfSpaceMessage(); + } + } + + void onWorkspaceClick(CellLayout layout) { + showWorkspace(true, layout); + } + + private void updateButtonWithIconFromExternalActivity( + int buttonId, ComponentName activityName, int fallbackDrawableId) { + ImageView button = (ImageView) findViewById(buttonId); + Drawable toolbarIcon = null; + try { + PackageManager packageManager = getPackageManager(); + // Look for the toolbar icon specified in the activity meta-data + Bundle metaData = packageManager.getActivityInfo( + activityName, PackageManager.GET_META_DATA).metaData; + if (metaData != null) { + int iconResId = metaData.getInt(TOOLBAR_ICON_METADATA_NAME); + if (iconResId != 0) { + Resources res = packageManager.getResourcesForActivity(activityName); + toolbarIcon = res.getDrawable(iconResId); + } + } + } catch (NameNotFoundException e) { + // Do nothing + } + // If we were unable to find the icon via the meta-data, use a generic one + if (toolbarIcon == null) { + button.setImageResource(fallbackDrawableId); + } else { + button.setImageDrawable(toolbarIcon); + } + } + + private void updateGlobalSearchIcon() { + if (LauncherApplication.isScreenXLarge()) { + final SearchManager searchManager = + (SearchManager) getSystemService(Context.SEARCH_SERVICE); + ComponentName activityName = searchManager.getGlobalSearchActivity(); + if (activityName != null) { + updateButtonWithIconFromExternalActivity( + R.id.search_button, activityName, R.drawable.search_button_generic); + } else { + findViewById(R.id.search_button).setVisibility(View.GONE); + } + } + } + + private void updateVoiceSearchIcon() { + if (LauncherApplication.isScreenXLarge()) { + Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); + ComponentName activityName = intent.resolveActivity(getPackageManager()); + if (activityName != null) { + updateButtonWithIconFromExternalActivity( + R.id.voice_button, activityName, R.drawable.ic_voice_search); + } else { + findViewById(R.id.voice_button).setVisibility(View.GONE); + } + } + } + + /** + * Sets the app market icon (shown when all apps is visible on x-large screens) + */ + private void updateAppMarketIcon() { + if (LauncherApplication.isScreenXLarge()) { + Intent intent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_APP_MARKET); + // Find the app market activity by resolving an intent. + // (If multiple app markets are installed, it will return the ResolverActivity.) + ComponentName activityName = intent.resolveActivity(getPackageManager()); + if (activityName != null) { + mAppMarketIntent = intent; + updateButtonWithIconFromExternalActivity( + R.id.market_button, activityName, R.drawable.app_market_generic); + } + } + } + /** * Displays the shortcut creation dialog and launches, if necessary, the * appropriate activity. @@ -2043,7 +3003,6 @@ public final class Launcher extends Activity switch (which) { case AddAdapter.ITEM_SHORTCUT: { - // Insert extra item to handle picking application pickShortcut(); break; } @@ -2091,7 +3050,7 @@ public final class Launcher extends Activity } public void onShow(DialogInterface dialog) { - mWaitingForResult = true; + mWaitingForResult = true; } } @@ -2108,7 +3067,7 @@ public final class Launcher extends Activity if (mPaused || "lock".equals(reason)) { animate = false; } - closeAllApps(animate); + showWorkspace(animate); } } } @@ -2156,12 +3115,16 @@ public final class Launcher extends Activity */ public int getCurrentWorkspaceScreen() { if (mWorkspace != null) { - return mWorkspace.getCurrentScreen(); + return mWorkspace.getCurrentPage(); } else { return SCREEN_COUNT / 2; } } + void setAllAppsPagedView(PagedView view) { + mAllAppsPagedView = view; + } + /** * Refreshes the shortcuts shown on the workspace. * @@ -2211,15 +3174,15 @@ public final class Launcher extends Activity break; case LauncherSettings.Favorites.ITEM_TYPE_USER_FOLDER: final FolderIcon newFolder = FolderIcon.fromXml(R.layout.folder_icon, this, - (ViewGroup) workspace.getChildAt(workspace.getCurrentScreen()), - (UserFolderInfo) item); + (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()), + (UserFolderInfo) item, mIconCache); workspace.addInScreen(newFolder, item.screen, item.cellX, item.cellY, 1, 1, false); break; case LauncherSettings.Favorites.ITEM_TYPE_LIVE_FOLDER: final FolderIcon newLiveFolder = LiveFolderIcon.fromXml( R.layout.live_folder_icon, this, - (ViewGroup) workspace.getChildAt(workspace.getCurrentScreen()), + (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()), (LiveFolderInfo) item); workspace.addInScreen(newLiveFolder, item.screen, item.cellX, item.cellY, 1, 1, false); @@ -2267,6 +3230,8 @@ public final class Launcher extends Activity workspace.addInScreen(item.hostView, item.screen, item.cellX, item.cellY, item.spanX, item.spanY, false); + addWidgetToAutoAdvanceIfNeeded(item.hostView, appWidgetInfo); + workspace.requestLayout(); mDesktopItems.add(item); @@ -2287,7 +3252,7 @@ public final class Launcher extends Activity if (mSavedState != null) { if (!mWorkspace.hasFocus()) { - mWorkspace.getChildAt(mWorkspace.getCurrentScreen()).requestFocus(); + mWorkspace.getChildAt(mWorkspace.getCurrentPage()).requestFocus(); } final long[] userFolders = mSavedState.getLongArray(RUNTIME_STATE_USER_FOLDERS); @@ -2316,12 +3281,26 @@ public final class Launcher extends Activity } /** + * Updates the icons on the launcher that are affected by changes to the package list + * on the device. + */ + private void updateIconsAffectedByPackageManagerChanges() { + updateAppMarketIcon(); + updateGlobalSearchIcon(); + updateVoiceSearchIcon(); + } + + /** * Add the icons for all apps. * * Implementation of the method from LauncherModel.Callbacks. */ public void bindAllApplications(ArrayList<ApplicationInfo> apps) { mAllAppsGrid.setApps(apps); + if (mCustomizePagedView != null) { + mCustomizePagedView.setApps(apps); + } + updateIconsAffectedByPackageManagerChanges(); } /** @@ -2333,6 +3312,10 @@ public final class Launcher extends Activity setLoadOnResume(); removeDialog(DIALOG_CREATE_SHORTCUT); mAllAppsGrid.addApps(apps); + if (mCustomizePagedView != null) { + mCustomizePagedView.addApps(apps); + } + updateIconsAffectedByPackageManagerChanges(); } /** @@ -2345,6 +3328,10 @@ public final class Launcher extends Activity removeDialog(DIALOG_CREATE_SHORTCUT); mWorkspace.updateShortcuts(apps); mAllAppsGrid.updateApps(apps); + if (mCustomizePagedView != null) { + mCustomizePagedView.updateApps(apps); + } + updateIconsAffectedByPackageManagerChanges(); } /** @@ -2358,6 +3345,20 @@ public final class Launcher extends Activity mWorkspace.removeItems(apps); } mAllAppsGrid.removeApps(apps); + if (mCustomizePagedView != null) { + mCustomizePagedView.removeApps(apps); + } + updateIconsAffectedByPackageManagerChanges(); + } + + /** + * A number of packages were updated. + */ + public void bindPackagesUpdated() { + // update the customization drawer contents + if (mCustomizePagedView != null) { + mCustomizePagedView.update(); + } } /** diff --git a/src/com/android/launcher2/LauncherAnimatorListenerAdapter.java b/src/com/android/launcher2/LauncherAnimatorListenerAdapter.java new file mode 100644 index 0000000..3ab4868 --- /dev/null +++ b/src/com/android/launcher2/LauncherAnimatorListenerAdapter.java @@ -0,0 +1,70 @@ +/* + * 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.animation.Animator; + +import java.util.HashSet; + +/** + * This adapter class provides empty implementations of the methods from {@link android.animation.Animator.AnimatorListener}. + * Any custom listener that cares only about a subset of the methods of this listener can + * simply subclass this adapter class instead of implementing the interface directly. + */ +public abstract class LauncherAnimatorListenerAdapter implements Animator.AnimatorListener { + HashSet<Animator> cancelled = new HashSet<Animator>(); + + /** + * {@inheritDoc} + */ + @Override + public final void onAnimationCancel(Animator animation) { + onAnimationEndOrCancel(animation); + cancelled.add(animation); + } + + /** + * {@inheritDoc} + */ + @Override + public final void onAnimationEnd(Animator animation) { + if (!cancelled.contains(animation)) onAnimationEndOrCancel(animation); + cancelled.remove(animation); + } + + /** + * Like onAnimationEnd, except it's called immediately in the case on onAnimationCancel, and + * it's only called once in that case + */ + public void onAnimationEndOrCancel(Animator animation) { + } + + /** + * {@inheritDoc} + */ + @Override + public void onAnimationRepeat(Animator animation) { + } + + /** + * {@inheritDoc} + */ + @Override + public void onAnimationStart(Animator animation) { + } + +} diff --git a/src/com/android/launcher2/LauncherAppWidgetHost.java b/src/com/android/launcher2/LauncherAppWidgetHost.java index a5761ec..46e66e7 100644 --- a/src/com/android/launcher2/LauncherAppWidgetHost.java +++ b/src/com/android/launcher2/LauncherAppWidgetHost.java @@ -30,7 +30,7 @@ public class LauncherAppWidgetHost extends AppWidgetHost { public LauncherAppWidgetHost(Context context, int hostId) { super(context, hostId); } - + @Override protected AppWidgetHostView onCreateView(Context context, int appWidgetId, AppWidgetProviderInfo appWidget) { diff --git a/src/com/android/launcher2/LauncherAppWidgetInfo.java b/src/com/android/launcher2/LauncherAppWidgetInfo.java index 8499ebb..844abb5 100644 --- a/src/com/android/launcher2/LauncherAppWidgetInfo.java +++ b/src/com/android/launcher2/LauncherAppWidgetInfo.java @@ -17,30 +17,55 @@ package com.android.launcher2; import android.appwidget.AppWidgetHostView; +import android.content.ComponentName; import android.content.ContentValues; /** - * Represents a widget, which just contains an identifier. + * Represents a widget (either instantiated or about to be) in the Launcher. */ class LauncherAppWidgetInfo extends ItemInfo { /** + * Indicates that the widget hasn't been instantiated yet. + */ + static final int NO_ID = -1; + + /** * Identifier for this widget when talking with * {@link android.appwidget.AppWidgetManager} for updates. */ - int appWidgetId; - + int appWidgetId = NO_ID; + + ComponentName providerName; + + // TODO: Are these necessary here? + int minWidth = -1; + int minHeight = -1; + /** * View that holds this widget after it's been created. This view isn't created * until Launcher knows it's needed. */ AppWidgetHostView hostView = null; + /** + * Constructor for use with AppWidgets that haven't been instantiated yet. + */ + LauncherAppWidgetInfo(ComponentName providerName) { + itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; + this.providerName = providerName; + + // Since the widget isn't instantiated yet, we don't know these values. Set them to -1 + // to indicate that they should be calculated based on the layout and minWidth/minHeight + spanX = -1; + spanY = -1; + } + LauncherAppWidgetInfo(int appWidgetId) { itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; this.appWidgetId = appWidgetId; } - + @Override void onAddToDatabase(ContentValues values) { super.onAddToDatabase(values); @@ -52,7 +77,6 @@ class LauncherAppWidgetInfo extends ItemInfo { return "AppWidget(id=" + Integer.toString(appWidgetId) + ")"; } - @Override void unbind() { super.unbind(); diff --git a/src/com/android/launcher2/LauncherApplication.java b/src/com/android/launcher2/LauncherApplication.java index eda92d9..ed007dd 100644 --- a/src/com/android/launcher2/LauncherApplication.java +++ b/src/com/android/launcher2/LauncherApplication.java @@ -20,6 +20,7 @@ import android.app.Application; import android.content.ContentResolver; import android.content.Intent; import android.content.IntentFilter; +import android.content.res.Configuration; import android.database.ContentObserver; import android.os.Handler; import dalvik.system.VMRuntime; @@ -27,6 +28,9 @@ import dalvik.system.VMRuntime; public class LauncherApplication extends Application { public LauncherModel mModel; public IconCache mIconCache; + private static boolean sIsScreenXLarge; + private static float sScreenDensity; + private static final boolean ENABLE_ROTATION = false; @Override public void onCreate() { @@ -34,6 +38,10 @@ public class LauncherApplication extends Application { super.onCreate(); + // set sIsScreenXLarge and sScreenDensity *before* creating icon cache + sIsScreenXLarge = (getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_XLARGE; + sScreenDensity = getResources().getDisplayMetrics().density; + mIconCache = new IconCache(this); mModel = new LauncherModel(this, mIconCache); @@ -46,6 +54,7 @@ public class LauncherApplication extends Application { filter = new IntentFilter(); filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); + filter.addAction(Intent.ACTION_LOCALE_CHANGED); registerReceiver(mModel, filter); // Register for changes to the favorites @@ -89,4 +98,16 @@ public class LauncherApplication extends Application { LauncherModel getModel() { return mModel; } + + public static boolean isInPlaceRotationEnabled() { + return sIsScreenXLarge && ENABLE_ROTATION; + } + + public static boolean isScreenXLarge() { + return sIsScreenXLarge; + } + + public static float getScreenDensity() { + return sScreenDensity; + } } diff --git a/src/com/android/launcher2/LauncherModel.java b/src/com/android/launcher2/LauncherModel.java index b819510..713268a 100644 --- a/src/com/android/launcher2/LauncherModel.java +++ b/src/com/android/launcher2/LauncherModel.java @@ -16,6 +16,15 @@ package com.android.launcher2; +import java.lang.ref.WeakReference; +import java.net.URISyntaxException; +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; + import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; import android.content.BroadcastReceiver; @@ -23,9 +32,9 @@ import android.content.ComponentName; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.ContentValues; +import android.content.Context; import android.content.Intent; import android.content.Intent.ShortcutIconResource; -import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; @@ -38,22 +47,13 @@ import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; import android.os.Parcelable; -import android.os.RemoteException; -import android.util.Log; import android.os.Process; +import android.os.RemoteException; import android.os.SystemClock; - -import java.lang.ref.WeakReference; -import java.net.URISyntaxException; -import java.text.Collator; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; +import android.util.Log; import com.android.launcher.R; +import com.android.launcher2.InstallWidgetReceiver.WidgetMimeTypeHandlerData; /** * Maintains in-memory state of the Launcher. It is expected that there should be only one @@ -95,6 +95,9 @@ public class LauncherModel extends BroadcastReceiver { private Bitmap mDefaultIcon; + private static int mCellCountX; + private static int mCellCountY; + public interface Callbacks { public boolean setLoadOnResume(); public int getCurrentWorkspaceScreen(); @@ -107,6 +110,7 @@ public class LauncherModel extends BroadcastReceiver { public void bindAppsAdded(ArrayList<ApplicationInfo> apps); public void bindAppsUpdated(ArrayList<ApplicationInfo> apps); public void bindAppsRemoved(ArrayList<ApplicationInfo> apps, boolean permanent); + public void bindPackagesUpdated(); public boolean isAllAppsVisible(); } @@ -116,7 +120,7 @@ public class LauncherModel extends BroadcastReceiver { mIconCache = iconCache; mDefaultIcon = Utilities.createIconBitmap( - app.getPackageManager().getDefaultActivityIcon(), app); + mIconCache.getFullResDefaultActivityIcon(), app); mAllAppsLoadDelay = app.getResources().getInteger(R.integer.config_allAppsBatchLoadDelay); @@ -147,6 +151,7 @@ public class LauncherModel extends BroadcastReceiver { */ static void moveItemInDatabase(Context context, ItemInfo item, long container, int screen, int cellX, int cellY) { + item.container = container; item.screen = screen; item.cellX = cellX; @@ -157,8 +162,8 @@ public class LauncherModel extends BroadcastReceiver { final ContentResolver cr = context.getContentResolver(); values.put(LauncherSettings.Favorites.CONTAINER, item.container); - values.put(LauncherSettings.Favorites.CELLX, item.cellX); - values.put(LauncherSettings.Favorites.CELLY, item.cellY); + values.put(LauncherSettings.Favorites.CELLX, cellX); + values.put(LauncherSettings.Favorites.CELLY, cellY); values.put(LauncherSettings.Favorites.SCREEN, item.screen); sWorker.post(new Runnable() { @@ -187,6 +192,48 @@ public class LauncherModel extends BroadcastReceiver { } /** + * Returns an ItemInfo array containing all the items in the LauncherModel. + * The ItemInfo.id is not set through this function. + */ + static ArrayList<ItemInfo> getItemsInLocalCoordinates(Context context) { + ArrayList<ItemInfo> items = new ArrayList<ItemInfo>(); + final ContentResolver cr = context.getContentResolver(); + Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, new String[] { + LauncherSettings.Favorites.ITEM_TYPE, LauncherSettings.Favorites.CONTAINER, + LauncherSettings.Favorites.SCREEN, LauncherSettings.Favorites.CELLX, LauncherSettings.Favorites.CELLY, + LauncherSettings.Favorites.SPANX, LauncherSettings.Favorites.SPANY }, null, null, null); + + final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); + final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER); + final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); + final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); + final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); + final int spanXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANX); + final int spanYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANY); + + try { + while (c.moveToNext()) { + ItemInfo item = new ItemInfo(); + item.cellX = c.getInt(cellXIndex); + item.cellY = c.getInt(cellYIndex); + item.spanX = c.getInt(spanXIndex); + item.spanY = c.getInt(spanYIndex); + item.container = c.getInt(containerIndex); + item.itemType = c.getInt(itemTypeIndex); + item.screen = c.getInt(screenIndex); + + items.add(item); + } + } catch (Exception e) { + items.clear(); + } finally { + c.close(); + } + + return items; + } + + /** * Find a folder in the db, creating the FolderInfo if necessary, and adding it to folderList. */ FolderInfo getFolderById(Context context, HashMap<Long,FolderInfo> folderList, long id) { @@ -245,9 +292,10 @@ public class LauncherModel extends BroadcastReceiver { final ContentValues values = new ContentValues(); final ContentResolver cr = context.getContentResolver(); - item.onAddToDatabase(values); + item.updateValuesWithCoordinates(values, cellX, cellY); + Uri result = cr.insert(notify ? LauncherSettings.Favorites.CONTENT_URI : LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, values); @@ -257,6 +305,32 @@ public class LauncherModel extends BroadcastReceiver { } /** + * Creates a new unique child id, for a given cell span across all layouts. + */ + static int getCellLayoutChildId( + int cellId, int screen, int localCellX, int localCellY, int spanX, int spanY) { + return ((cellId & 0xFF) << 24) + | (screen & 0xFF) << 16 | (localCellX & 0xFF) << 8 | (localCellY & 0xFF); + } + + static int getCellCountX() { + return mCellCountX; + } + + static int getCellCountY() { + return mCellCountY; + } + + /** + * Updates the model orientation helper to take into account the current layout dimensions + * when performing local/canonical coordinate transformations. + */ + static void updateWorkspaceLayoutCells(int shortAxisCellCount, int longAxisCellCount) { + mCellCountX = shortAxisCellCount; + mCellCountY = longAxisCellCount; + } + + /** * Update an item to the database in a specified container. */ static void updateItemInDatabase(Context context, ItemInfo item) { @@ -264,6 +338,7 @@ public class LauncherModel extends BroadcastReceiver { final ContentResolver cr = context.getContentResolver(); item.onAddToDatabase(values); + item.updateValuesWithCoordinates(values, item.cellX, item.cellY); cr.update(LauncherSettings.Favorites.getContentUri(item.id, false), values, null, null); } @@ -309,7 +384,7 @@ public class LauncherModel extends BroadcastReceiver { */ public void onReceive(Context context, Intent intent) { if (DEBUG_LOADERS) Log.d(TAG, "onReceive intent=" + intent); - + final String action = intent.getAction(); if (Intent.ACTION_PACKAGE_CHANGED.equals(action) @@ -350,25 +425,41 @@ public class LauncherModel extends BroadcastReceiver { String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_ADD, packages)); // Then, rebind everything. - boolean runLoader = true; - if (mCallbacks != null) { - Callbacks callbacks = mCallbacks.get(); - if (callbacks != null) { - // If they're paused, we can skip loading, because they'll do it again anyway - if (callbacks.setLoadOnResume()) { - runLoader = false; - } - } - } - if (runLoader) { - startLoader(mApp, false); - } - + startLoaderFromBackground(); } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); enqueuePackageUpdated(new PackageUpdatedTask( PackageUpdatedTask.OP_UNAVAILABLE, packages)); + } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) { + // If we have changed locale we need to clear out the labels in all apps. + // 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. Either way, mAllAppsLoaded will be cleared so it re-reads everything + // next time. + mAllAppsLoaded = false; + startLoaderFromBackground(); + } + } + /** + * When the launcher is in the background, it's possible for it to miss paired + * configuration changes. So whenever we trigger the loader from the background + * tell the launcher that it needs to re-run the loader when it comes back instead + * of doing it now. + */ + public void startLoaderFromBackground() { + boolean runLoader = false; + if (mCallbacks != null) { + Callbacks callbacks = mCallbacks.get(); + if (callbacks != null) { + // Only actually run the loader if they're not paused. + if (!callbacks.setLoadOnResume()) { + runLoader = true; + } + } + } + if (runLoader) { + startLoader(mApp, false); } } @@ -474,7 +565,7 @@ public class LauncherModel extends BroadcastReceiver { } if (DEBUG_LOADERS) { Log.d(TAG, "waited " - + (SystemClock.uptimeMillis()-workspaceWaitTime) + + (SystemClock.uptimeMillis()-workspaceWaitTime) + "ms for previous step to finish binding"); } } @@ -494,7 +585,6 @@ public class LauncherModel extends BroadcastReceiver { android.os.Process.setThreadPriority(mIsLaunching ? Process.THREAD_PRIORITY_DEFAULT : Process.THREAD_PRIORITY_BACKGROUND); } - if (loadWorkspaceFirst) { if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace"); loadAndBindWorkspace(); @@ -596,14 +686,13 @@ public class LauncherModel extends BroadcastReceiver { if (item.container != LauncherSettings.Favorites.CONTAINER_DESKTOP) { return true; } - for (int x = item.cellX; x < (item.cellX+item.spanX); x++) { for (int y = item.cellY; y < (item.cellY+item.spanY); y++) { if (occupied[item.screen][x][y] != null) { Log.e(TAG, "Error loading shortcut " + item - + " into cell (" + item.screen + ":" + + " into cell (" + item.screen + ":" + x + "," + y - + ") occupied by " + + ") occupied by " + occupied[item.screen][x][y]); return false; } @@ -635,7 +724,8 @@ public class LauncherModel extends BroadcastReceiver { final Cursor c = contentResolver.query( LauncherSettings.Favorites.CONTENT_URI, null, null, null, null); - final ItemInfo occupied[][][] = new ItemInfo[Launcher.SCREEN_COUNT][Launcher.NUMBER_CELLS_X][Launcher.NUMBER_CELLS_Y]; + final ItemInfo occupied[][][] = + new ItemInfo[Launcher.SCREEN_COUNT][mCellCountX][mCellCountY]; try { final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); @@ -744,7 +834,6 @@ public class LauncherModel extends BroadcastReceiver { UserFolderInfo folderInfo = findOrMakeUserFolder(mFolders, id); folderInfo.title = c.getString(titleIndex); - folderInfo.id = id; container = c.getInt(containerIndex); folderInfo.container = container; @@ -756,7 +845,6 @@ public class LauncherModel extends BroadcastReceiver { if (!checkItemPlacement(occupied, folderInfo)) { break; } - switch (container) { case LauncherSettings.Favorites.CONTAINER_DESKTOP: mItems.add(folderInfo); @@ -779,7 +867,6 @@ public class LauncherModel extends BroadcastReceiver { itemsToRemove.add(id); } else { LiveFolderInfo liveFolderInfo = findOrMakeLiveFolder(mFolders, id); - intentDescription = c.getString(intentIndex); intent = null; if (intentDescription != null) { @@ -825,7 +912,7 @@ public class LauncherModel extends BroadcastReceiver { final AppWidgetProviderInfo provider = widgets.getAppWidgetInfo(appWidgetId); - + if (!isSafeMode && (provider == null || provider.provider == null || provider.provider.getPackageName() == null)) { Log.e(TAG, "Deleting widget that isn't installed anymore: id=" @@ -886,13 +973,13 @@ public class LauncherModel extends BroadcastReceiver { if (DEBUG_LOADERS) { Log.d(TAG, "loaded workspace in " + (SystemClock.uptimeMillis()-t) + "ms"); Log.d(TAG, "workspace layout: "); - for (int y = 0; y < Launcher.NUMBER_CELLS_Y; y++) { + for (int y = 0; y < mCellCountY; y++) { String line = ""; for (int s = 0; s < Launcher.SCREEN_COUNT; s++) { if (s > 0) { line += " | "; } - for (int x = 0; x < Launcher.NUMBER_CELLS_X; x++) { + for (int x = 0; x < mCellCountX; x++) { line += ((occupied[s][x][y] != null) ? "#" : "."); } } @@ -1116,7 +1203,7 @@ public class LauncherModel extends BroadcastReceiver { startIndex = i; for (int j=0; i<N && j<batchSize; j++) { // This builds the icon bitmaps. - mAllAppsList.add(new ApplicationInfo(apps.get(i), mIconCache)); + mAllAppsList.add(new ApplicationInfo(packageManager, apps.get(i), mIconCache)); i++; } @@ -1279,6 +1366,15 @@ public class LauncherModel extends BroadcastReceiver { } }); } + + mHandler.post(new Runnable() { + @Override + public void run() { + if (callbacks == mCallbacks.get()) { + callbacks.bindPackagesUpdated(); + } + } + }); } } @@ -1374,7 +1470,8 @@ public class LauncherModel extends BroadcastReceiver { Resources resources = packageManager.getResourcesForApplication(packageName); if (resources != null) { final int id = resources.getIdentifier(resourceName, null, null); - icon = Utilities.createIconBitmap(resources.getDrawable(id), context); + icon = Utilities.createIconBitmap( + mIconCache.getFullResIcon(resources, id), context); } } catch (Exception e) { // drop this. we have other places to look for icons @@ -1423,16 +1520,69 @@ public class LauncherModel extends BroadcastReceiver { } ShortcutInfo addShortcut(Context context, Intent data, - CellLayout.CellInfo cellInfo, boolean notify) { + int screen, int cellX, int cellY, boolean notify) { - final ShortcutInfo info = infoFromShortcutIntent(context, data); + final ShortcutInfo info = infoFromShortcutIntent(context, data, null); addItemToDatabase(context, info, LauncherSettings.Favorites.CONTAINER_DESKTOP, - cellInfo.screen, cellInfo.cellX, cellInfo.cellY, notify); + screen, cellX, cellY, notify); return info; } - private ShortcutInfo infoFromShortcutIntent(Context context, Intent data) { + /** + * Attempts to find an AppWidgetProviderInfo that matches the given component. + */ + AppWidgetProviderInfo findAppWidgetProviderInfoWithComponent(Context context, + ComponentName component) { + List<AppWidgetProviderInfo> widgets = + AppWidgetManager.getInstance(context).getInstalledProviders(); + for (AppWidgetProviderInfo info : widgets) { + if (info.provider.equals(component)) { + return info; + } + } + return null; + } + + /** + * Returns a list of all the widgets that can handle configuration with a particular mimeType. + */ + List<WidgetMimeTypeHandlerData> resolveWidgetsForMimeType(Context context, String mimeType) { + final PackageManager packageManager = context.getPackageManager(); + final List<WidgetMimeTypeHandlerData> supportedConfigurationActivities = + new ArrayList<WidgetMimeTypeHandlerData>(); + + final Intent supportsIntent = + new Intent(InstallWidgetReceiver.ACTION_SUPPORTS_CLIPDATA_MIMETYPE); + supportsIntent.setType(mimeType); + + // Create a set of widget configuration components that we can test against + final List<AppWidgetProviderInfo> widgets = + AppWidgetManager.getInstance(context).getInstalledProviders(); + final HashMap<ComponentName, AppWidgetProviderInfo> configurationComponentToWidget = + new HashMap<ComponentName, AppWidgetProviderInfo>(); + for (AppWidgetProviderInfo info : widgets) { + configurationComponentToWidget.put(info.configure, info); + } + + // Run through each of the intents that can handle this type of clip data, and cross + // reference them with the components that are actual configuration components + final List<ResolveInfo> activities = packageManager.queryIntentActivities(supportsIntent, + PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo info : activities) { + final ActivityInfo activityInfo = info.activityInfo; + final ComponentName infoComponent = new ComponentName(activityInfo.packageName, + activityInfo.name); + if (configurationComponentToWidget.containsKey(infoComponent)) { + supportedConfigurationActivities.add( + new InstallWidgetReceiver.WidgetMimeTypeHandlerData(info, + configurationComponentToWidget.get(infoComponent))); + } + } + return supportedConfigurationActivities; + } + + ShortcutInfo infoFromShortcutIntent(Context context, Intent data, Bitmap fallbackIcon) { Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); Parcelable bitmap = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON); @@ -1455,7 +1605,8 @@ public class LauncherModel extends BroadcastReceiver { Resources resources = packageManager.getResourcesForApplication( iconResource.packageName); final int id = resources.getIdentifier(iconResource.resourceName, null, null); - icon = Utilities.createIconBitmap(resources.getDrawable(id), context); + icon = Utilities.createIconBitmap( + mIconCache.getFullResIcon(resources, id), context); } catch (Exception e) { Log.w(TAG, "Could not load shortcut icon: " + extra); } @@ -1465,8 +1616,12 @@ public class LauncherModel extends BroadcastReceiver { final ShortcutInfo info = new ShortcutInfo(); if (icon == null) { - icon = getFallbackIcon(); - info.usingFallbackIcon = true; + if (fallbackIcon != null) { + icon = fallbackIcon; + } else { + icon = getFallbackIcon(); + info.usingFallbackIcon = true; + } } info.setIcon(icon); @@ -1478,7 +1633,7 @@ public class LauncherModel extends BroadcastReceiver { return info; } - private static void loadLiveFolderIcon(Context context, Cursor c, int iconTypeIndex, + private void loadLiveFolderIcon(Context context, Cursor c, int iconTypeIndex, int iconPackageIndex, int iconResourceIndex, LiveFolderInfo liveFolderInfo) { int iconType = c.getInt(iconTypeIndex); @@ -1488,13 +1643,14 @@ public class LauncherModel extends BroadcastReceiver { String resourceName = c.getString(iconResourceIndex); PackageManager packageManager = context.getPackageManager(); try { - Resources resources = packageManager.getResourcesForApplication(packageName); - final int id = resources.getIdentifier(resourceName, null, null); - liveFolderInfo.icon = Utilities.createIconBitmap(resources.getDrawable(id), - context); + Resources appResources = packageManager.getResourcesForApplication(packageName); + final int id = appResources.getIdentifier(resourceName, null, null); + liveFolderInfo.icon = Utilities.createIconBitmap( + mIconCache.getFullResIcon(appResources, id), context); } catch (Exception e) { + Resources resources = context.getResources(); liveFolderInfo.icon = Utilities.createIconBitmap( - context.getResources().getDrawable(R.drawable.ic_launcher_folder), + mIconCache.getFullResIcon(resources, R.drawable.ic_launcher_folder), context); } liveFolderInfo.iconResource = new Intent.ShortcutIconResource(); @@ -1502,8 +1658,9 @@ public class LauncherModel extends BroadcastReceiver { liveFolderInfo.iconResource.resourceName = resourceName; break; default: + Resources resources = context.getResources(); liveFolderInfo.icon = Utilities.createIconBitmap( - context.getResources().getDrawable(R.drawable.ic_launcher_folder), + mIconCache.getFullResIcon(resources, R.drawable.ic_launcher_folder), context); } } diff --git a/src/com/android/launcher2/LiveFolderInfo.java b/src/com/android/launcher2/LiveFolderInfo.java index 7d0a0f5..74b0217 100644 --- a/src/com/android/launcher2/LiveFolderInfo.java +++ b/src/com/android/launcher2/LiveFolderInfo.java @@ -18,7 +18,6 @@ package com.android.launcher2; import android.content.ContentValues; import android.content.Intent; -import android.graphics.drawable.Drawable; import android.graphics.Bitmap; import android.net.Uri; diff --git a/src/com/android/launcher2/PagedView.java b/src/com/android/launcher2/PagedView.java new file mode 100644 index 0000000..09133e0 --- /dev/null +++ b/src/com/android/launcher2/PagedView.java @@ -0,0 +1,1389 @@ +/* + * 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 java.util.ArrayList; +import java.util.HashMap; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.ActionMode; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.view.animation.Animation.AnimationListener; +import android.widget.Checkable; +import android.widget.Scroller; + +import com.android.launcher.R; + +/** + * An abstraction of the original Workspace which supports browsing through a + * sequential list of "pages" + */ +public abstract class PagedView extends ViewGroup { + private static final String TAG = "PagedView"; + protected static final int INVALID_PAGE = -1; + + // the min drag distance for a fling to register, to prevent random page shifts + private static final int MIN_LENGTH_FOR_FLING = 25; + // The min drag distance to trigger a page shift (regardless of velocity) + private static final int MIN_LENGTH_FOR_MOVE = 200; + + private static final int PAGE_SNAP_ANIMATION_DURATION = 550; + protected static final float NANOTIME_DIV = 1000000000.0f; + + private static final float OVERSCROLL_DAMP_FACTOR = 0.08f; + private static final int MINIMUM_SNAP_VELOCITY = 2200; + private static final int MIN_FLING_VELOCITY = 250; + + // the velocity at which a fling gesture will cause us to snap to the next page + protected int mSnapVelocity = 500; + + protected float mSmoothingTime; + protected float mTouchX; + + protected boolean mFirstLayout = true; + + protected int mCurrentPage; + protected int mNextPage = INVALID_PAGE; + protected int mMaxScrollX; + protected Scroller mScroller; + private VelocityTracker mVelocityTracker; + + private float mDownMotionX; + protected float mLastMotionX; + protected float mLastMotionY; + private int mLastScreenCenter = -1; + + protected final static int TOUCH_STATE_REST = 0; + protected final static int TOUCH_STATE_SCROLLING = 1; + protected final static int TOUCH_STATE_PREV_PAGE = 2; + protected final static int TOUCH_STATE_NEXT_PAGE = 3; + protected final static float ALPHA_QUANTIZE_LEVEL = 0.0001f; + + protected int mTouchState = TOUCH_STATE_REST; + + protected OnLongClickListener mLongClickListener; + + protected boolean mAllowLongPress = true; + + protected int mTouchSlop; + private int mPagingTouchSlop; + private int mMaximumVelocity; + protected int mPageSpacing; + protected int mPageLayoutPaddingTop; + protected int mPageLayoutPaddingBottom; + protected int mPageLayoutPaddingLeft; + protected int mPageLayoutPaddingRight; + protected int mPageLayoutWidthGap; + protected int mPageLayoutHeightGap; + protected int mCellCountX; + protected int mCellCountY; + protected boolean mCenterPagesVertically; + protected boolean mAllowOverScroll = true; + protected int mUnboundedScrollX; + + protected static final int INVALID_POINTER = -1; + + protected int mActivePointerId = INVALID_POINTER; + + private PageSwitchListener mPageSwitchListener; + + private ArrayList<Boolean> mDirtyPageContent; + private boolean mDirtyPageAlpha; + + // 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; + + protected PagedViewIconCache mPageViewIconCache; + + // If true, syncPages and syncPageItems will be called to refresh pages + protected boolean mContentIsRefreshable = true; + + // If true, modify alpha of neighboring pages as user scrolls left/right + protected boolean mFadeInAdjacentScreens = true; + + // It true, use a different slop parameter (pagingTouchSlop = 2 * touchSlop) for deciding + // to switch to a new page + protected boolean mUsePagingTouchSlop = true; + + // If true, the subclass should directly update mScrollX itself in its computeScroll method + // (SmoothPagedView does this) + protected boolean mDeferScrollUpdate = false; + + protected boolean mIsPageMoving = false; + + /** + * Simple cache mechanism for PagedViewIcon outlines. + */ + class PagedViewIconCache { + private final HashMap<Object, Bitmap> iconOutlineCache = new HashMap<Object, Bitmap>(); + + public void clear() { + iconOutlineCache.clear(); + } + public void addOutline(Object key, Bitmap b) { + iconOutlineCache.put(key, b); + } + public void removeOutline(Object key) { + if (iconOutlineCache.containsKey(key)) { + iconOutlineCache.remove(key); + } + } + public Bitmap getOutline(Object key) { + return iconOutlineCache.get(key); + } + } + + public interface PageSwitchListener { + void onPageSwitch(View newPage, int newPageIndex); + } + + public PagedView(Context context) { + this(context, null); + } + + public PagedView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + 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); + mPageSpacing = a.getDimensionPixelSize(R.styleable.PagedView_pageSpacing, 0); + mPageLayoutPaddingTop = a.getDimensionPixelSize( + R.styleable.PagedView_pageLayoutPaddingTop, 10); + mPageLayoutPaddingBottom = a.getDimensionPixelSize( + R.styleable.PagedView_pageLayoutPaddingBottom, 10); + mPageLayoutPaddingLeft = a.getDimensionPixelSize( + R.styleable.PagedView_pageLayoutPaddingLeft, 10); + mPageLayoutPaddingRight = a.getDimensionPixelSize( + R.styleable.PagedView_pageLayoutPaddingRight, 10); + mPageLayoutWidthGap = a.getDimensionPixelSize( + R.styleable.PagedView_pageLayoutWidthGap, -1); + mPageLayoutHeightGap = a.getDimensionPixelSize( + R.styleable.PagedView_pageLayoutHeightGap, -1); + a.recycle(); + + setHapticFeedbackEnabled(false); + init(); + } + + /** + * Initializes various states for this workspace. + */ + protected void init() { + mDirtyPageContent = new ArrayList<Boolean>(); + mDirtyPageContent.ensureCapacity(32); + mPageViewIconCache = new PagedViewIconCache(); + mScroller = new Scroller(getContext(), new ScrollInterpolator()); + mCurrentPage = 0; + mCenterPagesVertically = true; + + final ViewConfiguration configuration = ViewConfiguration.get(getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); + mPagingTouchSlop = configuration.getScaledPagingTouchSlop(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + } + + public void setPageSwitchListener(PageSwitchListener pageSwitchListener) { + mPageSwitchListener = pageSwitchListener; + if (mPageSwitchListener != null) { + mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage); + } + } + + /** + * Returns the index of the currently displayed page. + * + * @return The index of the currently displayed page. + */ + int getCurrentPage() { + return mCurrentPage; + } + + int getPageCount() { + return getChildCount(); + } + + View getPageAt(int index) { + return getChildAt(index); + } + + int getScrollWidth() { + return getWidth(); + } + + /** + * Updates the scroll of the current page immediately to its final scroll position. We use this + * in CustomizePagedView to allow tabs to share the same PagedView while resetting the scroll of + * the previous tab page. + */ + protected void updateCurrentPageScroll() { + int newX = getChildOffset(mCurrentPage) - getRelativeChildOffset(mCurrentPage); + scrollTo(newX, 0); + mScroller.setFinalX(newX); + } + + /** + * Sets the current page. + */ + void setCurrentPage(int currentPage) { + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + if (getChildCount() == 0 || currentPage == mCurrentPage) { + return; + } + + mCurrentPage = Math.max(0, Math.min(currentPage, getPageCount() - 1)); + updateCurrentPageScroll(); + + invalidate(); + notifyPageSwitchListener(); + } + + protected void notifyPageSwitchListener() { + if (mPageSwitchListener != null) { + mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage); + } + } + + private void pageBeginMoving() { + mIsPageMoving = true; + onPageBeginMoving(); + } + + private void pageEndMoving() { + onPageEndMoving(); + mIsPageMoving = false; + } + + // a method that subclasses can override to add behavior + protected void onPageBeginMoving() { + } + + // a method that subclasses can override to add behavior + protected void onPageEndMoving() { + } + + /** + * Registers the specified listener on each page contained in this workspace. + * + * @param l The listener used to respond to long clicks. + */ + @Override + public void setOnLongClickListener(OnLongClickListener l) { + mLongClickListener = l; + final int count = getPageCount(); + for (int i = 0; i < count; i++) { + getPageAt(i).setOnLongClickListener(l); + } + } + + @Override + public void scrollBy(int x, int y) { + scrollTo(mUnboundedScrollX + x, mScrollY + y); + } + + @Override + public void scrollTo(int x, int y) { + mUnboundedScrollX = x; + + if (x < 0) { + super.scrollTo(0, y); + if (mAllowOverScroll) { + overScroll(x); + } + } else if (x > mMaxScrollX) { + super.scrollTo(mMaxScrollX, y); + if (mAllowOverScroll) { + overScroll(x - mMaxScrollX); + } + } else { + super.scrollTo(x, y); + } + + mTouchX = x; + mSmoothingTime = System.nanoTime() / NANOTIME_DIV; + } + + // we moved this functionality to a helper function so SmoothPagedView can reuse it + protected boolean computeScrollHelper() { + if (mScroller.computeScrollOffset()) { + mDirtyPageAlpha = true; + scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); + invalidate(); + return true; + } else if (mNextPage != INVALID_PAGE) { + mDirtyPageAlpha = true; + mCurrentPage = Math.max(0, Math.min(mNextPage, getPageCount() - 1)); + mNextPage = INVALID_PAGE; + notifyPageSwitchListener(); + pageEndMoving(); + return true; + } + return false; + } + + @Override + public void computeScroll() { + computeScrollHelper(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + if (widthMode != MeasureSpec.EXACTLY) { + throw new IllegalStateException("Workspace can only be used in EXACTLY mode."); + } + + /* Allow the height to be set as WRAP_CONTENT. This allows the particular case + * of the All apps view on XLarge displays to not take up more space then it needs. Width + * is still not allowed to be set as WRAP_CONTENT since many parts of the code expect + * each page to have the same width. + */ + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int maxChildHeight = 0; + + final int verticalPadding = mPaddingTop + mPaddingBottom; + + // The children are given the same width and height as the workspace + // unless they were set to WRAP_CONTENT + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + // disallowing padding in paged view (just pass 0) + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + int childWidthMode; + if (lp.width == LayoutParams.WRAP_CONTENT) { + childWidthMode = MeasureSpec.AT_MOST; + } else { + childWidthMode = MeasureSpec.EXACTLY; + } + + int childHeightMode; + if (lp.height == LayoutParams.WRAP_CONTENT) { + childHeightMode = MeasureSpec.AT_MOST; + } else { + childHeightMode = MeasureSpec.EXACTLY; + } + + final int childWidthMeasureSpec = + MeasureSpec.makeMeasureSpec(widthSize, childWidthMode); + final int childHeightMeasureSpec = + MeasureSpec.makeMeasureSpec(heightSize - verticalPadding, childHeightMode); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + maxChildHeight = Math.max(maxChildHeight, child.getMeasuredHeight()); + } + + if (heightMode == MeasureSpec.AT_MOST) { + heightSize = maxChildHeight + verticalPadding; + } + if (childCount > 0) { + mMaxScrollX = getChildOffset(childCount - 1) - getRelativeChildOffset(childCount - 1); + } else { + mMaxScrollX = 0; + } + + setMeasuredDimension(widthSize, heightSize); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) { + setHorizontalScrollBarEnabled(false); + int newX = getChildOffset(mCurrentPage) - getRelativeChildOffset(mCurrentPage); + scrollTo(newX, 0); + mScroller.setFinalX(newX); + setHorizontalScrollBarEnabled(true); + mFirstLayout = false; + } + + final int verticalPadding = mPaddingTop + mPaddingBottom; + final int childCount = getChildCount(); + int childLeft = 0; + if (childCount > 0) { + childLeft = getRelativeChildOffset(0); + } + + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != View.GONE) { + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + int childTop = mPaddingTop; + if (mCenterPagesVertically) { + childTop += ((getMeasuredHeight() - verticalPadding) - childHeight) / 2; + } + child.layout(childLeft, childTop, + childLeft + childWidth, childTop + childHeight); + childLeft += childWidth + mPageSpacing; + } + } + } + + protected void updateAdjacentPagesAlpha() { + if (mFadeInAdjacentScreens) { + if (mDirtyPageAlpha || (mTouchState == TOUCH_STATE_SCROLLING) || !mScroller.isFinished()) { + int halfScreenSize = getMeasuredWidth() / 2; + int screenCenter = mScrollX + halfScreenSize; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; ++i) { + View layout = (View) getChildAt(i); + int childWidth = layout.getMeasuredWidth(); + int halfChildWidth = (childWidth / 2); + int childCenter = getChildOffset(i) + halfChildWidth; + + // On the first layout, we may not have a width nor a proper offset, so for now + // we should just assume full page width (and calculate the offset according to + // that). + if (childWidth <= 0) { + childWidth = getMeasuredWidth(); + childCenter = (i * childWidth) + (childWidth / 2); + } + + int d = halfChildWidth; + int distanceFromScreenCenter = childCenter - screenCenter; + if (distanceFromScreenCenter > 0) { + if (i > 0) { + d += getChildAt(i - 1).getMeasuredWidth() / 2; + } + } else { + if (i < childCount - 1) { + d += getChildAt(i + 1).getMeasuredWidth() / 2; + } + } + d += mPageSpacing; + + // Preventing potential divide-by-zero + d = Math.max(1, d); + + float dimAlpha = (float) (Math.abs(distanceFromScreenCenter)) / d; + dimAlpha = Math.max(0.0f, Math.min(1.0f, (dimAlpha * dimAlpha))); + float alpha = 1.0f - dimAlpha; + + if (alpha < ALPHA_QUANTIZE_LEVEL) { + alpha = 0.0f; + } else if (alpha > 1.0f - ALPHA_QUANTIZE_LEVEL) { + alpha = 1.0f; + } + + if (Float.compare(alpha, layout.getAlpha()) != 0) { + layout.setAlpha(alpha); + } + } + mDirtyPageAlpha = false; + } + } + } + + protected void screenScrolled(int screenCenter) { + } + + @Override + protected void dispatchDraw(Canvas canvas) { + int halfScreenSize = getMeasuredWidth() / 2; + int screenCenter = mScrollX + halfScreenSize; + + if (screenCenter != mLastScreenCenter) { + screenScrolled(screenCenter); + updateAdjacentPagesAlpha(); + mLastScreenCenter = screenCenter; + } + + // Find out which screens are visible; as an optimization we only call draw on them + // As an optimization, this code assumes that all pages have the same width as the 0th + // page. + final int pageCount = getChildCount(); + if (pageCount > 0) { + final int pageWidth = getChildAt(0).getMeasuredWidth(); + final int screenWidth = getMeasuredWidth(); + int x = getRelativeChildOffset(0) + pageWidth; + int leftScreen = 0; + int rightScreen = 0; + while (x <= mScrollX) { + leftScreen++; + x += pageWidth + mPageSpacing; + // replace above line with this if you don't assume all pages have same width as 0th + // page: + // x += getChildAt(leftScreen).getMeasuredWidth(); + } + rightScreen = leftScreen; + while (x < mScrollX + screenWidth) { + rightScreen++; + x += pageWidth + mPageSpacing; + // replace above line with this if you don't assume all pages have same width as 0th + // page: + //if (rightScreen < pageCount) { + // x += getChildAt(rightScreen).getMeasuredWidth(); + //} + } + rightScreen = Math.min(getChildCount() - 1, rightScreen); + + final long drawingTime = getDrawingTime(); + for (int i = leftScreen; i <= rightScreen; i++) { + drawChild(canvas, getChildAt(i), drawingTime); + } + } + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { + int page = indexOfChild(child); + if (page != mCurrentPage || !mScroller.isFinished()) { + snapToPage(page); + return true; + } + return false; + } + + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + int focusablePage; + if (mNextPage != INVALID_PAGE) { + focusablePage = mNextPage; + } else { + focusablePage = mCurrentPage; + } + View v = getPageAt(focusablePage); + if (v != null) { + v.requestFocus(direction, previouslyFocusedRect); + } + return false; + } + + @Override + public boolean dispatchUnhandledMove(View focused, int direction) { + if (direction == View.FOCUS_LEFT) { + if (getCurrentPage() > 0) { + snapToPage(getCurrentPage() - 1); + return true; + } + } else if (direction == View.FOCUS_RIGHT) { + if (getCurrentPage() < getPageCount() - 1) { + snapToPage(getCurrentPage() + 1); + return true; + } + } + return super.dispatchUnhandledMove(focused, direction); + } + + @Override + public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { + if (mCurrentPage >= 0 && mCurrentPage < getPageCount()) { + getPageAt(mCurrentPage).addFocusables(views, direction); + } + if (direction == View.FOCUS_LEFT) { + if (mCurrentPage > 0) { + getPageAt(mCurrentPage - 1).addFocusables(views, direction); + } + } else if (direction == View.FOCUS_RIGHT){ + if (mCurrentPage < getPageCount() - 1) { + getPageAt(mCurrentPage + 1).addFocusables(views, direction); + } + } + } + + /** + * If one of our descendant views decides that it could be focused now, only + * pass that along if it's on the current page. + * + * This happens when live folders requery, and if they're off page, they + * end up calling requestFocus, which pulls it on page. + */ + @Override + public void focusableViewAvailable(View focused) { + View current = getPageAt(mCurrentPage); + View v = focused; + while (true) { + if (v == current) { + super.focusableViewAvailable(focused); + return; + } + if (v == this) { + return; + } + ViewParent parent = v.getParent(); + if (parent instanceof View) { + v = (View)v.getParent(); + } else { + return; + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (disallowIntercept) { + // We need to make sure to cancel our long press if + // a scrollable widget takes over touch events + final View currentPage = getChildAt(mCurrentPage); + currentPage.cancelLongPress(); + } + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onTouchEvent will be called and we do the actual + * scrolling there. + */ + + // Skip touch handling if there are no pages to swipe + if (getChildCount() <= 0) return super.onInterceptTouchEvent(ev); + + /* + * Shortcut the most recurring case: the user is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && + (mTouchState == TOUCH_STATE_SCROLLING)) { + return true; + } + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + if (mActivePointerId != INVALID_POINTER) { + determineScrollingStart(ev); + break; + } + // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN + // event. in that case, treat the first occurence of a move event as a ACTION_DOWN + // i.e. fall through to the next case (don't break) + // (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events + // while it's small- this was causing a crash before we checked for INVALID_POINTER) + } + + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + // Remember location of down touch + mDownMotionX = x; + mLastMotionX = x; + mLastMotionY = y; + mActivePointerId = ev.getPointerId(0); + mAllowLongPress = true; + + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. + */ + final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX()); + final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop); + if (finishedScrolling) { + mTouchState = TOUCH_STATE_REST; + mScroller.abortAnimation(); + } else { + mTouchState = TOUCH_STATE_SCROLLING; + } + + // check if this can be the beginning of a tap on the side of the pages + // to scroll the current page + if ((mTouchState != TOUCH_STATE_PREV_PAGE) && !handlePagingClicks() && + (mTouchState != TOUCH_STATE_NEXT_PAGE)) { + if (getChildCount() > 0) { + int width = getMeasuredWidth(); + int offset = getRelativeChildOffset(mCurrentPage); + if (x < offset - mPageSpacing) { + mTouchState = TOUCH_STATE_PREV_PAGE; + } else if (x > (width - offset + mPageSpacing)) { + mTouchState = TOUCH_STATE_NEXT_PAGE; + } + } + } + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + mTouchState = TOUCH_STATE_REST; + mAllowLongPress = false; + mActivePointerId = INVALID_POINTER; + break; + + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + 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" + Animation anim = AnimationUtils.loadAnimation(getContext(), + R.anim.paged_view_click_feedback); + anim.setAnimationListener(new AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} + @Override + public void onAnimationRepeat(Animation animation) { + r.run(); + } + @Override + public void onAnimationEnd(Animation animation) {} + }); + v.startAnimation(anim); + } + + /* + * Determines if we should change the touch state to start scrolling after the + * user moves their touch point too far. + */ + protected void determineScrollingStart(MotionEvent ev) { + /* + * Locally do absolute value. mLastMotionX is set to the y value + * of the down event. + */ + final int pointerIndex = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(pointerIndex); + final float y = ev.getY(pointerIndex); + final int xDiff = (int) Math.abs(x - mLastMotionX); + final int yDiff = (int) Math.abs(y - mLastMotionY); + + final int touchSlop = mTouchSlop; + boolean xPaged = xDiff > mPagingTouchSlop; + boolean xMoved = xDiff > touchSlop; + boolean yMoved = yDiff > touchSlop; + + if (xMoved || yMoved) { + if (mUsePagingTouchSlop ? xPaged : xMoved) { + // Scroll if the user moved far enough along the X axis + mTouchState = TOUCH_STATE_SCROLLING; + mLastMotionX = x; + mTouchX = mScrollX; + mSmoothingTime = System.nanoTime() / NANOTIME_DIV; + pageBeginMoving(); + } + // Either way, cancel any pending longpress + if (mAllowLongPress) { + mAllowLongPress = false; + // Try canceling the long press. It could also have been scheduled + // by a distant descendant, so use the mAllowLongPress flag to block + // everything + final View currentPage = getPageAt(mCurrentPage); + if (currentPage != null) { + currentPage.cancelLongPress(); + } + } + } + } + + protected boolean handlePagingClicks() { + return false; + } + + // This curve determines how the effect of scrolling over the limits of the page dimishes + // as the user pulls further and further from the bounds + private float overScrollInfluenceCurve(float f) { + f -= 1.0f; + return f * f * f + 1.0f; + } + + protected void overScroll(float amount) { + int screenSize = getMeasuredWidth(); + + float f = (amount / screenSize); + + if (f == 0) return; + f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f))); + + int overScrollAmount = (int) Math.round(OVERSCROLL_DAMP_FACTOR * f * screenSize); + if (amount < 0) { + mScrollX = overScrollAmount; + } else { + mScrollX = mMaxScrollX + overScrollAmount; + } + invalidate(); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + // Skip touch handling if there are no pages to swipe + if (getChildCount() <= 0) return super.onTouchEvent(ev); + + acquireVelocityTrackerAndAddMovement(ev); + + final int action = ev.getAction(); + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + // Remember where the motion event started + mDownMotionX = mLastMotionX = ev.getX(); + mActivePointerId = ev.getPointerId(0); + if (mTouchState == TOUCH_STATE_SCROLLING) { + pageBeginMoving(); + } + break; + + case MotionEvent.ACTION_MOVE: + if (mTouchState == TOUCH_STATE_SCROLLING) { + // Scroll to follow the motion event + final int pointerIndex = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(pointerIndex); + final int deltaX = (int) (mLastMotionX - x); + mLastMotionX = x; + + if (deltaX != 0) { + mTouchX += deltaX; + mSmoothingTime = System.nanoTime() / NANOTIME_DIV; + if (!mDeferScrollUpdate) { + scrollBy(deltaX, 0); + } else { + invalidate(); + } + } else { + awakenScrollBars(); + } + } else { + determineScrollingStart(ev); + } + break; + + case MotionEvent.ACTION_UP: + if (mTouchState == TOUCH_STATE_SCROLLING) { + final int activePointerId = mActivePointerId; + final int pointerIndex = ev.findPointerIndex(activePointerId); + final float x = ev.getX(pointerIndex); + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int velocityX = (int) velocityTracker.getXVelocity(activePointerId); + final int deltaX = (int) (x - mDownMotionX); + boolean isfling = Math.abs(deltaX) > MIN_LENGTH_FOR_FLING; + boolean isSignificantMove = Math.abs(deltaX) > MIN_LENGTH_FOR_MOVE; + + final int snapVelocity = mSnapVelocity; + if ((isSignificantMove && deltaX > 0 || + (isfling && velocityX > snapVelocity)) && mCurrentPage > 0) { + snapToPageWithVelocity(mCurrentPage - 1, velocityX); + } else if ((isSignificantMove && deltaX < 0 || + (isfling && velocityX < -snapVelocity)) && + mCurrentPage < getChildCount() - 1) { + snapToPageWithVelocity(mCurrentPage + 1, velocityX); + } else { + snapToDestination(); + } + } else if (mTouchState == TOUCH_STATE_PREV_PAGE && !handlePagingClicks()) { + // at this point we have not moved beyond the touch slop + // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so + // we can just page + int nextPage = Math.max(0, mCurrentPage - 1); + if (nextPage != mCurrentPage) { + snapToPage(nextPage); + } else { + snapToDestination(); + } + } else if (mTouchState == TOUCH_STATE_NEXT_PAGE && !handlePagingClicks()) { + // at this point we have not moved beyond the touch slop + // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so + // we can just page + int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1); + if (nextPage != mCurrentPage) { + snapToPage(nextPage); + } else { + snapToDestination(); + } + } + mTouchState = TOUCH_STATE_REST; + mActivePointerId = INVALID_POINTER; + releaseVelocityTracker(); + break; + + case MotionEvent.ACTION_CANCEL: + if (mTouchState == TOUCH_STATE_SCROLLING) { + snapToDestination(); + } + mTouchState = TOUCH_STATE_REST; + mActivePointerId = INVALID_POINTER; + releaseVelocityTracker(); + break; + + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + return true; + } + + private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + } + + private void releaseVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> + MotionEvent.ACTION_POINTER_INDEX_SHIFT; + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + // TODO: Make this decision more intelligent. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionX = mDownMotionX = ev.getX(newPointerIndex); + mLastMotionY = ev.getY(newPointerIndex); + mActivePointerId = ev.getPointerId(newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + @Override + public void requestChildFocus(View child, View focused) { + super.requestChildFocus(child, focused); + int page = indexOfChild(child); + if (page >= 0 && !isInTouchMode()) { + snapToPage(page); + } + } + + protected int getChildIndexForRelativeOffset(int relativeOffset) { + final int childCount = getChildCount(); + int left; + int right; + for (int i = 0; i < childCount; ++i) { + left = getRelativeChildOffset(i); + right = (left + getChildAt(i).getMeasuredWidth()); + if (left <= relativeOffset && relativeOffset <= right) { + return i; + } + } + return -1; + } + + protected int getRelativeChildOffset(int index) { + return (getMeasuredWidth() - getChildAt(index).getMeasuredWidth()) / 2; + } + + protected int getChildOffset(int index) { + if (getChildCount() == 0) + return 0; + + int offset = getRelativeChildOffset(0); + for (int i = 0; i < index; ++i) { + offset += getChildAt(i).getMeasuredWidth() + mPageSpacing; + } + return offset; + } + + int getPageNearestToCenterOfScreen() { + int minDistanceFromScreenCenter = getMeasuredWidth(); + int minDistanceFromScreenCenterIndex = -1; + int screenCenter = mScrollX + (getMeasuredWidth() / 2); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; ++i) { + View layout = (View) getChildAt(i); + int childWidth = layout.getMeasuredWidth(); + int halfChildWidth = (childWidth / 2); + int childCenter = getChildOffset(i) + halfChildWidth; + int distanceFromScreenCenter = Math.abs(childCenter - screenCenter); + if (distanceFromScreenCenter < minDistanceFromScreenCenter) { + minDistanceFromScreenCenter = distanceFromScreenCenter; + minDistanceFromScreenCenterIndex = i; + } + } + return minDistanceFromScreenCenterIndex; + } + + protected void snapToDestination() { + snapToPage(getPageNearestToCenterOfScreen(), PAGE_SNAP_ANIMATION_DURATION); + } + + private static class ScrollInterpolator implements Interpolator { + public ScrollInterpolator() { + } + + public float getInterpolation(float t) { + t -= 1.0f; + return t*t*t*t*t + 1; + } + } + + // We want the duration of the page snap animation to be influenced by the distance that + // the screen has to travel, however, we don't want this duration to be effected in a + // purely linear fashion. Instead, we use this method to moderate the effect that the distance + // of travel has on the overall snap duration. + float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * Math.PI / 2.0f; + return (float) Math.sin(f); + } + + protected void snapToPageWithVelocity(int whichPage, int velocity) { + whichPage = Math.max(0, Math.min(whichPage, getChildCount() - 1)); + int halfScreenSize = getMeasuredWidth() / 2; + + final int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage); + int delta = newX - mUnboundedScrollX; + int duration = 0; + + if (Math.abs(velocity) < MIN_FLING_VELOCITY) { + // 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); + return; + } + + // Here we compute a "distance" that will be used in the computation of the overall + // snap duration. This is a function of the actual distance that needs to be traveled; + // we keep this value close to half screen size in order to reduce the variance in snap + // duration as a function of the distance the page needs to travel. + float distanceRatio = 1.0f * Math.abs(delta) / 2 * halfScreenSize; + float distance = halfScreenSize + halfScreenSize * + distanceInfluenceForSnapDuration(distanceRatio); + + velocity = Math.abs(velocity); + velocity = Math.max(MINIMUM_SNAP_VELOCITY, 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 + // interpolator at zero, ie. 5. We use 6 to make it a little slower. + duration = 6 * Math.round(1000 * Math.abs(distance / velocity)); + + snapToPage(whichPage, delta, duration); + } + + protected void snapToPage(int whichPage) { + snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); + } + + protected void snapToPage(int whichPage, int duration) { + whichPage = Math.max(0, Math.min(whichPage, getPageCount() - 1)); + + int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage); + final int sX = mUnboundedScrollX; + final int delta = newX - sX; + snapToPage(whichPage, delta, duration); + } + + protected void snapToPage(int whichPage, int delta, int duration) { + mNextPage = whichPage; + + View focusedChild = getFocusedChild(); + if (focusedChild != null && whichPage != mCurrentPage && + focusedChild == getChildAt(mCurrentPage)) { + focusedChild.clearFocus(); + } + + pageBeginMoving(); + awakenScrollBars(duration); + if (duration == 0) { + duration = Math.abs(delta); + } + + if (!mScroller.isFinished()) mScroller.abortAnimation(); + mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration); + + // only load some associated pages + loadAssociatedPages(mNextPage); + notifyPageSwitchListener(); + invalidate(); + } + + @Override + protected Parcelable onSaveInstanceState() { + final SavedState state = new SavedState(super.onSaveInstanceState()); + state.currentPage = mCurrentPage; + return state; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState savedState = (SavedState) state; + super.onRestoreInstanceState(savedState.getSuperState()); + if (savedState.currentPage != -1) { + mCurrentPage = savedState.currentPage; + } + } + + public void scrollLeft() { + if (mScroller.isFinished()) { + if (mCurrentPage > 0) snapToPage(mCurrentPage - 1); + } else { + if (mNextPage > 0) snapToPage(mNextPage - 1); + } + } + + public void scrollRight() { + if (mScroller.isFinished()) { + if (mCurrentPage < getChildCount() -1) snapToPage(mCurrentPage + 1); + } else { + if (mNextPage < getChildCount() -1) snapToPage(mNextPage + 1); + } + } + + public int getPageForView(View v) { + int result = -1; + if (v != null) { + ViewParent vp = v.getParent(); + int count = getChildCount(); + for (int i = 0; i < count; i++) { + if (vp == getChildAt(i)) { + return i; + } + } + } + return result; + } + + /** + * @return True is long presses are still allowed for the current touch + */ + public boolean allowLongPress() { + return mAllowLongPress; + } + + /** + * Set true to allow long-press events to be triggered, usually checked by + * {@link Launcher} to accept or block dpad-initiated long-presses. + */ + public void setAllowLongPress(boolean allowLongPress) { + mAllowLongPress = allowLongPress; + } + + public static class SavedState extends BaseSavedState { + int currentPage = -1; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + currentPage = in.readInt(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(currentPage); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + public void loadAssociatedPages(int page) { + if (mContentIsRefreshable) { + final int count = getChildCount(); + if (page < count) { + int lowerPageBound = getAssociatedLowerPageBound(page); + int upperPageBound = getAssociatedUpperPageBound(page); + for (int i = 0; i < count; ++i) { + final ViewGroup layout = (ViewGroup) getChildAt(i); + final int childCount = layout.getChildCount(); + if (lowerPageBound <= i && i <= upperPageBound) { + if (mDirtyPageContent.get(i)) { + syncPageItems(i); + mDirtyPageContent.set(i, false); + } + } else { + if (childCount > 0) { + layout.removeAllViews(); + } + mDirtyPageContent.set(i, true); + } + } + } + } + } + + protected int getAssociatedLowerPageBound(int page) { + return Math.max(0, page - 1); + } + protected int getAssociatedUpperPageBound(int page) { + final int count = getChildCount(); + 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) { + final ViewGroup layout = (ViewGroup) getChildAt(i); + final int grandChildCount = layout.getChildCount(); + for (int j = 0; j < grandChildCount; ++j) { + final View v = layout.getChildAt(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_SINGLE) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; ++i) { + final ViewGroup layout = (ViewGroup) getChildAt(i); + final int grandChildCount = layout.getChildCount(); + for (int j = 0; j < grandChildCount; ++j) { + final View v = layout.getChildAt(j); + if (v instanceof Checkable && ((Checkable) v).isChecked()) { + return (Checkable) v; + } + } + } + } + return null; + } + + public Object getChosenItem() { + View checkedView = (View) getSingleCheckedGrandchild(); + if (checkedView != null) { + return checkedView.getTag(); + } + 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 + * guaranteed that syncPageItems() will be called for a particular page before it is shown, + * and therefore, individual page items do not need to be updated in this method. + */ + public abstract void syncPages(); + + /** + * This method is called to synchronize the items that are on a particular page. If views on + * the page can be reused, then they should be updated within this method. + */ + public abstract void syncPageItems(int page); + + public void invalidatePageData() { + if (mContentIsRefreshable) { + // Update all the pages + syncPages(); + + // Mark each of the pages as dirty + final int count = getChildCount(); + mDirtyPageContent.clear(); + for (int i = 0; i < count; ++i) { + mDirtyPageContent.add(true); + } + + // Load any pages that are necessary for the current window of views + loadAssociatedPages(mCurrentPage); + mDirtyPageAlpha = true; + updateAdjacentPagesAlpha(); + requestLayout(); + } + } +} diff --git a/src/com/android/launcher2/PagedViewCellLayout.java b/src/com/android/launcher2/PagedViewCellLayout.java new file mode 100644 index 0000000..b779a97 --- /dev/null +++ b/src/com/android/launcher2/PagedViewCellLayout.java @@ -0,0 +1,460 @@ +/* + * 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.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewDebug; +import android.view.ViewGroup; + +/** + * An abstraction of the original CellLayout which supports laying out items + * which span multiple cells into a grid-like layout. Also supports dimming + * to give a preview of its contents. + */ +public class PagedViewCellLayout extends ViewGroup { + static final String TAG = "PagedViewCellLayout"; + + private float mHolographicAlpha; + + private boolean mCenterContent; + + private int mCellCountX; + private int mCellCountY; + private int mCellWidth; + private int mCellHeight; + private int mWidthGap; + private int mHeightGap; + private static int sDefaultCellDimensions = 96; + + public PagedViewCellLayout(Context context) { + this(context, null); + } + + public PagedViewCellLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PagedViewCellLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + setAlwaysDrawnWithCacheEnabled(false); + + // setup default cell parameters + mCellWidth = mCellHeight = sDefaultCellDimensions; + mCellCountX = LauncherModel.getCellCountX(); + mCellCountY = LauncherModel.getCellCountY(); + mHolographicAlpha = 0.0f; + mWidthGap = mHeightGap = -1; + } + + @Override + protected boolean onSetAlpha(int alpha) { + return true; + } + + @Override + public void setAlpha(float alpha) { + mHolographicAlpha = 1.0f - alpha; + setChildrenAlpha(alpha); + super.setAlpha(alpha); + } + + @Override + public void cancelLongPress() { + super.cancelLongPress(); + + // Cancel long press for all children + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + child.cancelLongPress(); + } + } + + public boolean addViewToCellLayout(View child, int index, int childId, + PagedViewCellLayout.LayoutParams params) { + final PagedViewCellLayout.LayoutParams lp = params; + + // Generate an id for each view, this assumes we have at most 256x256 cells + // per workspace screen + if (lp.cellX >= 0 && lp.cellX <= (mCellCountX - 1) && + lp.cellY >= 0 && (lp.cellY <= mCellCountY - 1)) { + // If the horizontal or vertical span is set to -1, it is taken to + // mean that it spans the extent of the CellLayout + if (lp.cellHSpan < 0) lp.cellHSpan = mCellCountX; + if (lp.cellVSpan < 0) lp.cellVSpan = mCellCountY; + + child.setId(childId); + + // We might be in the middle or end of shrinking/fading to a dimmed view + // Make sure this view's alpha is set the same as all the rest of the views + child.setAlpha(1.0f - mHolographicAlpha); + + addView(child, index, lp); + return true; + } + return false; + } + + @Override + public void requestChildFocus(View child, View focused) { + super.requestChildFocus(child, focused); + if (child != null) { + Rect r = new Rect(); + child.getDrawingRect(r); + requestRectangleOnScreen(r); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // TODO: currently ignoring padding + + int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); + + int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) { + throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions"); + } + + final int cellWidth = mCellWidth; + final int cellHeight = mCellHeight; + + int numWidthGaps = mCellCountX - 1; + int numHeightGaps = mCellCountY - 1; + + int vSpaceLeft = heightSpecSize - mPaddingTop + - mPaddingBottom - (cellHeight * mCellCountY); + int heightGap = vSpaceLeft / numHeightGaps; + + int hSpaceLeft = widthSpecSize - mPaddingLeft + - mPaddingRight - (cellWidth * mCellCountX); + int widthGap = hSpaceLeft / numWidthGaps; + + // center it around the min gaps + int minGap = Math.min(widthGap, heightGap); + int paddingLeft = mPaddingLeft; + int paddingTop = mPaddingTop; + /* + if (minGap < heightGap) { + // vertical space has shrunken, so change padding accordingly + paddingTop += ((heightGap - minGap) * (mCellCountY - 1)) / 2; + } else if (minGap < widthGap) { + // horizontal space has shrunken, so change padding accordingly + paddingLeft += ((widthGap - minGap) * (mCellCountX - 1)) / 2; + } + */ + if (mWidthGap > -1 && mHeightGap > -1) { + widthGap = mWidthGap; + heightGap = mHeightGap; + } else { + widthGap = heightGap = minGap; + } + + int newWidth = mPaddingLeft + mPaddingRight + (mCellCountX * cellWidth) + + ((mCellCountX - 1) * widthGap); + int newHeight = mPaddingTop + mPaddingBottom + (mCellCountY * cellHeight) + + ((mCellCountY - 1) * heightGap); + + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + PagedViewCellLayout.LayoutParams lp = + (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); + lp.setup(cellWidth, cellHeight, widthGap, heightGap, + paddingLeft, paddingTop); + + int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, + MeasureSpec.EXACTLY); + int childheightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, + MeasureSpec.EXACTLY); + + child.measure(childWidthMeasureSpec, childheightMeasureSpec); + } + + setMeasuredDimension(newWidth, newHeight); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int count = getChildCount(); + + int offsetX = 0; + if (mCenterContent) { + // determine the max width of all the rows and center accordingly + int maxRowWidth = 0; + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child.getVisibility() != GONE) { + PagedViewCellLayout.LayoutParams lp = + (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); + maxRowWidth = Math.max(maxRowWidth, lp.x + lp.width); + } + } + offsetX = (getMeasuredWidth() / 2) - (maxRowWidth / 2); + } + + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child.getVisibility() != GONE) { + PagedViewCellLayout.LayoutParams lp = + (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); + + int childLeft = offsetX + lp.x; + int childTop = lp.y; + child.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height); + } + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + return super.onTouchEvent(event) || true; + } + + public void enableCenteredContent(boolean enabled) { + mCenterContent = enabled; + } + + @Override + protected void setChildrenDrawingCacheEnabled(boolean enabled) { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View view = getChildAt(i); + view.setDrawingCacheEnabled(enabled); + // Update the drawing caches + if (!view.isHardwareAccelerated()) { + view.buildDrawingCache(true); + } + } + } + + public void setCellCount(int xCount, int yCount) { + mCellCountX = xCount; + mCellCountY = yCount; + requestLayout(); + } + + public void setGap(int widthGap, int heightGap) { + mWidthGap = widthGap; + mHeightGap = heightGap; + } + + public void setCellDimensions(int width, int height) { + mCellWidth = width; + mCellHeight = height; + requestLayout(); + } + + public int getDefaultCellDimensions() { + return sDefaultCellDimensions; + } + + private void setChildrenAlpha(float alpha) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + getChildAt(i).setAlpha(alpha); + } + } + + public int[] getCellCountForDimensions(int width, int height) { + // Always assume we're working with the smallest span to make sure we + // reserve enough space in both orientations + int smallerSize = Math.min(mCellWidth, mCellHeight); + + // Always round up to next largest cell + int spanX = (width + smallerSize) / smallerSize; + int spanY = (height + smallerSize) / smallerSize; + + return new int[] { spanX, spanY }; + } + + /** + * Start dragging the specified child + * + * @param child The child that is being dragged + */ + void onDragChild(View child) { + PagedViewCellLayout.LayoutParams lp = (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); + lp.isDragging = true; + } + + /** + * Estimates the number of cells that the specified width would take up. + */ + public int estimateCellHSpan(int width) { + // TODO: we need to take widthGap into effect + return (width + mCellWidth) / mCellWidth; + } + + /** + * Estimates the number of cells that the specified height would take up. + */ + public int estimateCellVSpan(int height) { + // TODO: we need to take heightGap into effect + return (height + mCellHeight) / mCellHeight; + } + + /** + * Estimates the width that the number of vSpan cells will take up. + */ + public int estimateCellWidth(int hSpan) { + // TODO: we need to take widthGap into effect + return hSpan * mCellWidth; + } + + /** + * Estimates the height that the number of vSpan cells will take up. + */ + public int estimateCellHeight(int vSpan) { + // TODO: we need to take heightGap into effect + return vSpan * mCellHeight; + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new PagedViewCellLayout.LayoutParams(getContext(), attrs); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof PagedViewCellLayout.LayoutParams; + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new PagedViewCellLayout.LayoutParams(p); + } + + public static class LayoutParams extends ViewGroup.MarginLayoutParams { + /** + * Horizontal location of the item in the grid. + */ + @ViewDebug.ExportedProperty + public int cellX; + + /** + * Vertical location of the item in the grid. + */ + @ViewDebug.ExportedProperty + public int cellY; + + /** + * Number of cells spanned horizontally by the item. + */ + @ViewDebug.ExportedProperty + public int cellHSpan; + + /** + * Number of cells spanned vertically by the item. + */ + @ViewDebug.ExportedProperty + public int cellVSpan; + + /** + * Is this item currently being dragged + */ + public boolean isDragging; + + // a data object that you can bind to this layout params + private Object mTag; + + // X coordinate of the view in the layout. + @ViewDebug.ExportedProperty + int x; + // Y coordinate of the view in the layout. + @ViewDebug.ExportedProperty + int y; + + public LayoutParams() { + super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + cellHSpan = 1; + cellVSpan = 1; + } + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + cellHSpan = 1; + cellVSpan = 1; + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + cellHSpan = 1; + cellVSpan = 1; + } + + public LayoutParams(LayoutParams source) { + super(source); + this.cellX = source.cellX; + this.cellY = source.cellY; + this.cellHSpan = source.cellHSpan; + this.cellVSpan = source.cellVSpan; + } + + public LayoutParams(int cellX, int cellY, int cellHSpan, int cellVSpan) { + super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + this.cellX = cellX; + this.cellY = cellY; + this.cellHSpan = cellHSpan; + this.cellVSpan = cellVSpan; + } + + public void setup(int cellWidth, int cellHeight, int widthGap, int heightGap, + int hStartPadding, int vStartPadding) { + + final int myCellHSpan = cellHSpan; + final int myCellVSpan = cellVSpan; + final int myCellX = cellX; + final int myCellY = cellY; + + width = myCellHSpan * cellWidth + ((myCellHSpan - 1) * widthGap) - + leftMargin - rightMargin; + height = myCellVSpan * cellHeight + ((myCellVSpan - 1) * heightGap) - + topMargin - bottomMargin; + + x = hStartPadding + myCellX * (cellWidth + widthGap) + leftMargin; + y = vStartPadding + myCellY * (cellHeight + heightGap) + topMargin; + } + + public Object getTag() { + return mTag; + } + + public void setTag(Object tag) { + mTag = tag; + } + + public String toString() { + return "(" + this.cellX + ", " + this.cellY + ", " + + this.cellHSpan + ", " + this.cellVSpan + ")"; + } + } +} diff --git a/src/com/android/launcher2/PagedViewExtendedLayout.java b/src/com/android/launcher2/PagedViewExtendedLayout.java new file mode 100644 index 0000000..52df9f1 --- /dev/null +++ b/src/com/android/launcher2/PagedViewExtendedLayout.java @@ -0,0 +1,71 @@ +/* + * 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.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.LinearLayout; + +/** + * The linear layout used strictly for the widget/wallpaper tab of the customization tray + */ +public class PagedViewExtendedLayout extends LinearLayout { + static final String TAG = "PagedViewWidgetLayout"; + + public PagedViewExtendedLayout(Context context) { + this(context, null); + } + + public PagedViewExtendedLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PagedViewExtendedLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @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; + } + + @Override + protected boolean onSetAlpha(int alpha) { + return true; + } + + @Override + public void setAlpha(float alpha) { + setChildrenAlpha(alpha); + super.setAlpha(alpha); + } + + private void setChildrenAlpha(float alpha) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + getChildAt(i).setAlpha(alpha); + } + } +} diff --git a/src/com/android/launcher2/PagedViewIcon.java b/src/com/android/launcher2/PagedViewIcon.java new file mode 100644 index 0000000..b9b9b37 --- /dev/null +++ b/src/com/android/launcher2/PagedViewIcon.java @@ -0,0 +1,249 @@ +/* + * 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.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.util.AttributeSet; +import android.widget.Checkable; +import android.widget.TextView; + +import com.android.launcher.R; +import com.android.launcher2.PagedView.PagedViewIconCache; + + + +/** + * 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 static HolographicOutlineHelper sHolographicOutlineHelper; + private Bitmap mCheckedOutline; + private Bitmap mHolographicOutline; + private Bitmap mIcon; + + private Object mIconCacheKey; + private PagedViewIconCache mIconCache; + + private int mAlpha = -1; + private int mHolographicAlpha; + + private boolean mIsChecked; + + // Highlight colors + private int mHoloBlurColor; + private int mHoloOutlineColor; + private int mCheckedBlurColor; + private int mCheckedOutlineColor; + + private static final HandlerThread sWorkerThread = new HandlerThread("pagedviewicon-helper"); + static { + sWorkerThread.start(); + } + + private static final int MESSAGE_CREATE_HOLOGRAPHIC_OUTLINE = 1; + + private static final Handler sWorker = new Handler(sWorkerThread.getLooper()) { + private DeferredHandler mHandler = new DeferredHandler(); + private Paint mPaint = new Paint(); + public void handleMessage(Message msg) { + final PagedViewIcon icon = (PagedViewIcon) msg.obj; + + final Bitmap holographicOutline = Bitmap.createBitmap( + icon.mIcon.getWidth(), icon.mIcon.getHeight(), Bitmap.Config.ARGB_8888); + Canvas holographicOutlineCanvas = new Canvas(holographicOutline); + holographicOutlineCanvas.drawBitmap(icon.mIcon, 0, 0, mPaint); + + sHolographicOutlineHelper.applyExpensiveOutlineWithBlur(holographicOutline, + holographicOutlineCanvas, icon.mHoloBlurColor, icon.mHoloOutlineColor); + + mHandler.post(new Runnable() { + public void run() { + icon.mHolographicOutline = holographicOutline; + icon.mIconCache.addOutline(icon.mIconCacheKey, holographicOutline); + icon.invalidate(); + } + }); + } + }; + + public PagedViewIcon(Context context) { + this(context, null); + } + + public PagedViewIcon(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PagedViewIcon(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PagedViewIcon, defStyle, 0); + mHoloBlurColor = a.getColor(R.styleable.PagedViewIcon_blurColor, 0); + mHoloOutlineColor = a.getColor(R.styleable.PagedViewIcon_outlineColor, 0); + mCheckedBlurColor = a.getColor(R.styleable.PagedViewIcon_checkedBlurColor, 0); + mCheckedOutlineColor = a.getColor(R.styleable.PagedViewIcon_checkedOutlineColor, 0); + + a.recycle(); + + if (sHolographicOutlineHelper == null) { + sHolographicOutlineHelper = new HolographicOutlineHelper(); + } + + setFocusable(true); + setBackgroundDrawable(null); + } + + private void queueHolographicOutlineCreation() { + // Generate the outline in the background + if (mHolographicOutline == null) { + Message m = sWorker.obtainMessage(MESSAGE_CREATE_HOLOGRAPHIC_OUTLINE); + m.obj = this; + sWorker.sendMessage(m); + } + } + + public void applyFromApplicationInfo(ApplicationInfo info, PagedViewIconCache cache, + boolean scaleUp) { + mIconCache = cache; + mIconCacheKey = info; + mHolographicOutline = mIconCache.getOutline(mIconCacheKey); + + mIcon = info.iconBitmap; + setCompoundDrawablesWithIntrinsicBounds(null, new FastBitmapDrawable(mIcon), null, null); + setText(info.title); + setTag(info); + + queueHolographicOutlineCreation(); + } + + public void applyFromResolveInfo(ResolveInfo info, PackageManager packageManager, + PagedViewIconCache cache, IconCache modelIconCache) { + mIconCache = cache; + mIconCacheKey = info; + mHolographicOutline = mIconCache.getOutline(mIconCacheKey); + + mIcon = Utilities.createIconBitmap( + modelIconCache.getFullResIcon(info, packageManager), mContext); + setCompoundDrawablesWithIntrinsicBounds(null, new FastBitmapDrawable(mIcon), null, null); + setText(info.loadLabel(packageManager)); + setTag(info); + + queueHolographicOutlineCreation(); + } + + @Override + public void setAlpha(float alpha) { + final float viewAlpha = sHolographicOutlineHelper.viewAlphaInterpolator(alpha); + final float holographicAlpha = sHolographicOutlineHelper.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) { + if (mHolographicOutline != null && mHolographicAlpha > 0) { + mPaint.setAlpha(mHolographicAlpha); + overlay = mHolographicOutline; + } + } else { + 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); + } + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + sWorker.removeMessages(MESSAGE_CREATE_HOLOGRAPHIC_OUTLINE, this); + } + + @Override + public boolean isChecked() { + return mIsChecked; + } + + @Override + public void setChecked(boolean checked) { + if (mIsChecked != checked) { + mIsChecked = checked; + + if (mIsChecked) { + mCheckedOutline = Bitmap.createBitmap(mIcon.getWidth(), mIcon.getHeight(), + Bitmap.Config.ARGB_8888); + Canvas checkedOutlineCanvas = new Canvas(mCheckedOutline); + mPaint.setAlpha(255); + checkedOutlineCanvas.drawBitmap(mIcon, 0, 0, mPaint); + + sHolographicOutlineHelper.applyExpensiveOutlineWithBlur(mCheckedOutline, + checkedOutlineCanvas, mCheckedBlurColor, mCheckedOutlineColor); + } else { + invalidateCheckedImage(); + } + + invalidate(); + } + } + + @Override + public void toggle() { + setChecked(!mIsChecked); + } +} diff --git a/src/com/android/launcher2/PendingAddItemInfo.java b/src/com/android/launcher2/PendingAddItemInfo.java new file mode 100644 index 0000000..7b564e0 --- /dev/null +++ b/src/com/android/launcher2/PendingAddItemInfo.java @@ -0,0 +1,52 @@ +/* + * 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.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.os.Parcelable; + +/** + * We pass this object with a drag from the customization tray + */ +class PendingAddItemInfo extends ItemInfo { + /** + * The component that will be created. + */ + ComponentName componentName; +} + +class PendingAddWidgetInfo extends PendingAddItemInfo { + int minWidth; + int minHeight; + + // Any configuration data that we want to pass to a configuration activity when + // starting up a widget + String mimeType; + Parcelable configurationData; + + public PendingAddWidgetInfo(AppWidgetProviderInfo i, String dataMimeType, Parcelable data) { + itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; + componentName = i.provider; + minWidth = i.minWidth; + minHeight = i.minHeight; + if (dataMimeType != null && data != null) { + mimeType = dataMimeType; + configurationData = data; + } + } +}
\ No newline at end of file diff --git a/src/com/android/launcher2/ShortcutInfo.java b/src/com/android/launcher2/ShortcutInfo.java index 2c5aec4..72f2d51 100644 --- a/src/com/android/launcher2/ShortcutInfo.java +++ b/src/com/android/launcher2/ShortcutInfo.java @@ -16,16 +16,14 @@ package com.android.launcher2; +import java.util.ArrayList; + import android.content.ComponentName; import android.content.ContentValues; -import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; import android.util.Log; -import java.util.ArrayList; - /** * Represents a launchable icon on the workspaces and in folders. */ diff --git a/src/com/android/launcher2/ShortcutsAdapter.java b/src/com/android/launcher2/ShortcutsAdapter.java index 19c3af0..93c500a 100644 --- a/src/com/android/launcher2/ShortcutsAdapter.java +++ b/src/com/android/launcher2/ShortcutsAdapter.java @@ -16,16 +16,15 @@ package com.android.launcher2; +import java.util.ArrayList; + import android.content.Context; -import android.content.pm.PackageManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; -import java.util.ArrayList; - import com.android.launcher.R; /** diff --git a/src/com/android/launcher2/SmoothPagedView.java b/src/com/android/launcher2/SmoothPagedView.java new file mode 100644 index 0000000..8b0a835 --- /dev/null +++ b/src/com/android/launcher2/SmoothPagedView.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher2; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.animation.Interpolator; +import android.widget.Scroller; + +public abstract class SmoothPagedView extends PagedView { + private static final float SMOOTHING_SPEED = 0.75f; + private static final float SMOOTHING_CONSTANT = (float) (0.016 / Math.log(SMOOTHING_SPEED)); + + private float mBaseLineFlingVelocity; + private float mFlingVelocityInfluence; + + static final int DEFAULT_MODE = 0; + static final int X_LARGE_MODE = 1; + + int mScrollMode; + + private Interpolator mScrollInterpolator; + + private static class WorkspaceOvershootInterpolator implements Interpolator { + private static final float DEFAULT_TENSION = 1.3f; + private float mTension; + + public WorkspaceOvershootInterpolator() { + mTension = DEFAULT_TENSION; + } + + public void setDistance(int distance) { + mTension = distance > 0 ? DEFAULT_TENSION / distance : DEFAULT_TENSION; + } + + public void disableSettle() { + mTension = 0.f; + } + + public float getInterpolation(float t) { + // _o(t) = t * t * ((tension + 1) * t + tension) + // o(t) = _o(t - 1) + 1 + t -= 1.0f; + return t * t * ((mTension + 1) * t + mTension) + 1.0f; + } + } + + /** + * Used to inflate the Workspace from XML. + * + * @param context The application's context. + * @param attrs The attributes set containing the Workspace's customization values. + */ + public SmoothPagedView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + /** + * Used to inflate the Workspace from XML. + * + * @param context The application's context. + * @param attrs The attributes set containing the Workspace's customization values. + * @param defStyle Unused. + */ + public SmoothPagedView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mUsePagingTouchSlop = false; + + // This means that we'll take care of updating the scroll parameter ourselves (we do it + // in computeScroll), we only do this in the OVERSHOOT_MODE, ie. on phones + mDeferScrollUpdate = mScrollMode != X_LARGE_MODE; + } + + protected int getScrollMode() { + return DEFAULT_MODE; + } + + /** + * Initializes various states for this workspace. + */ + @Override + protected void init() { + super.init(); + + mScrollMode = getScrollMode(); + if (mScrollMode == DEFAULT_MODE) { + mBaseLineFlingVelocity = 2500.0f; + mFlingVelocityInfluence = 0.4f; + mScrollInterpolator = new WorkspaceOvershootInterpolator(); + mScroller = new Scroller(getContext(), mScrollInterpolator); + } + } + + @Override + protected void snapToDestination() { + if (mScrollMode == X_LARGE_MODE) { + super.snapToDestination(); + } else { + snapToPageWithVelocity(getPageNearestToCenterOfScreen(), 0); + } + } + + @Override + protected void snapToPageWithVelocity(int whichPage, int velocity) { + if (mScrollMode == X_LARGE_MODE) { + super.snapToPageWithVelocity(whichPage, velocity); + } else { + snapToPageWithVelocity(whichPage, 0, true); + } + } + + private void snapToPageWithVelocity(int whichPage, int velocity, boolean settle) { + // if (!mScroller.isFinished()) return; + + whichPage = Math.max(0, Math.min(whichPage, getChildCount() - 1)); + + final int screenDelta = Math.max(1, Math.abs(whichPage - mCurrentPage)); + final int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage); + final int delta = newX - mUnboundedScrollX; + int duration = (screenDelta + 1) * 100; + + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + if (settle) { + ((WorkspaceOvershootInterpolator) mScrollInterpolator).setDistance(screenDelta); + } else { + ((WorkspaceOvershootInterpolator) mScrollInterpolator).disableSettle(); + } + + velocity = Math.abs(velocity); + if (velocity > 0) { + duration += (duration / (velocity / mBaseLineFlingVelocity)) * mFlingVelocityInfluence; + } else { + duration += 100; + } + + snapToPage(whichPage, delta, duration); + } + + @Override + protected void snapToPage(int whichPage) { + if (mScrollMode == X_LARGE_MODE) { + super.snapToPage(whichPage); + } else { + snapToPageWithVelocity(whichPage, 0, false); + } + } + + @Override + public void computeScroll() { + if (mScrollMode == X_LARGE_MODE) { + super.computeScroll(); + } else { + boolean scrollComputed = computeScrollHelper(); + + if (!scrollComputed && mTouchState == TOUCH_STATE_SCROLLING) { + final float now = System.nanoTime() / NANOTIME_DIV; + final float e = (float) Math.exp((now - mSmoothingTime) / SMOOTHING_CONSTANT); + + final float dx = mTouchX - mUnboundedScrollX; + scrollTo(Math.round(mUnboundedScrollX + dx * e), mScrollY); + mSmoothingTime = now; + + // Keep generating points as long as we're more than 1px away from the target + if (dx > 1.f || dx < -1.f) { + invalidate(); + } + } + } + } +} diff --git a/src/com/android/launcher2/SymmetricalLinearTween.java b/src/com/android/launcher2/SymmetricalLinearTween.java index 2e0ed8f..da02242 100644 --- a/src/com/android/launcher2/SymmetricalLinearTween.java +++ b/src/com/android/launcher2/SymmetricalLinearTween.java @@ -17,9 +17,7 @@ package com.android.launcher2; import android.os.Handler; -import android.os.Message; import android.os.SystemClock; -import android.util.Log; /** * Provides an animation between 0.0f and 1.0f over a given duration. diff --git a/src/com/android/launcher2/UserFolder.java b/src/com/android/launcher2/UserFolder.java index d49c27a..d6799f7 100644 --- a/src/com/android/launcher2/UserFolder.java +++ b/src/com/android/launcher2/UserFolder.java @@ -3,10 +3,8 @@ package com.android.launcher2; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; -import android.widget.ArrayAdapter; import com.android.launcher.R; @@ -40,11 +38,6 @@ public class UserFolder extends Folder implements DropTarget { itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) && item.container != mInfo.id; } - - public Rect estimateDropLocation(DragSource source, int x, int y, int xOffset, int yOffset, - DragView dragView, Object dragInfo, Rect recycle) { - return null; - } public void onDrop(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo) { @@ -79,6 +72,10 @@ public class UserFolder extends Folder implements DropTarget { } } + public boolean isDropEnabled() { + return true; + } + void bind(FolderInfo info) { super.bind(info); setContentAdapter(new ShortcutsAdapter(mContext, ((UserFolderInfo) info).contents)); @@ -91,4 +88,10 @@ public class UserFolder extends Folder implements DropTarget { super.onOpen(); requestFocus(); } + + @Override + public DropTarget getDropTargetDelegate(DragSource source, int x, int y, int xOffset, int yOffset, + DragView dragView, Object dragInfo) { + return null; + } } diff --git a/src/com/android/launcher2/Utilities.java b/src/com/android/launcher2/Utilities.java index 757e48e..03a2a52 100644 --- a/src/com/android/launcher2/Utilities.java +++ b/src/com/android/launcher2/Utilities.java @@ -16,9 +16,8 @@ package com.android.launcher2; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.PaintDrawable; +import android.content.Context; +import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BlurMaskFilter; import android.graphics.Canvas; @@ -26,19 +25,19 @@ import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Paint; import android.graphics.PaintFlagsDrawFilter; -import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.TableMaskFilter; import android.graphics.Typeface; -import android.text.Layout.Alignment; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.PaintDrawable; import android.text.StaticLayout; import android.text.TextPaint; +import android.text.Layout.Alignment; import android.util.DisplayMetrics; import android.util.Log; -import android.content.res.Resources; -import android.content.Context; import com.android.launcher.R; @@ -238,7 +237,7 @@ final class Utilities { final DisplayMetrics metrics = resources.getDisplayMetrics(); final float density = metrics.density; - sIconWidth = sIconHeight = (int) resources.getDimension(android.R.dimen.app_icon_size); + sIconWidth = sIconHeight = (int) resources.getDimension(R.dimen.app_icon_size); sIconTextureWidth = sIconTextureHeight = sIconWidth + 2; sBlurPaint.setMaskFilter(new BlurMaskFilter(5 * density, BlurMaskFilter.Blur.NORMAL)); diff --git a/src/com/android/launcher2/WallpaperChooser.java b/src/com/android/launcher2/WallpaperChooser.java index bf8ba2e..0a8d9f4 100644 --- a/src/com/android/launcher2/WallpaperChooser.java +++ b/src/com/android/launcher2/WallpaperChooser.java @@ -17,7 +17,13 @@ package com.android.launcher2; import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.FragmentTransaction; import android.app.WallpaperManager; +import android.content.Context; +import android.content.DialogInterface; import android.content.res.Resources; import android.graphics.BitmapFactory; import android.graphics.Bitmap; @@ -28,27 +34,26 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.Window; import android.view.View.OnClickListener; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.Gallery; +import android.widget.GridView; import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.SpinnerAdapter; import java.io.IOException; import java.util.ArrayList; import com.android.launcher.R; -public class WallpaperChooser extends Activity implements AdapterView.OnItemSelectedListener, - OnClickListener { +public class WallpaperChooser extends Activity { private static final String TAG = "Launcher.WallpaperChooser"; - private Gallery mGallery; - private ImageView mImageView; - private boolean mIsWallpaperSet; - - private Bitmap mBitmap; + private ViewGroup mWallpaperChooserBase; + private ImageView mImageView = null; + private Bitmap mBitmap = null; private ArrayList<Integer> mThumbs; private ArrayList<Integer> mImages; @@ -57,20 +62,35 @@ public class WallpaperChooser extends Activity implements AdapterView.OnItemSele @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); - requestWindowFeature(Window.FEATURE_NO_TITLE); + /* We need some container to attach to in order for the fragment to be + * considered embedded, so inflate an empty FrameLayout and use that + * as the parent view + */ + setContentView(R.layout.wallpaper_chooser_base); + mWallpaperChooserBase = (ViewGroup) findViewById(R.id.wallpaper_chooser_base); findWallpapers(); - setContentView(R.layout.wallpaper_chooser); - - mGallery = (Gallery) findViewById(R.id.gallery); - mGallery.setAdapter(new ImageAdapter(this)); - mGallery.setOnItemSelectedListener(this); - mGallery.setCallbackDuringFling(false); + DialogFragment newFragment = new WallpaperDialogFragment(this); + if (LauncherApplication.isScreenXLarge()) { + // Display a dialog instead of embedding the view in the activity + newFragment.show(getFragmentManager(), "dialog"); + } else { + // Embed the fragment in the base view + FragmentTransaction ft = getFragmentManager().openTransaction(); + ft.add(R.id.wallpaper_chooser_base, newFragment); + ft.commit(); + } + } - findViewById(R.id.set).setOnClickListener(this); + @Override + protected void onDestroy() { + super.onDestroy(); - mImageView = (ImageView) findViewById(R.id.wallpaper); + if (mLoader != null && mLoader.getStatus() != WallpaperLoader.Status.FINISHED) { + mLoader.cancel(true); + mLoader = null; + } } private void findWallpapers() { @@ -105,40 +125,7 @@ public class WallpaperChooser extends Activity implements AdapterView.OnItemSele } } - @Override - protected void onResume() { - super.onResume(); - mIsWallpaperSet = false; - } - - @Override - protected void onDestroy() { - super.onDestroy(); - - if (mLoader != null && mLoader.getStatus() != WallpaperLoader.Status.FINISHED) { - mLoader.cancel(true); - mLoader = null; - } - } - - public void onItemSelected(AdapterView parent, View v, int position, long id) { - if (mLoader != null && mLoader.getStatus() != WallpaperLoader.Status.FINISHED) { - mLoader.cancel(); - } - mLoader = (WallpaperLoader) new WallpaperLoader().execute(position); - } - - /* - * When using touch if you tap an image it triggers both the onItemClick and - * the onTouchEvent causing the wallpaper to be set twice. Ensure we only - * set the wallpaper once. - */ private void selectWallpaper(int position) { - if (mIsWallpaperSet) { - return; - } - - mIsWallpaperSet = true; try { WallpaperManager wpm = (WallpaperManager)getSystemService(WALLPAPER_SERVICE); wpm.setResource(mImages.get(position)); @@ -149,10 +136,95 @@ public class WallpaperChooser extends Activity implements AdapterView.OnItemSele } } - public void onNothingSelected(AdapterView parent) { + private class WallpaperDialogFragment extends DialogFragment implements + AdapterView.OnItemSelectedListener, AdapterView.OnItemClickListener { + private Context mContext; + + public WallpaperDialogFragment(Context context) { + super(); + mContext = context; + setCancelable(true); + } + + @Override + public void onDismiss(DialogInterface dialog) { + WallpaperChooser.this.finish(); + } + + /* This will only be called when in XLarge mode, since this Fragment is invoked like + * a dialog in that mode + */ + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final View v = getLayoutInflater().inflate( + R.layout.wallpaper_chooser, mWallpaperChooserBase, false); + + GridView gridView = (GridView) v.findViewById(R.id.gallery); + gridView.setOnItemClickListener(this); + gridView.setAdapter(new ImageAdapter(WallpaperChooser.this)); + + final int viewInset = + getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_inset); + + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + builder.setCancelable(true); + builder.setNegativeButton(R.string.wallpaper_cancel, null); + builder.setTitle(R.string.wallpaper_dialog_title); + builder.setView(gridView, viewInset, viewInset, viewInset, viewInset); + return builder.create(); + } + + /* This will be called on both XLarge and small screens, but since the dialog + * is already created on XLarge, we want to skip this view creation + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + /* Only generate a custom view if this fragment is being embedded in a view, + * i.e: on a small screen + */ + if (!LauncherApplication.isScreenXLarge()) { + View view = inflater.inflate(R.layout.wallpaper_chooser, container, false); + + final Gallery gallery = (Gallery) view.findViewById(R.id.gallery); + gallery.setCallbackDuringFling(false); + gallery.setOnItemSelectedListener(this); + gallery.setAdapter(new ImageAdapter(WallpaperChooser.this)); + + View setButton = view.findViewById(R.id.set); + setButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + selectWallpaper(gallery.getSelectedItemPosition()); + } + }); + mImageView = (ImageView) view.findViewById(R.id.wallpaper); + return view; + } + return super.onCreateView(inflater, container, savedInstanceState); + } + + // Click handler for the Dialog's GridView + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + selectWallpaper(position); + } + + // Selection handler for the embedded Gallery view + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + if (mLoader != null && mLoader.getStatus() != WallpaperLoader.Status.FINISHED) { + mLoader.cancel(); + } + mLoader = (WallpaperLoader) new WallpaperLoader().execute(position); + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + } } - private class ImageAdapter extends BaseAdapter { + private class ImageAdapter extends BaseAdapter implements ListAdapter, SpinnerAdapter { private LayoutInflater mLayoutInflater; ImageAdapter(WallpaperChooser context) { @@ -179,7 +251,7 @@ public class WallpaperChooser extends Activity implements AdapterView.OnItemSele } else { image = (ImageView) convertView; } - + int thumbRes = mThumbs.get(position); image.setImageResource(thumbRes); Drawable thumbDrawable = image.getDrawable(); @@ -189,14 +261,11 @@ public class WallpaperChooser extends Activity implements AdapterView.OnItemSele Log.e(TAG, "Error decoding thumbnail resId=" + thumbRes + " for wallpaper #" + position); } + return image; } } - public void onClick(View v) { - selectWallpaper(mGallery.getSelectedItemPosition()); - } - class WallpaperLoader extends AsyncTask<Integer, Void, Bitmap> { BitmapFactory.Options mOptions; @@ -205,7 +274,8 @@ public class WallpaperChooser extends Activity implements AdapterView.OnItemSele mOptions.inDither = false; mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; } - + + @Override protected Bitmap doInBackground(Integer... params) { if (isCancelled()) return null; try { @@ -225,17 +295,20 @@ public class WallpaperChooser extends Activity implements AdapterView.OnItemSele if (mBitmap != null) { mBitmap.recycle(); } - + + // This should always be the case, but check anyways final ImageView view = mImageView; - view.setImageBitmap(b); - - mBitmap = b; - - final Drawable drawable = view.getDrawable(); - drawable.setFilterBitmap(true); - drawable.setDither(true); - - view.postInvalidate(); + if (view != null) { + view.setImageBitmap(b); + + mBitmap = b; + + final Drawable drawable = view.getDrawable(); + drawable.setFilterBitmap(true); + drawable.setDither(true); + + view.postInvalidate(); + } mLoader = null; } else { diff --git a/src/com/android/launcher2/Workspace.java b/src/com/android/launcher2/Workspace.java index d193448..0a3f915 100644 --- a/src/com/android/launcher2/Workspace.java +++ b/src/com/android/launcher2/Workspace.java @@ -18,147 +18,187 @@ package com.android.launcher2; import java.util.ArrayList; import java.util.HashSet; - +import java.util.List; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.animation.Animator.AnimatorListener; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.app.AlertDialog; import android.app.WallpaperManager; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; +import android.content.ClipData; +import android.content.ClipDescription; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; +import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Camera; import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region.Op; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.IBinder; -import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; +import android.util.Pair; +import android.view.Display; +import android.view.DragEvent; import android.view.MotionEvent; -import android.view.VelocityTracker; import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.ViewParent; -import android.view.animation.Interpolator; -import android.widget.Scroller; +import android.view.animation.DecelerateInterpolator; import android.widget.TextView; +import android.widget.Toast; import com.android.launcher.R; +import com.android.launcher2.InstallWidgetReceiver.WidgetMimeTypeHandlerData; /** - * The workspace is a wide area with a wallpaper and a finite number of screens. Each - * screen contains a number of icons, folders or widgets the user can interact with. - * A workspace is meant to be used with a fixed width only. + * The workspace is a wide area with a wallpaper and a finite number of pages. + * Each page contains a number of icons, folders or widgets the user can + * interact with. A workspace is meant to be used with a fixed width only. */ -public class Workspace extends ViewGroup implements DropTarget, DragSource, DragScroller { +public class Workspace extends SmoothPagedView + implements DropTarget, DragSource, DragScroller, View.OnTouchListener { @SuppressWarnings({"UnusedDeclaration"}) private static final String TAG = "Launcher.Workspace"; - private static final int INVALID_SCREEN = -1; - - /** - * The velocity at which a fling gesture will cause us to snap to the next screen - */ - private static final int SNAP_VELOCITY = 600; + + // This is how much the workspace shrinks when we enter all apps or + // customization mode + private static final float SHRINK_FACTOR = 0.16f; + + // Y rotation to apply to the workspace screens + private static final float WORKSPACE_ROTATION = 12.5f; + private static final float WORKSPACE_TRANSLATION = 50.0f; + + // These are extra scale factors to apply to the mini home screens + // so as to achieve the desired transform + private static final float EXTRA_SCALE_FACTOR_0 = 0.972f; + private static final float EXTRA_SCALE_FACTOR_1 = 1.0f; + private static final float EXTRA_SCALE_FACTOR_2 = 1.10f; + + private static final int CHILDREN_OUTLINE_FADE_OUT_DELAY = 0; + private static final int CHILDREN_OUTLINE_FADE_OUT_DURATION = 375; + private static final int CHILDREN_OUTLINE_FADE_IN_DURATION = 100; + + private static final int BACKGROUND_FADE_OUT_DURATION = 350; + private static final int BACKGROUND_FADE_IN_DURATION = 350; + + // These animators are used to fade the children's outlines + private ObjectAnimator mChildrenOutlineFadeInAnimation; + private ObjectAnimator mChildrenOutlineFadeOutAnimation; + private float mChildrenOutlineAlpha = 0; + + // These properties refer to the background protection gradient used for AllApps and Customize + private ObjectAnimator mBackgroundFadeInAnimation; + private ObjectAnimator mBackgroundFadeOutAnimation; + private Drawable mBackground; + private float mBackgroundAlpha = 0; + private float mOverScrollMaxBackgroundAlpha = 0.0f; + private int mOverScrollPageIndex = -1; private final WallpaperManager mWallpaperManager; - - private int mDefaultScreen; - private boolean mFirstLayout = true; + private int mDefaultPage; - private int mCurrentScreen; - private int mNextScreen = INVALID_SCREEN; - private Scroller mScroller; - private VelocityTracker mVelocityTracker; + private boolean mPageMoving = false; /** * CellInfo for the cell that is currently being dragged */ private CellLayout.CellInfo mDragInfo; - + /** * Target drop area calculated during last acceptDrop call. */ private int[] mTargetCell = null; - private float mLastMotionX; - private float mLastMotionY; - - private final static int TOUCH_STATE_REST = 0; - private final static int TOUCH_STATE_SCROLLING = 1; - - private int mTouchState = TOUCH_STATE_REST; - - private OnLongClickListener mLongClickListener; + /** + * The CellLayout that is currently being dragged over + */ + private CellLayout mDragTargetLayout = null; private Launcher mLauncher; private IconCache mIconCache; private DragController mDragController; - - /** - * Cache of vacant cells, used during drag events and invalidated as needed. - */ - private CellLayout.CellInfo mVacantCache = null; - + + // These are temporary variables to prevent having to allocate a new object just to + // return an (x, y) value from helper functions. Do NOT use them to maintain other state. private int[] mTempCell = new int[2]; private int[] mTempEstimate = new int[2]; + private float[] mTempOriginXY = new float[2]; + private float[] mTempDragCoordinates = new float[2]; + private float[] mTempTouchCoordinates = new float[2]; + private float[] mTempCellLayoutCenterCoordinates = new float[2]; + private float[] mTempDragBottomRightCoordinates = new float[2]; + private Matrix mTempInverseMatrix = new Matrix(); - private boolean mAllowLongPress = true; + private static final int DEFAULT_CELL_COUNT_X = 4; + private static final int DEFAULT_CELL_COUNT_Y = 4; - private int mTouchSlop; - private int mMaximumVelocity; - - private static final int INVALID_POINTER = -1; - - private int mActivePointerId = INVALID_POINTER; - private Drawable mPreviousIndicator; private Drawable mNextIndicator; - - private static final float NANOTIME_DIV = 1000000000.0f; - private static final float SMOOTHING_SPEED = 0.75f; - private static final float SMOOTHING_CONSTANT = (float) (0.016 / Math.log(SMOOTHING_SPEED)); - private float mSmoothingTime; - private float mTouchX; - private WorkspaceOvershootInterpolator mScrollInterpolator; + // State variable that indicates whether the pages are small (ie when you're + // in all apps or customize mode) + private boolean mIsSmall = false; + private boolean mIsInUnshrinkAnimation = false; + private AnimatorListener mUnshrinkAnimationListener; + private enum ShrinkPosition { + SHRINK_TO_TOP, SHRINK_TO_MIDDLE, SHRINK_TO_BOTTOM_HIDDEN, SHRINK_TO_BOTTOM_VISIBLE }; + private ShrinkPosition mShrunkenState; + private boolean mWaitingToShrink = false; + private ShrinkPosition mWaitingToShrinkPosition; + private AnimatorSet mAnimator; - private static final float BASELINE_FLING_VELOCITY = 2500.f; - private static final float FLING_VELOCITY_INFLUENCE = 0.4f; - - private static class WorkspaceOvershootInterpolator implements Interpolator { - private static final float DEFAULT_TENSION = 1.3f; - private float mTension; + /** Is the user is dragging an item near the edge of a page? */ + private boolean mInScrollArea = false; - public WorkspaceOvershootInterpolator() { - mTension = DEFAULT_TENSION; - } - - public void setDistance(int distance) { - mTension = distance > 0 ? DEFAULT_TENSION / distance : DEFAULT_TENSION; - } + /** If mInScrollArea is true, the direction of the scroll. */ + private int mPendingScrollDirection = DragController.SCROLL_NONE; - public void disableSettle() { - mTension = 0.f; - } + private boolean mInDragMode = false; + + private final HolographicOutlineHelper mOutlineHelper = new HolographicOutlineHelper(); + private Bitmap mDragOutline = null; + private final Rect mTempRect = new Rect(); + private final int[] mTempXY = new int[2]; + + private ValueAnimator mDropAnim = null; + private TimeInterpolator mQuintEaseOutInterpolator = new DecelerateInterpolator(2.5f); + private View mDropView = null; + private int[] mDropViewPos = new int[] { -1, -1 }; + + // Paint used to draw external drop outline + private final Paint mExternalDragOutlinePaint = new Paint(); + + /** Used to trigger an animation as soon as the workspace stops scrolling. */ + private Animator mAnimOnPageEndMoving = null; + + // Camera and Matrix used to determine the final position of a neighboring CellLayout + private final Matrix mMatrix = new Matrix(); + private final Camera mCamera = new Camera(); + private final float mTempFloat2[] = new float[2]; - public float getInterpolation(float t) { - // _o(t) = t * t * ((tension + 1) * t + tension) - // o(t) = _o(t - 1) + 1 - t -= 1.0f; - return t * t * ((mTension + 1) * t + mTension) + 1.0f; - } - } - /** * Used to inflate the Workspace from XML. * * @param context The application's context. - * @param attrs The attribtues set containing the Workspace's customization values. + * @param attrs The attributes set containing the Workspace's customization values. */ public Workspace(Context context, AttributeSet attrs) { this(context, attrs, 0); @@ -168,37 +208,71 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag * Used to inflate the Workspace from XML. * * @param context The application's context. - * @param attrs The attribtues set containing the Workspace's customization values. + * @param attrs The attributes set containing the Workspace's customization values. * @param defStyle Unused. */ public Workspace(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + mContentIsRefreshable = false; + + if (!LauncherApplication.isScreenXLarge()) { + mFadeInAdjacentScreens = false; + } mWallpaperManager = WallpaperManager.getInstance(context); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Workspace, defStyle, 0); - mDefaultScreen = a.getInt(R.styleable.Workspace_defaultScreen, 1); + + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.Workspace, defStyle, 0); + int cellCountX = a.getInt(R.styleable.Workspace_cellCountX, DEFAULT_CELL_COUNT_X); + int cellCountY = a.getInt(R.styleable.Workspace_cellCountY, DEFAULT_CELL_COUNT_Y); + mDefaultPage = a.getInt(R.styleable.Workspace_defaultScreen, 1); a.recycle(); + LauncherModel.updateWorkspaceLayoutCells(cellCountX, cellCountY); setHapticFeedbackEnabled(false); + initWorkspace(); } /** * Initializes various states for this workspace. */ - private void initWorkspace() { + protected void initWorkspace() { Context context = getContext(); - mScrollInterpolator = new WorkspaceOvershootInterpolator(); - mScroller = new Scroller(context, mScrollInterpolator); - mCurrentScreen = mDefaultScreen; - Launcher.setScreen(mCurrentScreen); + mCurrentPage = mDefaultPage; + Launcher.setScreen(mCurrentPage); LauncherApplication app = (LauncherApplication)context.getApplicationContext(); mIconCache = app.getIconCache(); + mExternalDragOutlinePaint.setAntiAlias(true); + setWillNotDraw(false); + + try { + final Resources res = getResources(); + mBackground = res.getDrawable(R.drawable.all_apps_bg_gradient); + } catch (Resources.NotFoundException e) { + // In this case, we will skip drawing background protection + } + + mUnshrinkAnimationListener = new LauncherAnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mIsInUnshrinkAnimation = true; + } + @Override + public void onAnimationEndOrCancel(Animator animation) { + mIsInUnshrinkAnimation = false; + } + }; + mSnapVelocity = 600; + } - final ViewConfiguration configuration = ViewConfiguration.get(getContext()); - mTouchSlop = configuration.getScaledTouchSlop(); - mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + @Override + protected int getScrollMode() { + if (LauncherApplication.isScreenXLarge()) { + return SmoothPagedView.X_LARGE_MODE; + } else { + return SmoothPagedView.DEFAULT_MODE; + } } @Override @@ -206,6 +280,7 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag if (!(child instanceof CellLayout)) { throw new IllegalArgumentException("A Workspace can only have CellLayout children."); } + ((CellLayout) child).setOnInterceptTouchListener(this); super.addView(child, index, params); } @@ -214,6 +289,7 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag if (!(child instanceof CellLayout)) { throw new IllegalArgumentException("A Workspace can only have CellLayout children."); } + ((CellLayout) child).setOnInterceptTouchListener(this); super.addView(child); } @@ -222,6 +298,7 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag if (!(child instanceof CellLayout)) { throw new IllegalArgumentException("A Workspace can only have CellLayout children."); } + ((CellLayout) child).setOnInterceptTouchListener(this); super.addView(child, index); } @@ -230,6 +307,7 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag if (!(child instanceof CellLayout)) { throw new IllegalArgumentException("A Workspace can only have CellLayout children."); } + ((CellLayout) child).setOnInterceptTouchListener(this); super.addView(child, width, height); } @@ -238,6 +316,7 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag if (!(child instanceof CellLayout)) { throw new IllegalArgumentException("A Workspace can only have CellLayout children."); } + ((CellLayout) child).setOnInterceptTouchListener(this); super.addView(child, params); } @@ -245,94 +324,52 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag * @return The open folder on the current screen, or null if there is none */ Folder getOpenFolder() { - CellLayout currentScreen = (CellLayout) getChildAt(mCurrentScreen); - int count = currentScreen.getChildCount(); + CellLayout currentPage = (CellLayout) getChildAt(mCurrentPage); + int count = currentPage.getChildCount(); for (int i = 0; i < count; i++) { - View child = currentScreen.getChildAt(i); - CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); - if (lp.cellHSpan == 4 && lp.cellVSpan == 4 && child instanceof Folder) { - return (Folder) child; + View child = currentPage.getChildAt(i); + if (child instanceof Folder) { + Folder folder = (Folder) child; + if (folder.getInfo().opened) + return folder; } } return null; } ArrayList<Folder> getOpenFolders() { - final int screens = getChildCount(); - ArrayList<Folder> folders = new ArrayList<Folder>(screens); + final int screenCount = getChildCount(); + ArrayList<Folder> folders = new ArrayList<Folder>(screenCount); - for (int screen = 0; screen < screens; screen++) { - CellLayout currentScreen = (CellLayout) getChildAt(screen); - int count = currentScreen.getChildCount(); + for (int screen = 0; screen < screenCount; screen++) { + CellLayout currentPage = (CellLayout) getChildAt(screen); + int count = currentPage.getChildCount(); for (int i = 0; i < count; i++) { - View child = currentScreen.getChildAt(i); - CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); - if (lp.cellHSpan == 4 && lp.cellVSpan == 4 && child instanceof Folder) { - folders.add((Folder) child); + View child = currentPage.getChildAt(i); + if (child instanceof Folder) { + Folder folder = (Folder) child; + if (folder.getInfo().opened) + folders.add(folder); break; } } } - return folders; } - boolean isDefaultScreenShowing() { - return mCurrentScreen == mDefaultScreen; - } - - /** - * Returns the index of the currently displayed screen. - * - * @return The index of the currently displayed screen. - */ - int getCurrentScreen() { - return mCurrentScreen; + boolean isDefaultPageShowing() { + return mCurrentPage == mDefaultPage; } /** * Sets the current screen. * - * @param currentScreen - */ - void setCurrentScreen(int currentScreen) { - if (!mScroller.isFinished()) mScroller.abortAnimation(); - clearVacantCache(); - mCurrentScreen = Math.max(0, Math.min(currentScreen, getChildCount() - 1)); - mPreviousIndicator.setLevel(mCurrentScreen); - mNextIndicator.setLevel(mCurrentScreen); - scrollTo(mCurrentScreen * getWidth(), 0); - updateWallpaperOffset(); - invalidate(); - } - - /** - * Adds the specified child in the current screen. The position and dimension of - * the child are defined by x, y, spanX and spanY. - * - * @param child The child to add in one of the workspace's screens. - * @param x The X position of the child in the screen's grid. - * @param y The Y position of the child in the screen's grid. - * @param spanX The number of cells spanned horizontally by the child. - * @param spanY The number of cells spanned vertically by the child. - */ - void addInCurrentScreen(View child, int x, int y, int spanX, int spanY) { - addInScreen(child, mCurrentScreen, x, y, spanX, spanY, false); - } - - /** - * Adds the specified child in the current screen. The position and dimension of - * the child are defined by x, y, spanX and spanY. - * - * @param child The child to add in one of the workspace's screens. - * @param x The X position of the child in the screen's grid. - * @param y The Y position of the child in the screen's grid. - * @param spanX The number of cells spanned horizontally by the child. - * @param spanY The number of cells spanned vertically by the child. - * @param insert When true, the child is inserted at the beginning of the children list. + * @param currentPage */ - void addInCurrentScreen(View child, int x, int y, int spanX, int spanY, boolean insert) { - addInScreen(child, mCurrentScreen, x, y, spanX, spanY, insert); + @Override + void setCurrentPage(int currentPage) { + super.setCurrentPage(currentPage); + updateWallpaperOffset(mScrollX); } /** @@ -350,6 +387,10 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag addInScreen(child, screen, x, y, spanX, spanY, false); } + void addInFullScreen(View child, int screen) { + addInScreen(child, screen, 0, 0, -1, -1); + } + /** * Adds the specified child in the specified screen. The position and dimension of * the child are defined by x, y, spanX and spanY. @@ -369,8 +410,6 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag return; } - clearVacantCache(); - final CellLayout group = (CellLayout) getChildAt(screen); CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); if (lp == null) { @@ -381,124 +420,286 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag lp.cellHSpan = spanX; lp.cellVSpan = spanY; } - group.addView(child, insert ? 0 : -1, lp); + + // Get the canonical child id to uniquely represent this view in this screen + int childId = LauncherModel.getCellLayoutChildId(-1, screen, x, y, spanX, spanY); + boolean markCellsAsOccupied = !(child instanceof Folder); + if (!group.addViewToCellLayout(child, insert ? 0 : -1, childId, lp, markCellsAsOccupied)) { + // TODO: This branch occurs when the workspace is adding views + // outside of the defined grid + // maybe we should be deleting these items from the LauncherModel? + Log.w(TAG, "Failed to add to item at (" + lp.cellX + "," + lp.cellY + ") to CellLayout"); + } + if (!(child instanceof Folder)) { child.setHapticFeedbackEnabled(false); child.setOnLongClickListener(mLongClickListener); } if (child instanceof DropTarget) { - mDragController.addDropTarget((DropTarget)child); + mDragController.addDropTarget((DropTarget) child); } } - CellLayout.CellInfo findAllVacantCells(boolean[] occupied) { - CellLayout group = (CellLayout) getChildAt(mCurrentScreen); - if (group != null) { - return group.findAllVacantCells(occupied, null); + public boolean onTouch(View v, MotionEvent event) { + // this is an intercepted event being forwarded from a cell layout + if (mIsSmall || mIsInUnshrinkAnimation) { + // Only allow clicks on a CellLayout if it is visible + if (mShrunkenState != ShrinkPosition.SHRINK_TO_BOTTOM_HIDDEN) { + mLauncher.onWorkspaceClick((CellLayout) v); + } + return true; + } else if (!mPageMoving) { + if (v == getChildAt(mCurrentPage - 1)) { + snapToPage(mCurrentPage - 1); + return true; + } else if (v == getChildAt(mCurrentPage + 1)) { + snapToPage(mCurrentPage + 1); + return true; + } } - return null; + return false; + } + + protected void onWindowVisibilityChanged (int visibility) { + mLauncher.onWindowVisibilityChanged(visibility); } - private void clearVacantCache() { - if (mVacantCache != null) { - mVacantCache.clearVacantCells(); - mVacantCache = null; + @Override + public boolean dispatchUnhandledMove(View focused, int direction) { + if (mIsSmall || mIsInUnshrinkAnimation) { + // when the home screens are shrunken, shouldn't allow side-scrolling + return false; } + return super.dispatchUnhandledMove(focused, direction); } - /** - * Registers the specified listener on each screen contained in this workspace. - * - * @param l The listener used to respond to long clicks. - */ @Override - public void setOnLongClickListener(OnLongClickListener l) { - mLongClickListener = l; - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - getChildAt(i).setOnLongClickListener(l); + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (mIsSmall || mIsInUnshrinkAnimation) { + if (mLauncher.isAllAppsVisible() && + mShrunkenState == ShrinkPosition.SHRINK_TO_BOTTOM_HIDDEN) { + // Intercept this event so we can show the workspace in full view + // when it is clicked on and it is small + return true; + } + return false; } + return super.onInterceptTouchEvent(ev); } + @Override + protected void determineScrollingStart(MotionEvent ev) { + if (!mIsSmall && !mIsInUnshrinkAnimation) super.determineScrollingStart(ev); + } + + protected void onPageBeginMoving() { + if (mNextPage != INVALID_PAGE) { + // we're snapping to a particular screen + enableChildrenCache(mCurrentPage, mNextPage); + } else { + // this is when user is actively dragging a particular screen, they might + // swipe it either left or right (but we won't advance by more than one screen) + enableChildrenCache(mCurrentPage - 1, mCurrentPage + 1); + } + showOutlines(); + mPageMoving = true; + } + + protected void onPageEndMoving() { + clearChildrenCache(); + // Hide the outlines, as long as we're not dragging + if (!mDragController.dragging()) { + hideOutlines(); + } + // Check for an animation that's waiting to be started + if (mAnimOnPageEndMoving != null) { + mAnimOnPageEndMoving.start(); + mAnimOnPageEndMoving = null; + } + mOverScrollMaxBackgroundAlpha = 0.0f; + mOverScrollPageIndex = -1; + mPageMoving = false; + } + + @Override + protected void notifyPageSwitchListener() { + super.notifyPageSwitchListener(); + + if (mPreviousIndicator != null) { + // if we know the next page, we show the indication for it right away; it looks + // weird if the indicators are lagging + int page = mNextPage; + if (page == INVALID_PAGE) { + page = mCurrentPage; + } + mPreviousIndicator.setLevel(page); + mNextIndicator.setLevel(page); + } + Launcher.setScreen(mCurrentPage); + }; + private void updateWallpaperOffset() { updateWallpaperOffset(getChildAt(getChildCount() - 1).getRight() - (mRight - mLeft)); } private void updateWallpaperOffset(int scrollRange) { - IBinder token = getWindowToken(); - if (token != null) { - mWallpaperManager.setWallpaperOffsetSteps(1.0f / (getChildCount() - 1), 0 ); - mWallpaperManager.setWallpaperOffsets(getWindowToken(), - Math.max(0.f, Math.min(mScrollX/(float)scrollRange, 1.f)), 0); + final boolean isStaticWallpaper = (mWallpaperManager != null) && + (mWallpaperManager.getWallpaperInfo() == null); + if (LauncherApplication.isScreenXLarge() && !isStaticWallpaper) { + IBinder token = getWindowToken(); + if (token != null) { + mWallpaperManager.setWallpaperOffsetSteps(1.0f / (getChildCount() - 1), 0 ); + mWallpaperManager.setWallpaperOffsets(getWindowToken(), + Math.max(0.f, Math.min(mScrollX/(float)scrollRange, 1.f)), 0); + } } } - - @Override - public void scrollTo(int x, int y) { - super.scrollTo(x, y); - mTouchX = x; - mSmoothingTime = System.nanoTime() / NANOTIME_DIV; + + public void showOutlines() { + if (!mIsSmall && !mIsInUnshrinkAnimation) { + if (mChildrenOutlineFadeOutAnimation != null) mChildrenOutlineFadeOutAnimation.cancel(); + if (mChildrenOutlineFadeInAnimation != null) mChildrenOutlineFadeInAnimation.cancel(); + mChildrenOutlineFadeInAnimation = ObjectAnimator.ofFloat(this, "childrenOutlineAlpha", 1.0f); + mChildrenOutlineFadeInAnimation.setDuration(CHILDREN_OUTLINE_FADE_IN_DURATION); + mChildrenOutlineFadeInAnimation.start(); + } } - - @Override - public void computeScroll() { - if (mScroller.computeScrollOffset()) { - mTouchX = mScrollX = mScroller.getCurrX(); - mSmoothingTime = System.nanoTime() / NANOTIME_DIV; - mScrollY = mScroller.getCurrY(); - updateWallpaperOffset(); - postInvalidate(); - } else if (mNextScreen != INVALID_SCREEN) { - mCurrentScreen = Math.max(0, Math.min(mNextScreen, getChildCount() - 1)); - mPreviousIndicator.setLevel(mCurrentScreen); - mNextIndicator.setLevel(mCurrentScreen); - Launcher.setScreen(mCurrentScreen); - mNextScreen = INVALID_SCREEN; - clearChildrenCache(); - } else if (mTouchState == TOUCH_STATE_SCROLLING) { - final float now = System.nanoTime() / NANOTIME_DIV; - final float e = (float) Math.exp((now - mSmoothingTime) / SMOOTHING_CONSTANT); - final float dx = mTouchX - mScrollX; - mScrollX += dx * e; - mSmoothingTime = now; - - // Keep generating points as long as we're more than 1px away from the target - if (dx > 1.f || dx < -1.f) { - updateWallpaperOffset(); - postInvalidate(); - } + + public void hideOutlines() { + if (!mIsSmall && !mIsInUnshrinkAnimation) { + if (mChildrenOutlineFadeInAnimation != null) mChildrenOutlineFadeInAnimation.cancel(); + if (mChildrenOutlineFadeOutAnimation != null) mChildrenOutlineFadeOutAnimation.cancel(); + mChildrenOutlineFadeOutAnimation = ObjectAnimator.ofFloat(this, "childrenOutlineAlpha", 0.0f); + mChildrenOutlineFadeOutAnimation.setDuration(CHILDREN_OUTLINE_FADE_OUT_DURATION); + mChildrenOutlineFadeOutAnimation.setStartDelay(CHILDREN_OUTLINE_FADE_OUT_DELAY); + mChildrenOutlineFadeOutAnimation.start(); } } - @Override - protected void dispatchDraw(Canvas canvas) { - boolean restore = false; - int restoreCount = 0; - - // ViewGroup.dispatchDraw() supports many features we don't need: - // clip to padding, layout animation, animation listener, disappearing - // children, etc. The following implementation attempts to fast-track - // the drawing dispatch by drawing only what we know needs to be drawn. - - boolean fastDraw = mTouchState != TOUCH_STATE_SCROLLING && mNextScreen == INVALID_SCREEN; - // If we are not scrolling or flinging, draw only the current screen - if (fastDraw) { - drawChild(canvas, getChildAt(mCurrentScreen), getDrawingTime()); + public void setChildrenOutlineAlpha(float alpha) { + mChildrenOutlineAlpha = alpha; + for (int i = 0; i < getChildCount(); i++) { + CellLayout cl = (CellLayout) getChildAt(i); + cl.setBackgroundAlpha(alpha); + } + } + + public float getChildrenOutlineAlpha() { + return mChildrenOutlineAlpha; + } + + public void showBackgroundGradient() { + if (mBackground == null) return; + if (mBackgroundFadeOutAnimation != null) mBackgroundFadeOutAnimation.cancel(); + if (mBackgroundFadeInAnimation != null) mBackgroundFadeInAnimation.cancel(); + mBackgroundFadeInAnimation = ObjectAnimator.ofFloat(this, "backgroundAlpha", 1.0f); + mBackgroundFadeInAnimation.setInterpolator(new DecelerateInterpolator(1.5f)); + mBackgroundFadeInAnimation.setDuration(BACKGROUND_FADE_IN_DURATION); + mBackgroundFadeInAnimation.start(); + } + + public void hideBackgroundGradient() { + if (mBackground == null) return; + if (mBackgroundFadeInAnimation != null) mBackgroundFadeInAnimation.cancel(); + if (mBackgroundFadeOutAnimation != null) mBackgroundFadeOutAnimation.cancel(); + mBackgroundFadeOutAnimation = ObjectAnimator.ofFloat(this, "backgroundAlpha", 0.0f); + mBackgroundFadeOutAnimation.setInterpolator(new DecelerateInterpolator(1.5f)); + mBackgroundFadeOutAnimation.setDuration(BACKGROUND_FADE_OUT_DURATION); + mBackgroundFadeOutAnimation.start(); + } + + public void setBackgroundAlpha(float alpha) { + mBackgroundAlpha = alpha; + invalidate(); + } + + public float getBackgroundAlpha() { + return mBackgroundAlpha; + } + + /** + * Due to 3D transformations, if two CellLayouts are theoretically touching each other, + * on the xy plane, when one is rotated along the y-axis, the gap between them is perceived + * as being larger. This method computes what offset the rotated view should be translated + * in order to minimize this perceived gap. + * @param degrees Angle of the view + * @param width Width of the view + * @param height Height of the view + * @return Offset to be used in a View.setTranslationX() call + */ + private float getOffsetXForRotation(float degrees, int width, int height) { + mMatrix.reset(); + mCamera.save(); + mCamera.rotateY(Math.abs(degrees)); + mCamera.getMatrix(mMatrix); + mCamera.restore(); + + mMatrix.preTranslate(-width * 0.5f, -height * 0.5f); + mMatrix.postTranslate(width * 0.5f, height * 0.5f); + mTempFloat2[0] = width; + mTempFloat2[1] = height; + mMatrix.mapPoints(mTempFloat2); + return (width - mTempFloat2[0]) * (degrees > 0.0f ? 1.0f : -1.0f); + } + + float backgroundAlphaInterpolator(float r) { + float pivotA = 0.1f; + float pivotB = 0.4f; + if (r < pivotA) { + return 0; + } else if (r > pivotB) { + return 1.0f; } else { - final long drawingTime = getDrawingTime(); - final float scrollPos = (float) mScrollX / getWidth(); - final int leftScreen = (int) scrollPos; - final int rightScreen = leftScreen + 1; - if (leftScreen >= 0) { - drawChild(canvas, getChildAt(leftScreen), drawingTime); - } - if (scrollPos != leftScreen && rightScreen < getChildCount()) { - drawChild(canvas, getChildAt(rightScreen), drawingTime); - } + return (r - pivotA)/(pivotB - pivotA); } + } + + float overScrollBackgroundAlphaInterpolator(float r) { + float threshold = 0.08f; - if (restore) { - canvas.restoreToCount(restoreCount); + if (r > mOverScrollMaxBackgroundAlpha) { + mOverScrollMaxBackgroundAlpha = r; + } else if (r < mOverScrollMaxBackgroundAlpha) { + r = mOverScrollMaxBackgroundAlpha; + } + + return Math.min(r / threshold, 1.0f); + } + + @Override + protected void screenScrolled(int screenCenter) { + final int halfScreenSize = getMeasuredWidth() / 2; + + for (int i = 0; i < getChildCount(); i++) { + CellLayout cl = (CellLayout) getChildAt(i); + if (cl != null) { + int totalDistance = cl.getMeasuredWidth() + mPageSpacing; + int delta = screenCenter - (getChildOffset(i) - + getRelativeChildOffset(i) + halfScreenSize); + + float scrollProgress = delta / (totalDistance * 1.0f); + scrollProgress = Math.min(scrollProgress, 1.0f); + scrollProgress = Math.max(scrollProgress, -1.0f); + + // If the current page (i) is being overscrolled, we use a different + // set of rules for setting the background alpha multiplier. + if ((mScrollX < 0 && i == 0) || (mScrollX > mMaxScrollX && + i == getChildCount() -1 )) { + cl.setBackgroundAlphaMultiplier( + overScrollBackgroundAlphaInterpolator(Math.abs(scrollProgress))); + mOverScrollPageIndex = i; + } else if (mOverScrollPageIndex != i) { + cl.setBackgroundAlphaMultiplier( + backgroundAlphaInterpolator(Math.abs(scrollProgress))); + + } + + float rotation = WORKSPACE_ROTATION * scrollProgress; + float translationX = getOffsetXForRotation(rotation, cl.getWidth(), cl.getHeight()); + cl.setTranslationX(translationX); + + cl.setRotationY(rotation); + } } } @@ -509,59 +710,84 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag } @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); - final int width = MeasureSpec.getSize(widthMeasureSpec); - final int widthMode = MeasureSpec.getMode(widthMeasureSpec); - if (widthMode != MeasureSpec.EXACTLY) { - throw new IllegalStateException("Workspace can only be used in EXACTLY mode."); + // 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 (mWaitingToShrink) { + shrink(mWaitingToShrinkPosition, false); + mWaitingToShrink = false; } - final int heightMode = MeasureSpec.getMode(heightMeasureSpec); - if (heightMode != MeasureSpec.EXACTLY) { - throw new IllegalStateException("Workspace can only be used in EXACTLY mode."); + if (LauncherApplication.isInPlaceRotationEnabled()) { + // When the device is rotated, the scroll position of the current screen + // needs to be refreshed + setCurrentPage(getCurrentPage()); } + } - // The children are given the same width and height as the workspace - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec); + @Override + protected void onDraw(Canvas canvas) { + // Draw the background gradient if necessary + if (mBackground != null && mBackgroundAlpha > 0.0f) { + mBackground.setAlpha((int) (mBackgroundAlpha * 255)); + mBackground.setBounds(mScrollX, 0, mScrollX + getMeasuredWidth(), getMeasuredHeight()); + mBackground.draw(canvas); } - - if (mFirstLayout) { - setHorizontalScrollBarEnabled(false); - scrollTo(mCurrentScreen * width, 0); - setHorizontalScrollBarEnabled(true); - updateWallpaperOffset(width * (getChildCount() - 1)); - mFirstLayout = false; - } + super.onDraw(canvas); } @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - int childLeft = 0; + protected void dispatchDraw(Canvas canvas) { + if (mIsSmall || mIsInUnshrinkAnimation) { + // Draw all the workspaces if we're small + final int pageCount = getChildCount(); + final long drawingTime = getDrawingTime(); + for (int i = 0; i < pageCount; i++) { + final View page = (View) getChildAt(i); - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - final View child = getChildAt(i); - if (child.getVisibility() != View.GONE) { - final int childWidth = child.getMeasuredWidth(); - child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight()); - childLeft += childWidth; + drawChild(canvas, page, drawingTime); } - } - } + } else { + super.dispatchDraw(canvas); - @Override - public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { - int screen = indexOfChild(child); - if (screen != mCurrentScreen || !mScroller.isFinished()) { - snapToScreen(screen); - return true; + final int width = getWidth(); + final int height = getHeight(); + + // In portrait orientation, draw the glowing edge when dragging to adjacent screens + if (mInScrollArea && (height > width)) { + final int pageHeight = getChildAt(0).getHeight(); + + // This determines the height of the glowing edge: 90% of the page height + final int padding = (int) ((height - pageHeight) * 0.5f + pageHeight * 0.1f); + + final CellLayout leftPage = (CellLayout) getChildAt(mCurrentPage - 1); + final CellLayout rightPage = (CellLayout) getChildAt(mCurrentPage + 1); + + if (leftPage != null && leftPage.getHover()) { + final Drawable d = getResources().getDrawable(R.drawable.page_hover_left); + d.setBounds(mScrollX, padding, mScrollX + d.getIntrinsicWidth(), height - padding); + d.draw(canvas); + } else if (rightPage != null && rightPage.getHover()) { + final Drawable d = getResources().getDrawable(R.drawable.page_hover_right); + d.setBounds(mScrollX + width - d.getIntrinsicWidth(), padding, mScrollX + width, height - padding); + d.draw(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(); + canvas.translate(xPos, yPos); + mDropView.draw(canvas); + canvas.restore(); + } } - return false; } @Override @@ -571,51 +797,20 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag if (openFolder != null) { return openFolder.requestFocus(direction, previouslyFocusedRect); } else { - int focusableScreen; - if (mNextScreen != INVALID_SCREEN) { - focusableScreen = mNextScreen; - } else { - focusableScreen = mCurrentScreen; - } - getChildAt(focusableScreen).requestFocus(direction, previouslyFocusedRect); + return super.onRequestFocusInDescendants(direction, previouslyFocusedRect); } } return false; } @Override - public boolean dispatchUnhandledMove(View focused, int direction) { - if (direction == View.FOCUS_LEFT) { - if (getCurrentScreen() > 0) { - snapToScreen(getCurrentScreen() - 1); - return true; - } - } else if (direction == View.FOCUS_RIGHT) { - if (getCurrentScreen() < getChildCount() - 1) { - snapToScreen(getCurrentScreen() + 1); - return true; - } - } - return super.dispatchUnhandledMove(focused, direction); - } - - @Override public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { if (!mLauncher.isAllAppsVisible()) { final Folder openFolder = getOpenFolder(); - if (openFolder == null) { - getChildAt(mCurrentScreen).addFocusables(views, direction); - if (direction == View.FOCUS_LEFT) { - if (mCurrentScreen > 0) { - getChildAt(mCurrentScreen - 1).addFocusables(views, direction); - } - } else if (direction == View.FOCUS_RIGHT){ - if (mCurrentScreen < getChildCount() - 1) { - getChildAt(mCurrentScreen + 1).addFocusables(views, direction); - } - } - } else { + if (openFolder != null) { openFolder.addFocusables(views, direction); + } else { + super.addFocusables(views, direction, focusableMode); } } } @@ -623,591 +818,1329 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { - if (mLauncher.isAllAppsVisible()) { + // (In XLarge mode, the workspace is shrunken below all apps, and responds to taps + // ie when you click on a mini-screen, it zooms back to that screen) + if (!LauncherApplication.isScreenXLarge() && mLauncher.isAllAppsVisible()) { return false; } } + return super.dispatchTouchEvent(ev); } - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - final boolean allAppsVisible = mLauncher.isAllAppsVisible(); - if (allAppsVisible) { - return false; // We don't want the events. Let them fall through to the all apps view. - } - - /* - * This method JUST determines whether we want to intercept the motion. - * If we return true, onTouchEvent will be called and we do the actual - * scrolling there. - */ - - /* - * Shortcut the most recurring case: the user is in the dragging - * state and he is moving his finger. We want to intercept this - * motion. - */ - final int action = ev.getAction(); - if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) { - return true; + void enableChildrenCache(int fromPage, int toPage) { + if (fromPage > toPage) { + final int temp = fromPage; + fromPage = toPage; + toPage = temp; } - acquireVelocityTrackerAndAddMovement(ev); - - switch (action & MotionEvent.ACTION_MASK) { - case MotionEvent.ACTION_MOVE: { - /* - * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check - * whether the user has moved far enough from his original down touch. - */ - - /* - * Locally do absolute value. mLastMotionX is set to the y value - * of the down event. - */ - final int pointerIndex = ev.findPointerIndex(mActivePointerId); - final float x = ev.getX(pointerIndex); - final float y = ev.getY(pointerIndex); - final int xDiff = (int) Math.abs(x - mLastMotionX); - final int yDiff = (int) Math.abs(y - mLastMotionY); - - final int touchSlop = mTouchSlop; - boolean xMoved = xDiff > touchSlop; - boolean yMoved = yDiff > touchSlop; - - if (xMoved || yMoved) { - - if (xMoved) { - // Scroll if the user moved far enough along the X axis - mTouchState = TOUCH_STATE_SCROLLING; - mLastMotionX = x; - mTouchX = mScrollX; - mSmoothingTime = System.nanoTime() / NANOTIME_DIV; - enableChildrenCache(mCurrentScreen - 1, mCurrentScreen + 1); - } - // Either way, cancel any pending longpress - if (mAllowLongPress) { - mAllowLongPress = false; - // Try canceling the long press. It could also have been scheduled - // by a distant descendant, so use the mAllowLongPress flag to block - // everything - final View currentScreen = getChildAt(mCurrentScreen); - currentScreen.cancelLongPress(); - } - } - break; - } + final int screenCount = getChildCount(); - case MotionEvent.ACTION_DOWN: { - final float x = ev.getX(); - final float y = ev.getY(); - // Remember location of down touch - mLastMotionX = x; - mLastMotionY = y; - mActivePointerId = ev.getPointerId(0); - mAllowLongPress = true; - - /* - * If being flinged and user touches the screen, initiate drag; - * otherwise don't. mScroller.isFinished should be false when - * being flinged. - */ - mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING; - break; - } + fromPage = Math.max(fromPage, 0); + toPage = Math.min(toPage, screenCount - 1); - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - - if (mTouchState != TOUCH_STATE_SCROLLING) { - final CellLayout currentScreen = (CellLayout)getChildAt(mCurrentScreen); - if (!currentScreen.lastDownOnOccupiedCell()) { - getLocationOnScreen(mTempCell); - // Send a tap to the wallpaper if the last down was on empty space - final int pointerIndex = ev.findPointerIndex(mActivePointerId); - if (pointerIndex >= 0) { - mWallpaperManager.sendWallpaperCommand(getWindowToken(), - "android.wallpaper.tap", - mTempCell[0] + (int) ev.getX(pointerIndex), - mTempCell[1] + (int) ev.getY(pointerIndex), 0, null); - } - } - } - - // Release the drag - clearChildrenCache(); - mTouchState = TOUCH_STATE_REST; - mActivePointerId = INVALID_POINTER; - mAllowLongPress = false; - releaseVelocityTracker(); - break; - - case MotionEvent.ACTION_POINTER_UP: - onSecondaryPointerUp(ev); - break; + for (int i = fromPage; i <= toPage; i++) { + final CellLayout layout = (CellLayout) getChildAt(i); + layout.setChildrenDrawnWithCacheEnabled(true); + layout.setChildrenDrawingCacheEnabled(true); } + } - /* - * The only time we want to intercept motion events is if we are in the - * drag mode. - */ - return mTouchState != TOUCH_STATE_REST; - } - - private void onSecondaryPointerUp(MotionEvent ev) { - final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> - MotionEvent.ACTION_POINTER_INDEX_SHIFT; - final int pointerId = ev.getPointerId(pointerIndex); - if (pointerId == mActivePointerId) { - // This was our active pointer going up. Choose a new - // active pointer and adjust accordingly. - // TODO: Make this decision more intelligent. - final int newPointerIndex = pointerIndex == 0 ? 1 : 0; - mLastMotionX = ev.getX(newPointerIndex); - mLastMotionY = ev.getY(newPointerIndex); - mActivePointerId = ev.getPointerId(newPointerIndex); - if (mVelocityTracker != null) { - mVelocityTracker.clear(); - } + void clearChildrenCache() { + final int screenCount = getChildCount(); + for (int i = 0; i < screenCount; i++) { + final CellLayout layout = (CellLayout) getChildAt(i); + layout.setChildrenDrawnWithCacheEnabled(false); } } - /** - * If one of our descendant views decides that it could be focused now, only - * pass that along if it's on the current screen. - * - * This happens when live folders requery, and if they're off screen, they - * end up calling requestFocus, which pulls it on screen. - */ @Override - public void focusableViewAvailable(View focused) { - View current = getChildAt(mCurrentScreen); - View v = focused; - while (true) { - if (v == current) { - super.focusableViewAvailable(focused); - return; + public boolean onTouchEvent(MotionEvent ev) { + if (mLauncher.isAllAppsVisible()) { + // Cancel any scrolling that is in progress. + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); } - if (v == this) { - return; + setCurrentPage(mCurrentPage); + + if (mShrunkenState == ShrinkPosition.SHRINK_TO_BOTTOM_HIDDEN) { + mLauncher.showWorkspace(true); + // Let the events fall through to the CellLayouts because if they are not + // hit, then we get a crash due to a missing ACTION_DOWN touch event } - ViewParent parent = v.getParent(); - if (parent instanceof View) { - v = (View)v.getParent(); + + return false; // We don't want the events + } + + return super.onTouchEvent(ev); + } + + public boolean isSmall() { + return mIsSmall; + } + + void shrinkToTop(boolean animated) { + shrink(ShrinkPosition.SHRINK_TO_TOP, animated); + } + + void shrinkToMiddle() { + shrink(ShrinkPosition.SHRINK_TO_MIDDLE, true); + } + + void shrinkToBottomHidden() { + shrinkToBottomHidden(true); + } + + void shrinkToBottomVisible() { + shrinkToBottomVisible(true); + } + + void shrinkToBottomHidden(boolean animated) { + shrink(ShrinkPosition.SHRINK_TO_BOTTOM_HIDDEN, animated); + } + + void shrinkToBottomVisible(boolean animated) { + shrink(ShrinkPosition.SHRINK_TO_BOTTOM_VISIBLE, animated); + } + + private float getYScaleForScreen(int screen) { + int x = Math.abs(screen - 2); + + // TODO: This should be generalized for use with arbitrary rotation angles. + switch(x) { + case 0: return EXTRA_SCALE_FACTOR_0; + case 1: return EXTRA_SCALE_FACTOR_1; + case 2: return EXTRA_SCALE_FACTOR_2; + } + return 1.0f; + } + + // we use this to shrink the workspace for the all apps view and the customize view + private void shrink(ShrinkPosition shrinkPosition, boolean animated) { + showBackgroundGradient(); + + if (mFirstLayout) { + // (mFirstLayout == "first layout has not happened yet") + // if we get a call to shrink() as part of our initialization (for example, if + // Launcher is started in All Apps mode) then we need to wait for a layout call + // to get our width so we can layout the mini-screen views correctly + mWaitingToShrink = true; + mWaitingToShrinkPosition = shrinkPosition; + return; + } + mIsSmall = true; + mShrunkenState = shrinkPosition; + + // Stop any scrolling, move to the current page right away + setCurrentPage((mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage); + updateWhichPagesAcceptDrops(mShrunkenState); + + // we intercept and reject all touch events when we're small, so be sure to reset the state + mTouchState = TOUCH_STATE_REST; + mActivePointerId = INVALID_POINTER; + + CellLayout currentPage = (CellLayout) getChildAt(mCurrentPage); + if (currentPage.getBackgroundAlphaMultiplier() < 1.0f) { + currentPage.setBackgroundAlpha(0.0f); + } + currentPage.setBackgroundAlphaMultiplier(1.0f); + + final Resources res = getResources(); + final int screenWidth = getWidth(); + final int screenHeight = getHeight(); + + // Making the assumption that all pages have the same width as the 0th + final int pageWidth = getChildAt(0).getMeasuredWidth(); + final int pageHeight = getChildAt(0).getMeasuredHeight(); + + final int scaledPageWidth = (int) (SHRINK_FACTOR * pageWidth); + final int scaledPageHeight = (int) (SHRINK_FACTOR * pageHeight); + final float extraScaledSpacing = res.getDimension(R.dimen.smallScreenExtraSpacing); + + final int screenCount = getChildCount(); + float totalWidth = screenCount * scaledPageWidth + (screenCount - 1) * extraScaledSpacing; + + boolean isPortrait = getMeasuredHeight() > getMeasuredWidth(); + float newY = (isPortrait ? + getResources().getDimension(R.dimen.allAppsSmallScreenVerticalMarginPortrait) : + getResources().getDimension(R.dimen.allAppsSmallScreenVerticalMarginLandscape)); + float finalAlpha = 1.0f; + float extraShrinkFactor = 1.0f; + if (shrinkPosition == ShrinkPosition.SHRINK_TO_BOTTOM_VISIBLE) { + newY = screenHeight - newY - scaledPageHeight; + } else if (shrinkPosition == ShrinkPosition.SHRINK_TO_BOTTOM_HIDDEN) { + + // We shrink and disappear to nothing in the case of all apps + // (which is when we shrink to the bottom) + newY = screenHeight - newY - scaledPageHeight; + finalAlpha = 0.0f; + } else if (shrinkPosition == ShrinkPosition.SHRINK_TO_MIDDLE) { + newY = screenHeight / 2 - scaledPageHeight / 2; + finalAlpha = 1.0f; + } else if (shrinkPosition == ShrinkPosition.SHRINK_TO_TOP) { + newY = (isPortrait ? + getResources().getDimension(R.dimen.customizeSmallScreenVerticalMarginPortrait) : + getResources().getDimension(R.dimen.customizeSmallScreenVerticalMarginLandscape)); + } + + // We animate all the screens to the centered position in workspace + // At the same time, the screens become greyed/dimmed + + // newX is initialized to the left-most position of the centered screens + float newX = mScroller.getFinalX() + screenWidth / 2 - totalWidth / 2; + + // We are going to scale about the center of the view, so we need to adjust the positions + // of the views accordingly + newX -= (pageWidth - scaledPageWidth) / 2.0f; + newY -= (pageHeight - scaledPageHeight) / 2.0f; + + if (mAnimator != null) { + mAnimator.cancel(); + } + mAnimator = new AnimatorSet(); + for (int i = 0; i < screenCount; i++) { + CellLayout cl = (CellLayout) getChildAt(i); + + float rotation = (-i + 2) * WORKSPACE_ROTATION; + float rotationScaleX = (float) (1.0f / Math.cos(Math.PI * rotation / 180.0f)); + float rotationScaleY = getYScaleForScreen(i); + + if (animated) { + final int duration = res.getInteger(R.integer.config_workspaceShrinkTime); + + ObjectAnimator animWithInterpolator = ObjectAnimator.ofPropertyValuesHolder(cl, + PropertyValuesHolder.ofFloat("x", newX), + PropertyValuesHolder.ofFloat("y", newY), + PropertyValuesHolder.ofFloat("scaleX", + SHRINK_FACTOR * rotationScaleX * extraShrinkFactor), + PropertyValuesHolder.ofFloat("scaleY", + SHRINK_FACTOR * rotationScaleY * extraShrinkFactor), + PropertyValuesHolder.ofFloat("backgroundAlpha", finalAlpha), + PropertyValuesHolder.ofFloat("alpha", finalAlpha), + PropertyValuesHolder.ofFloat("rotationY", rotation)); + + animWithInterpolator.setDuration(duration); + animWithInterpolator.setInterpolator(mZoomOutInterpolator); + mAnimator.playTogether(animWithInterpolator); } else { - return; + cl.setX((int)newX); + cl.setY((int)newY); + cl.setScaleX(SHRINK_FACTOR * rotationScaleX * extraShrinkFactor); + cl.setScaleY(SHRINK_FACTOR * rotationScaleY * extraShrinkFactor); + cl.setBackgroundAlpha(finalAlpha); + cl.setAlpha(finalAlpha); + cl.setRotationY(rotation); } + // increment newX for the next screen + newX += scaledPageWidth + extraScaledSpacing; } + if (animated) { + mAnimator.start(); + } + setChildrenDrawnWithCacheEnabled(true); } - void enableChildrenCache(int fromScreen, int toScreen) { - if (fromScreen > toScreen) { - final int temp = fromScreen; - fromScreen = toScreen; - toScreen = temp; + /* + * 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. + */ + static class ZInterpolator implements TimeInterpolator { + private float focalLength; + + public ZInterpolator(float foc) { + focalLength = foc; } - - final int count = getChildCount(); - fromScreen = Math.max(fromScreen, 0); - toScreen = Math.min(toScreen, count - 1); + public float getInterpolation(float input) { + return (1.0f - focalLength / (focalLength + input)) / + (1.0f - focalLength / (focalLength + 1.0f)); + } + } - for (int i = fromScreen; i <= toScreen; i++) { - final CellLayout layout = (CellLayout) getChildAt(i); - layout.setChildrenDrawnWithCacheEnabled(true); - layout.setChildrenDrawingCacheEnabled(true); + /* + * The exact reverse of ZInterpolator. + */ + static class InverseZInterpolator implements TimeInterpolator { + private ZInterpolator zInterpolator; + public InverseZInterpolator(float foc) { + zInterpolator = new ZInterpolator(foc); + } + public float getInterpolation(float input) { + return 1 - zInterpolator.getInterpolation(1 - input); } } - void clearChildrenCache() { - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - final CellLayout layout = (CellLayout) getChildAt(i); - layout.setChildrenDrawnWithCacheEnabled(false); + /* + * ZInterpolator compounded with an ease-out. + */ + static class ZoomOutInterpolator implements TimeInterpolator { + private final ZInterpolator zInterpolator = new ZInterpolator(0.2f); + private final DecelerateInterpolator decelerate = new DecelerateInterpolator(1.5f); + + public float getInterpolation(float input) { + return decelerate.getInterpolation(zInterpolator.getInterpolation(input)); } } - @Override - public boolean onTouchEvent(MotionEvent ev) { - - if (mLauncher.isAllAppsVisible()) { - // Cancel any scrolling that is in progress. - if (!mScroller.isFinished()) { - mScroller.abortAnimation(); - } - snapToScreen(mCurrentScreen); - return false; // We don't want the events. Let them fall through to the all apps view. + /* + * InvereZInterpolator compounded with an ease-out. + */ + static class ZoomInInterpolator implements TimeInterpolator { + private final InverseZInterpolator inverseZInterpolator = new InverseZInterpolator(0.35f); + private final DecelerateInterpolator decelerate = new DecelerateInterpolator(3.0f); + + public float getInterpolation(float input) { + return decelerate.getInterpolation(inverseZInterpolator.getInterpolation(input)); } + } - acquireVelocityTrackerAndAddMovement(ev); + private final ZoomOutInterpolator mZoomOutInterpolator = new ZoomOutInterpolator(); + private final ZoomInInterpolator mZoomInInterpolator = new ZoomInInterpolator(); - final int action = ev.getAction(); + private void updateWhichPagesAcceptDrops(ShrinkPosition state) { + updateWhichPagesAcceptDropsHelper(state, false, 1, 1); + } - switch (action & MotionEvent.ACTION_MASK) { - case MotionEvent.ACTION_DOWN: - /* - * If being flinged and user touches, stop the fling. isFinished - * will be false if being flinged. - */ - if (!mScroller.isFinished()) { - mScroller.abortAnimation(); - } + private void updateWhichPagesAcceptDropsDuringDrag(ShrinkPosition state, int spanX, int spanY) { + updateWhichPagesAcceptDropsHelper(state, true, spanX, spanY); + } - // Remember where the motion event started - mLastMotionX = ev.getX(); - mActivePointerId = ev.getPointerId(0); - if (mTouchState == TOUCH_STATE_SCROLLING) { - enableChildrenCache(mCurrentScreen - 1, mCurrentScreen + 1); - } - break; - case MotionEvent.ACTION_MOVE: - if (mTouchState == TOUCH_STATE_SCROLLING) { - // Scroll to follow the motion event - final int pointerIndex = ev.findPointerIndex(mActivePointerId); - final float x = ev.getX(pointerIndex); - final float deltaX = mLastMotionX - x; - mLastMotionX = x; - - if (deltaX < 0) { - if (mTouchX > 0) { - mTouchX += Math.max(-mTouchX, deltaX); - mSmoothingTime = System.nanoTime() / NANOTIME_DIV; - invalidate(); + private void updateWhichPagesAcceptDropsHelper( + ShrinkPosition state, boolean isDragHappening, int spanX, int spanY) { + final int screenCount = getChildCount(); + for (int i = 0; i < screenCount; i++) { + CellLayout cl = (CellLayout) getChildAt(i); + + switch (state) { + case SHRINK_TO_TOP: + if (!isDragHappening) { + boolean showDropHighlight = i == mCurrentPage; + cl.setAcceptsDrops(showDropHighlight); + break; } - } else if (deltaX > 0) { - final float availableToScroll = getChildAt(getChildCount() - 1).getRight() - - mTouchX - getWidth(); - if (availableToScroll > 0) { - mTouchX += Math.min(availableToScroll, deltaX); - mSmoothingTime = System.nanoTime() / NANOTIME_DIV; - invalidate(); + // otherwise, fall through below and mark non-full screens as accepting drops + case SHRINK_TO_BOTTOM_HIDDEN: + case SHRINK_TO_BOTTOM_VISIBLE: + if (!isDragHappening) { + // even if a drag isn't happening, we don't want to show a screen as + // accepting drops if it doesn't have at least one free cell + spanX = 1; + spanY = 1; } - } else { - awakenScrollBars(); - } - } - break; - case MotionEvent.ACTION_UP: - if (mTouchState == TOUCH_STATE_SCROLLING) { - final VelocityTracker velocityTracker = mVelocityTracker; - velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); - final int velocityX = (int) velocityTracker.getXVelocity(mActivePointerId); - - final int screenWidth = getWidth(); - final int whichScreen = (mScrollX + (screenWidth / 2)) / screenWidth; - final float scrolledPos = (float) mScrollX / screenWidth; - - if (velocityX > SNAP_VELOCITY && mCurrentScreen > 0) { - // Fling hard enough to move left. - // Don't fling across more than one screen at a time. - final int bound = scrolledPos < whichScreen ? - mCurrentScreen - 1 : mCurrentScreen; - snapToScreen(Math.min(whichScreen, bound), velocityX, true); - } else if (velocityX < -SNAP_VELOCITY && mCurrentScreen < getChildCount() - 1) { - // Fling hard enough to move right - // Don't fling across more than one screen at a time. - final int bound = scrolledPos > whichScreen ? - mCurrentScreen + 1 : mCurrentScreen; - snapToScreen(Math.max(whichScreen, bound), velocityX, true); - } else { - snapToScreen(whichScreen, 0, true); - } - } - mTouchState = TOUCH_STATE_REST; - mActivePointerId = INVALID_POINTER; - releaseVelocityTracker(); - break; - case MotionEvent.ACTION_CANCEL: - if (mTouchState == TOUCH_STATE_SCROLLING) { - final int screenWidth = getWidth(); - final int whichScreen = (mScrollX + (screenWidth / 2)) / screenWidth; - snapToScreen(whichScreen, 0, true); + // the page accepts drops if we can find at least one empty spot + cl.setAcceptsDrops(cl.findCellForSpan(null, spanX, spanY)); + break; + default: + throw new RuntimeException( + "updateWhichPagesAcceptDropsHelper passed an unhandled ShrinkPosition"); } - mTouchState = TOUCH_STATE_REST; - mActivePointerId = INVALID_POINTER; - releaseVelocityTracker(); - break; - case MotionEvent.ACTION_POINTER_UP: - onSecondaryPointerUp(ev); - break; } + } - return true; + /* + * + * We call these methods (onDragStartedWithItemSpans/onDragStartedWithItemMinSize) whenever we + * start a drag in Launcher, regardless of whether the drag has ever entered the Workspace + * + * These methods mark the appropriate pages as accepting drops (which alters their visual + * appearance). + * + */ + public void onDragStartedWithItemSpans(int spanX, int spanY) { + updateWhichPagesAcceptDropsDuringDrag(mShrunkenState, spanX, spanY); } - - private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) { - if (mVelocityTracker == null) { - mVelocityTracker = VelocityTracker.obtain(); - } - mVelocityTracker.addMovement(ev); + + public void onDragStartedWithItemMinSize(int minWidth, int minHeight) { + int[] spanXY = CellLayout.rectToCell(getResources(), minWidth, minHeight, null); + onDragStartedWithItemSpans(spanXY[0], spanXY[1]); } - private void releaseVelocityTracker() { - if (mVelocityTracker != null) { - mVelocityTracker.recycle(); - mVelocityTracker = null; + // we call this method whenever a drag and drop in Launcher finishes, even if Workspace was + // never dragged over + public void onDragStopped() { + updateWhichPagesAcceptDrops(mShrunkenState); + } + + @Override + protected boolean handlePagingClicks() { + return true; + } + + // We call this when we trigger an unshrink by clicking on the CellLayout cl + public void unshrink(CellLayout clThatWasClicked) { + int newCurrentPage = indexOfChild(clThatWasClicked); + if (mIsSmall) { + int newX = getChildOffset(newCurrentPage) - getRelativeChildOffset(newCurrentPage); + int delta = newX - mScrollX; + + final int screenCount = getChildCount(); + for (int i = 0; i < screenCount; i++) { + CellLayout cl = (CellLayout) getChildAt(i); + cl.setX(cl.getX() + delta); + } + setCurrentPage(newCurrentPage); + unshrink(); } } - void snapToScreen(int whichScreen) { - snapToScreen(whichScreen, 0, false); + void unshrink() { + unshrink(true); } - private void snapToScreen(int whichScreen, int velocity, boolean settle) { - //if (!mScroller.isFinished()) return; + void unshrink(boolean animated) { + hideBackgroundGradient(); - whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1)); - - clearVacantCache(); - enableChildrenCache(mCurrentScreen, whichScreen); + if (mIsSmall) { + mIsSmall = false; + if (mAnimator != null) { + mAnimator.cancel(); + } - mNextScreen = whichScreen; + mAnimator = new AnimatorSet(); + final int screenCount = getChildCount(); - mPreviousIndicator.setLevel(mNextScreen); - mNextIndicator.setLevel(mNextScreen); + final int duration = getResources().getInteger(R.integer.config_workspaceUnshrinkTime); + for (int i = 0; i < screenCount; i++) { + final CellLayout cl = (CellLayout)getChildAt(i); + float finalAlphaValue = (i == mCurrentPage) ? 1.0f : 0.0f; + float rotation = 0.0f; - View focusedChild = getFocusedChild(); - if (focusedChild != null && whichScreen != mCurrentScreen && - focusedChild == getChildAt(mCurrentScreen)) { - focusedChild.clearFocus(); - } - - final int screenDelta = Math.max(1, Math.abs(whichScreen - mCurrentScreen)); - final int newX = whichScreen * getWidth(); - final int delta = newX - mScrollX; - int duration = (screenDelta + 1) * 100; + if (i < mCurrentPage) { + rotation = WORKSPACE_ROTATION; + } else if (i > mCurrentPage) { + rotation = -WORKSPACE_ROTATION; + } - if (!mScroller.isFinished()) { - mScroller.abortAnimation(); - } - - if (settle) { - mScrollInterpolator.setDistance(screenDelta); - } else { - mScrollInterpolator.disableSettle(); + float translation = getOffsetXForRotation(rotation, cl.getWidth(), cl.getHeight()); + + if (animated) { + ObjectAnimator animWithInterpolator = ObjectAnimator.ofPropertyValuesHolder(cl, + PropertyValuesHolder.ofFloat("translationX", translation), + PropertyValuesHolder.ofFloat("translationY", 0.0f), + PropertyValuesHolder.ofFloat("scaleX", 1.0f), + PropertyValuesHolder.ofFloat("scaleY", 1.0f), + PropertyValuesHolder.ofFloat("backgroundAlpha", 0.0f), + PropertyValuesHolder.ofFloat("alpha", finalAlphaValue), + PropertyValuesHolder.ofFloat("rotationY", rotation)); + animWithInterpolator.setDuration(duration); + animWithInterpolator.setInterpolator(mZoomInInterpolator); + mAnimator.playTogether(animWithInterpolator); + } else { + cl.setTranslationX(translation); + cl.setTranslationY(0.0f); + cl.setScaleX(1.0f); + cl.setScaleY(1.0f); + cl.setBackgroundAlpha(0.0f); + cl.setAlpha(finalAlphaValue); + cl.setRotationY(rotation); + } + } + + if (animated) { + // 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(mUnshrinkAnimationListener); + mAnimator.start(); + } } - - velocity = Math.abs(velocity); - if (velocity > 0) { - duration += (duration / (velocity / BASELINE_FLING_VELOCITY)) - * FLING_VELOCITY_INFLUENCE; - } else { - duration += 100; + } + + /** + * Draw the View v into the given Canvas. + * + * @param v the view to draw + * @param destCanvas the canvas to draw on + * @param padding the horizontal and vertical padding to use when drawing + */ + private void drawDragView(View v, Canvas destCanvas, int padding) { + final Rect clipRect = mTempRect; + v.getDrawingRect(clipRect); + + // For a TextView, adjust the clip rect so that we don't include the text label + if (v instanceof TextView) { + final TextView tv = (TextView) v; + clipRect.bottom = clipRect.top + tv.getCompoundPaddingTop() - 1; } - awakenScrollBars(duration); - mScroller.startScroll(mScrollX, 0, delta, 0, duration); - invalidate(); + // Draw the View into the bitmap. + // 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.translate(-v.getScrollX() + padding / 2, -v.getScrollY() + padding / 2); + destCanvas.clipRect(clipRect, Op.REPLACE); + v.draw(destCanvas); + destCanvas.restore(); + } + + /** + * Returns a new bitmap to be used as the object outline, e.g. to visualize the drop location. + * Responsibility for the bitmap is transferred to the caller. + */ + private Bitmap createDragOutline(View v, Canvas canvas, int padding) { + final int outlineColor = getResources().getColor(R.color.drag_outline_color); + final Bitmap b = Bitmap.createBitmap( + v.getWidth() + padding, v.getHeight() + padding, Bitmap.Config.ARGB_8888); + + canvas.setBitmap(b); + drawDragView(v, canvas, padding); + mOutlineHelper.applyExpensiveOuterOutline(b, canvas, outlineColor, true); + + 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(R.color.drag_outline_color); + 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.applyExpensiveOuterOutline(b, canvas, outlineColor, true); + + return b; + } + + /** + * Returns a new bitmap to show when the given View is being dragged around. + * Responsibility for the bitmap is transferred to the caller. + */ + private Bitmap createDragBitmap(View v, Canvas canvas, int padding) { + final int outlineColor = getResources().getColor(R.color.drag_outline_color); + final Bitmap b = Bitmap.createBitmap( + mDragOutline.getWidth(), mDragOutline.getHeight(), Bitmap.Config.ARGB_8888); + + canvas.setBitmap(b); + canvas.drawBitmap(mDragOutline, 0, 0, null); + drawDragView(v, canvas, padding); + mOutlineHelper.applyOuterBlur(b, canvas, outlineColor); + + return b; } void startDrag(CellLayout.CellInfo cellInfo) { View child = cellInfo.cell; - + // Make sure the drag was started by a long press as opposed to a long click. if (!child.isInTouchMode()) { return; } - + mDragInfo = cellInfo; - mDragInfo.screen = mCurrentScreen; - - CellLayout current = ((CellLayout) getChildAt(mCurrentScreen)); + mDragInfo.screen = mCurrentPage; + + CellLayout current = getCurrentDropLayout(); current.onDragChild(child); - mDragController.startDrag(child, this, child.getTag(), DragController.DRAG_ACTION_MOVE); - invalidate(); + + 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.OUTER_BLUR_RADIUS; + + // The outline is used to visualize where the item will land if dropped + mDragOutline = createDragOutline(child, canvas, bitmapPadding); + + // The drag bitmap follows the touch point around on the screen + final Bitmap b = createDragBitmap(child, canvas, bitmapPadding); + + final int bmpWidth = b.getWidth(); + final int bmpHeight = b.getHeight(); + child.getLocationOnScreen(mTempXY); + final int screenX = (int) mTempXY[0] + (child.getWidth() - bmpWidth) / 2; + final int screenY = (int) mTempXY[1] + (child.getHeight() - bmpHeight) / 2; + mDragController.startDrag(b, screenX, screenY, 0, 0, bmpWidth, bmpHeight, this, + child.getTag(), DragController.DRAG_ACTION_MOVE, null); + b.recycle(); } - @Override - protected Parcelable onSaveInstanceState() { - final SavedState state = new SavedState(super.onSaveInstanceState()); - state.currentScreen = mCurrentScreen; - return state; + void addApplicationShortcut(ShortcutInfo info, int screen, int cellX, int cellY, + boolean insertAtFirst, int intersectX, int intersectY) { + final CellLayout cellLayout = (CellLayout) getChildAt(screen); + View view = mLauncher.createShortcut(R.layout.application, cellLayout, (ShortcutInfo) info); + + final int[] cellXY = new int[2]; + cellLayout.findCellForSpanThatIntersects(cellXY, 1, 1, intersectX, intersectY); + addInScreen(view, screen, cellXY[0], cellXY[1], 1, 1, insertAtFirst); + LauncherModel.addOrMoveItemInDatabase(mLauncher, info, + LauncherSettings.Favorites.CONTAINER_DESKTOP, screen, + cellXY[0], cellXY[1]); } - @Override - protected void onRestoreInstanceState(Parcelable state) { - SavedState savedState = (SavedState) state; - super.onRestoreInstanceState(savedState.getSuperState()); - if (savedState.currentScreen != -1) { - mCurrentScreen = savedState.currentScreen; - Launcher.setScreen(mCurrentScreen); - } + private void setPositionForDropAnimation( + View dragView, int dragViewX, int dragViewY, View parent, View child) { + final CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); + + // Based on the position of the drag view, find the top left of the original view + int viewX = dragViewX + (dragView.getWidth() - child.getWidth()) / 2; + int viewY = dragViewY + (dragView.getHeight() - child.getHeight()) / 2; + viewX += getResources().getInteger(R.integer.config_dragViewOffsetX); + viewY += getResources().getInteger(R.integer.config_dragViewOffsetY); + + // Set its old pos (in the new parent's coordinates); it will be animated + // in animateViewIntoPosition after the next layout pass + lp.oldX = viewX - (parent.getLeft() - mScrollX); + lp.oldY = viewY - (parent.getTop() - mScrollY); } - void addApplicationShortcut(ShortcutInfo info, CellLayout.CellInfo cellInfo) { - addApplicationShortcut(info, cellInfo, false); + public void animateViewIntoPosition(final View view) { + final CellLayout parent = (CellLayout) view.getParent(); + final CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams(); + + // Convert the animation params to be relative to the Workspace, not the CellLayout + final int fromX = lp.oldX + parent.getLeft(); + final int fromY = lp.oldY + parent.getTop(); + + final int dx = lp.x - lp.oldX; + final int dy = lp.y - lp.oldY; + + // Calculate the duration of the animation based on the object's distance + final float dist = (float) Math.sqrt(dx*dx + dy*dy); + final Resources res = getResources(); + final float maxDist = (float) res.getInteger(R.integer.config_dropAnimMaxDist); + int duration = res.getInteger(R.integer.config_dropAnimMaxDuration); + if (dist < maxDist) { + duration *= mQuintEaseOutInterpolator.getInterpolation(dist / maxDist); + } + + if (mDropAnim != null) { + // This should really be end(), but that will not be called synchronously, + // so instead we use LauncherAnimatorListenerAdapter.onAnimationEndOrCancel() + // and call cancel() here. + mDropAnim.cancel(); + } + mDropAnim = new ValueAnimator(); + mDropAnim.setInterpolator(mQuintEaseOutInterpolator); + + // The view is invisible during the animation; we render it manually. + mDropAnim.addListener(new LauncherAnimatorListenerAdapter() { + public void onAnimationStart(Animator animation) { + // Set this here so that we don't render it until the animation begins + mDropView = view; + } + + public void onAnimationEndOrCancel(Animator animation) { + if (mDropView != null) { + mDropView.setVisibility(View.VISIBLE); + mDropView = null; + } + } + }); + + mDropAnim.setDuration(duration); + mDropAnim.setFloatValues(0.0f, 1.0f); + mDropAnim.removeAllUpdateListeners(); + mDropAnim.addUpdateListener(new AnimatorUpdateListener() { + public void onAnimationUpdate(ValueAnimator animation) { + final float percent = (Float) animation.getAnimatedValue(); + // Invalidate the old position + invalidate(mDropViewPos[0], mDropViewPos[1], + mDropViewPos[0] + view.getWidth(), mDropViewPos[1] + view.getHeight()); + + mDropViewPos[0] = fromX + (int) (percent * dx + 0.5f); + mDropViewPos[1] = fromY + (int) (percent * dy + 0.5f); + invalidate(mDropViewPos[0], mDropViewPos[1], + mDropViewPos[0] + view.getWidth(), mDropViewPos[1] + view.getHeight()); + } + }); + + view.setVisibility(View.INVISIBLE); + + if (!mScroller.isFinished()) { + mAnimOnPageEndMoving = mDropAnim; + } else { + mDropAnim.start(); + } } - void addApplicationShortcut(ShortcutInfo info, CellLayout.CellInfo cellInfo, - boolean insertAtFirst) { - final CellLayout layout = (CellLayout) getChildAt(cellInfo.screen); - final int[] result = new int[2]; + /** + * {@inheritDoc} + */ + public boolean acceptDrop(DragSource source, int x, int y, + int xOffset, int yOffset, DragView dragView, Object dragInfo) { + + // If it's an external drop (e.g. from All Apps), check if it should be accepted + if (source != this) { + // Don't accept the drop if we're not over a screen at time of drop + if (mDragTargetLayout == null || !mDragTargetLayout.getAcceptsDrops()) { + return false; + } + + final CellLayout.CellInfo dragCellInfo = mDragInfo; + final int spanX = dragCellInfo == null ? 1 : dragCellInfo.spanX; + final int spanY = dragCellInfo == null ? 1 : dragCellInfo.spanY; + + final View ignoreView = dragCellInfo == null ? null : dragCellInfo.cell; - layout.cellToPoint(cellInfo.cellX, cellInfo.cellY, result); - onDropExternal(result[0], result[1], info, layout, insertAtFirst); + // Don't accept the drop if there's no room for the item + if (!mDragTargetLayout.findCellForSpanIgnoring(null, spanX, spanY, ignoreView)) { + mLauncher.showOutOfSpaceMessage(); + return false; + } + } + return true; } public void onDrop(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo) { - final CellLayout cellLayout = getCurrentDropLayout(); + + int originX = x - xOffset; + int originY = y - yOffset; + + if (mIsSmall || mIsInUnshrinkAnimation) { + // get originX and originY in the local coordinate system of the screen + mTempOriginXY[0] = originX; + mTempOriginXY[1] = originY; + mapPointFromSelfToChild(mDragTargetLayout, mTempOriginXY); + originX = (int)mTempOriginXY[0]; + originY = (int)mTempOriginXY[1]; + } + if (source != this) { - onDropExternal(x - xOffset, y - yOffset, dragInfo, cellLayout); + if (mIsSmall) { + // if we drag and drop to small screens, don't pass the touch x/y coords (when we + // enable spring-loaded adding, however, we do want to pass the touch x/y coords) + onDropExternal(-1, -1, dragInfo, mDragTargetLayout); + } else { + onDropExternal(originX, originY, dragInfo, mDragTargetLayout); + } + } else if (mDragInfo != null) { + final View cell = mDragInfo.cell; + CellLayout dropTargetLayout = mDragTargetLayout; + + // Handle the case where the user drops when in the scroll area. + // This is treated as a drop on the adjacent page. + if (dropTargetLayout == null && mInScrollArea) { + if (mPendingScrollDirection == DragController.SCROLL_LEFT) { + dropTargetLayout = (CellLayout) getChildAt(mCurrentPage - 1); + } else if (mPendingScrollDirection == DragController.SCROLL_RIGHT) { + dropTargetLayout = (CellLayout) getChildAt(mCurrentPage + 1); + } + } + + if (dropTargetLayout != null) { + // Move internally + mTargetCell = findNearestVacantArea(originX, originY, + mDragInfo.spanX, mDragInfo.spanY, cell, dropTargetLayout, + mTargetCell); + + final int screen = (mTargetCell == null) ? + mDragInfo.screen : indexOfChild(dropTargetLayout); + + if (screen != mCurrentPage) { + snapToPage(screen); + } + + if (mTargetCell != null) { + if (screen != mDragInfo.screen) { + // Reparent the view + ((CellLayout) getChildAt(mDragInfo.screen)).removeView(cell); + addInScreen(cell, screen, mTargetCell[0], mTargetCell[1], + mDragInfo.spanX, mDragInfo.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]; + cell.setId(LauncherModel.getCellLayoutChildId(-1, mDragInfo.screen, + mTargetCell[0], mTargetCell[1], mDragInfo.spanX, mDragInfo.spanY)); + + LauncherModel.moveItemInDatabase(mLauncher, info, + LauncherSettings.Favorites.CONTAINER_DESKTOP, screen, + lp.cellX, lp.cellY); + } + } + + final CellLayout parent = (CellLayout) cell.getParent(); + + // Prepare it to be animated into its new position + // This must be called after the view has been re-parented + setPositionForDropAnimation(dragView, originX, originY, parent, cell); + parent.onDropChild(cell); + } + } + + public void onDragEnter(DragSource source, int x, int y, int xOffset, + int yOffset, DragView dragView, Object dragInfo) { + mDragTargetLayout = null; // Reset the drag state + + if (!mIsSmall) { + mDragTargetLayout = getCurrentDropLayout(); + mDragTargetLayout.onDragEnter(); + showOutlines(); + } + } + + public DropTarget getDropTargetDelegate(DragSource source, int x, int y, + int xOffset, int yOffset, DragView dragView, Object dragInfo) { + + if (mIsSmall || mIsInUnshrinkAnimation) { + // If we're shrunken, don't let anyone drag on folders/etc that are on the mini-screens + return null; + } + // We may need to delegate the drag to a child view. If a 1x1 item + // would land in a cell occupied by a DragTarget (e.g. a Folder), + // then drag events should be handled by that child. + + ItemInfo item = (ItemInfo)dragInfo; + CellLayout currentLayout = getCurrentDropLayout(); + + int dragPointX, dragPointY; + if (item.spanX == 1 && item.spanY == 1) { + // For a 1x1, calculate the drop cell exactly as in onDragOver + dragPointX = x - xOffset; + dragPointY = y - yOffset; } else { - // Move internally - if (mDragInfo != null) { - final View cell = mDragInfo.cell; - int index = mScroller.isFinished() ? mCurrentScreen : mNextScreen; - if (index != mDragInfo.screen) { - final CellLayout originalCellLayout = (CellLayout) getChildAt(mDragInfo.screen); - originalCellLayout.removeView(cell); - cellLayout.addView(cell); + // Otherwise, use the exact drag coordinates + dragPointX = x; + dragPointY = y; + } + dragPointX += mScrollX - currentLayout.getLeft(); + dragPointY += mScrollY - currentLayout.getTop(); + + // If we are dragging over a cell that contains a DropTarget that will + // accept the drop, delegate to that DropTarget. + final int[] cellXY = mTempCell; + currentLayout.estimateDropCell(dragPointX, dragPointY, item.spanX, item.spanY, cellXY); + View child = currentLayout.getChildAt(cellXY[0], cellXY[1]); + if (child instanceof DropTarget) { + DropTarget target = (DropTarget)child; + if (target.acceptDrop(source, x, y, xOffset, yOffset, dragView, dragInfo)) { + return target; + } + } + return null; + } + + /** + * Tests to see if the drop will be accepted by Launcher, and if so, includes additional data + * in the returned structure related to the widgets that match the drop (or a null list if it is + * a shortcut drop). If the drop is not accepted then a null structure is returned. + */ + private Pair<Integer, List<WidgetMimeTypeHandlerData>> validateDrag(DragEvent event) { + final LauncherModel model = mLauncher.getModel(); + final ClipDescription desc = event.getClipDescription(); + final int mimeTypeCount = desc.getMimeTypeCount(); + for (int i = 0; i < mimeTypeCount; ++i) { + final String mimeType = desc.getMimeType(i); + if (mimeType.equals(InstallShortcutReceiver.SHORTCUT_MIMETYPE)) { + return new Pair<Integer, List<WidgetMimeTypeHandlerData>>(i, null); + } else { + final List<WidgetMimeTypeHandlerData> widgets = + model.resolveWidgetsForMimeType(mContext, mimeType); + if (widgets.size() > 0) { + return new Pair<Integer, List<WidgetMimeTypeHandlerData>>(i, widgets); } - mTargetCell = estimateDropCell(x - xOffset, y - yOffset, - mDragInfo.spanX, mDragInfo.spanY, cell, cellLayout, mTargetCell); - cellLayout.onDropChild(cell, mTargetCell); - - final ItemInfo info = (ItemInfo) cell.getTag(); - CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams(); - LauncherModel.moveItemInDatabase(mLauncher, info, - LauncherSettings.Favorites.CONTAINER_DESKTOP, index, lp.cellX, lp.cellY); } } + return null; } - public void onDragEnter(DragSource source, int x, int y, int xOffset, int yOffset, - DragView dragView, Object dragInfo) { - clearVacantCache(); + /** + * 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)) { + Toast.makeText(mContext, mContext.getString(R.string.out_of_space), + Toast.LENGTH_SHORT).show(); + 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.OUTER_BLUR_RADIUS; + mDragOutline = createExternalDragOutline(canvas, bitmapPadding); + + // Show the current page outlines to indicate that we can accept this drop + showOutlines(); + layout.setHover(true); + layout.onDragEnter(); + layout.visualizeDropLocation(null, mDragOutline, x, y, 1, 1); + + return true; + } + case DragEvent.ACTION_DRAG_LOCATION: + // Visualize the drop location + layout.visualizeDropLocation(null, mDragOutline, x, y, 1, 1); + return true; + case DragEvent.ACTION_DROP: { + // Try and add any shortcuts + int newDropCount = 0; + 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.getItem(index).getIntent(); + Object info = model.infoFromShortcutIntent(mContext, intent, data.getIcon()); + onDropExternal(x, y, info, layout); + } 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, mCurrentPage, 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.setHover(false); + 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 + * coordinate space. The argument xy is modified with the return result. + * + */ + void mapPointFromSelfToChild(View v, float[] xy) { + mapPointFromSelfToChild(v, xy, null); + } + + /* + * + * Convert the 2D coordinate xy from the parent View's coordinate space to this CellLayout's + * coordinate space. The argument xy is modified with the return result. + * + * if cachedInverseMatrix is not null, this method will just use that matrix instead of + * computing it itself; we use this to avoid redundant matrix inversions in + * findMatchingPageForDragOver + * + */ + void mapPointFromSelfToChild(View v, float[] xy, Matrix cachedInverseMatrix) { + if (cachedInverseMatrix == null) { + v.getMatrix().invert(mTempInverseMatrix); + cachedInverseMatrix = mTempInverseMatrix; + } + xy[0] = xy[0] + mScrollX - v.getLeft(); + xy[1] = xy[1] + mScrollY - v.getTop(); + cachedInverseMatrix.mapPoints(xy); + } + + /* + * + * Convert the 2D coordinate xy from this CellLayout's coordinate space to + * the parent View's coordinate space. The argument xy is modified with the return result. + * + */ + void mapPointFromChildToSelf(View v, float[] xy) { + v.getMatrix().mapPoints(xy); + xy[0] -= (mScrollX - v.getLeft()); + xy[1] -= (mScrollY - v.getTop()); + } + + static private float squaredDistance(float[] point1, float[] point2) { + float distanceX = point1[0] - point2[0]; + float distanceY = point2[1] - point2[1]; + return distanceX * distanceX + distanceY * distanceY; + } + + /* + * + * Returns true if the passed CellLayout cl overlaps with dragView + * + */ + boolean overlaps(CellLayout cl, DragView dragView, + int dragViewX, int dragViewY, Matrix cachedInverseMatrix) { + // Transform the coordinates of the item being dragged to the CellLayout's coordinates + final float[] draggedItemTopLeft = mTempDragCoordinates; + draggedItemTopLeft[0] = dragViewX + dragView.getScaledDragRegionXOffset(); + draggedItemTopLeft[1] = dragViewY + dragView.getScaledDragRegionYOffset(); + final float[] draggedItemBottomRight = mTempDragBottomRightCoordinates; + draggedItemBottomRight[0] = draggedItemTopLeft[0] + dragView.getScaledDragRegionWidth(); + draggedItemBottomRight[1] = draggedItemTopLeft[1] + dragView.getScaledDragRegionHeight(); + + // Transform the dragged item's top left coordinates + // to the CellLayout's local coordinates + mapPointFromSelfToChild(cl, draggedItemTopLeft, cachedInverseMatrix); + float overlapRegionLeft = Math.max(0f, draggedItemTopLeft[0]); + float overlapRegionTop = Math.max(0f, draggedItemTopLeft[1]); + + if (overlapRegionLeft <= cl.getWidth() && overlapRegionTop >= 0) { + // Transform the dragged item's bottom right coordinates + // to the CellLayout's local coordinates + mapPointFromSelfToChild(cl, draggedItemBottomRight, cachedInverseMatrix); + float overlapRegionRight = Math.min(cl.getWidth(), draggedItemBottomRight[0]); + float overlapRegionBottom = Math.min(cl.getHeight(), draggedItemBottomRight[1]); + + if (overlapRegionRight >= 0 && overlapRegionBottom <= cl.getHeight()) { + float overlap = (overlapRegionRight - overlapRegionLeft) * + (overlapRegionBottom - overlapRegionTop); + if (overlap > 0) { + return true; + } + } + } + return false; + } + + /* + * + * This method returns the CellLayout that is currently being dragged to. In order to drag + * to a CellLayout, either the touch point must be directly over the CellLayout, or as a second + * strategy, we see if the dragView is overlapping any CellLayout and choose the closest one + * + * Return null if no CellLayout is currently being dragged over + * + */ + private CellLayout findMatchingPageForDragOver( + DragView dragView, int originX, int originY, int offsetX, int offsetY) { + // We loop through all the screens (ie CellLayouts) and see which ones overlap + // with the item being dragged and then choose the one that's closest to the touch point + final int screenCount = getChildCount(); + CellLayout bestMatchingScreen = null; + float smallestDistSoFar = Float.MAX_VALUE; + + for (int i = 0; i < screenCount; i++) { + CellLayout cl = (CellLayout)getChildAt(i); + + final float[] touchXy = mTempTouchCoordinates; + touchXy[0] = originX + offsetX; + touchXy[1] = originY + offsetY; + + // Transform the touch coordinates to the CellLayout's local coordinates + // If the touch point is within the bounds of the cell layout, we can return immediately + cl.getMatrix().invert(mTempInverseMatrix); + mapPointFromSelfToChild(cl, touchXy, mTempInverseMatrix); + + if (touchXy[0] >= 0 && touchXy[0] <= cl.getWidth() && + touchXy[1] >= 0 && touchXy[1] <= cl.getHeight()) { + return cl; + } + + if (overlaps(cl, dragView, originX, originY, mTempInverseMatrix)) { + // Get the center of the cell layout in screen coordinates + final float[] cellLayoutCenter = mTempCellLayoutCenterCoordinates; + cellLayoutCenter[0] = cl.getWidth()/2; + cellLayoutCenter[1] = cl.getHeight()/2; + mapPointFromChildToSelf(cl, cellLayoutCenter); + + touchXy[0] = originX + offsetX; + touchXy[1] = originY + offsetY; + + // Calculate the distance between the center of the CellLayout + // and the touch point + float dist = squaredDistance(touchXy, cellLayoutCenter); + + if (dist < smallestDistSoFar) { + smallestDistSoFar = dist; + bestMatchingScreen = cl; + } + } + } + return bestMatchingScreen; } public void onDragOver(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo) { + // When touch is inside the scroll area, skip dragOver actions for the current screen + if (!mInScrollArea) { + CellLayout layout; + int originX = x - xOffset; + int originY = y - yOffset; + if (mIsSmall || mIsInUnshrinkAnimation) { + layout = findMatchingPageForDragOver( + dragView, originX, originY, xOffset, yOffset); + + if (layout != mDragTargetLayout) { + if (mDragTargetLayout != null) { + mDragTargetLayout.setHover(false); + } + mDragTargetLayout = layout; + if (mDragTargetLayout != null && mDragTargetLayout.getAcceptsDrops()) { + mDragTargetLayout.setHover(true); + } + } + } else { + layout = getCurrentDropLayout(); + + final ItemInfo item = (ItemInfo)dragInfo; + if (dragInfo instanceof LauncherAppWidgetInfo) { + LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo)dragInfo; + + if (widgetInfo.spanX == -1) { + // Calculate the grid spans needed to fit this widget + int[] spans = layout.rectToCell( + widgetInfo.minWidth, widgetInfo.minHeight, null); + item.spanX = spans[0]; + item.spanY = spans[1]; + } + } + + if (source instanceof AllAppsPagedView) { + // This is a hack to fix the point used to determine which cell an icon from + // the all apps screen is over + if (item != null && item.spanX == 1 && layout != null) { + int dragRegionLeft = (dragView.getWidth() - layout.getCellWidth()) / 2; + + originX += dragRegionLeft - dragView.getDragRegionLeft(); + if (dragView.getDragRegionWidth() != layout.getCellWidth()) { + dragView.setDragRegion(dragView.getDragRegionLeft(), + dragView.getDragRegionTop(), + layout.getCellWidth(), + dragView.getDragRegionHeight()); + } + } + } + + if (layout != mDragTargetLayout) { + if (mDragTargetLayout != null) { + mDragTargetLayout.onDragExit(); + } + layout.onDragEnter(); + mDragTargetLayout = layout; + } + + // only visualize the drop locations for moving icons within the home screen on + // tablet on phone, we also visualize icons dragged in from All Apps + if ((!LauncherApplication.isScreenXLarge() || source == this) + && mDragTargetLayout != null) { + final View child = (mDragInfo == null) ? null : mDragInfo.cell; + int localOriginX = originX - (mDragTargetLayout.getLeft() - mScrollX); + int localOriginY = originY - (mDragTargetLayout.getTop() - mScrollY); + mDragTargetLayout.visualizeDropLocation(child, mDragOutline, + localOriginX, localOriginY, item.spanX, item.spanY); + } + } + } } - public void onDragExit(DragSource source, int x, int y, int xOffset, int yOffset, - DragView dragView, Object dragInfo) { - clearVacantCache(); + public void onDragExit(DragSource source, int x, int y, int xOffset, + int yOffset, DragView dragView, Object dragInfo) { + if (mDragTargetLayout != null) { + mDragTargetLayout.onDragExit(); + } + if (!mIsPageMoving) { + hideOutlines(); + } + clearAllHovers(); } - private void onDropExternal(int x, int y, Object dragInfo, CellLayout cellLayout) { + private void onDropExternal(int x, int y, Object dragInfo, + CellLayout cellLayout) { onDropExternal(x, y, dragInfo, cellLayout, false); } - - private void onDropExternal(int x, int y, Object dragInfo, CellLayout cellLayout, - boolean insertAtFirst) { - // Drag from somewhere else + + @Override + public void getHitRect(Rect outRect) { + // We want the workspace to have the whole area of the display (it will find the correct + // cell layout to drop to in the existing drag/drop logic. + final Display d = mLauncher.getWindowManager().getDefaultDisplay(); + outRect.set(0, 0, d.getWidth(), d.getHeight()); + } + + /** + * Add the item specified by dragInfo to the given layout. + * This is basically the equivalent of onDropExternal, except it's not initiated + * by drag and drop. + * @return true if successful + */ + public boolean addExternalItemToScreen(Object dragInfo, View layout) { + CellLayout cl = (CellLayout) layout; ItemInfo info = (ItemInfo) dragInfo; - View view; + if (cl.findCellForSpan(mTempEstimate, info.spanX, info.spanY)) { + onDropExternal(-1, -1, dragInfo, cl, false); + return true; + } + mLauncher.showOutOfSpaceMessage(); + return false; + } - switch (info.itemType) { - case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: - case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: - if (info.container == NO_ID && info instanceof ApplicationInfo) { - // Came from all apps -- make a copy - info = new ShortcutInfo((ApplicationInfo)info); + // Drag from somewhere else + // NOTE: This can also be called when we are outside of a drag event, when we want + // to add an item to one of the workspace screens. + private void onDropExternal(int x, int y, Object dragInfo, + CellLayout cellLayout, boolean insertAtFirst) { + int screen = indexOfChild(cellLayout); + if (dragInfo instanceof PendingAddItemInfo) { + PendingAddItemInfo info = (PendingAddItemInfo) dragInfo; + // When dragging and dropping from customization tray, we deal with creating + // widgets/shortcuts/folders in a slightly different way + // Only set touchXY if you are supporting spring loaded adding of items + int[] touchXY = new int[2]; + touchXY[0] = x; + touchXY[1] = y; + switch (info.itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: + mLauncher.addAppWidgetFromDrop((PendingAddWidgetInfo) info, screen, touchXY); + break; + case LauncherSettings.Favorites.ITEM_TYPE_LIVE_FOLDER: + mLauncher.addLiveFolderFromDrop(info.componentName, screen, touchXY); + break; + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + mLauncher.processShortcutFromDrop(info.componentName, screen, touchXY); + break; + default: + throw new IllegalStateException("Unknown item type: " + info.itemType); } - view = mLauncher.createShortcut(R.layout.application, cellLayout, (ShortcutInfo)info); - break; - case LauncherSettings.Favorites.ITEM_TYPE_USER_FOLDER: - view = FolderIcon.fromXml(R.layout.folder_icon, mLauncher, - (ViewGroup) getChildAt(mCurrentScreen), ((UserFolderInfo) info)); - break; - default: - throw new IllegalStateException("Unknown item type: " + info.itemType); - } + cellLayout.onDragExit(); + cellLayout.animateDrop(); + } else { + // This is for other drag/drop cases, like dragging from All Apps + ItemInfo info = (ItemInfo) dragInfo; - cellLayout.addView(view, insertAtFirst ? 0 : -1); - view.setHapticFeedbackEnabled(false); - view.setOnLongClickListener(mLongClickListener); - if (view instanceof DropTarget) { - mDragController.addDropTarget((DropTarget) view); - } + View view = null; + + switch (info.itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + if (info.container == NO_ID && info instanceof ApplicationInfo) { + // Came from all apps -- make a copy + info = new ShortcutInfo((ApplicationInfo) info); + } + view = mLauncher.createShortcut(R.layout.application, cellLayout, + (ShortcutInfo) info); + break; + case LauncherSettings.Favorites.ITEM_TYPE_USER_FOLDER: + view = FolderIcon.fromXml(R.layout.folder_icon, mLauncher, + cellLayout, (UserFolderInfo) info, mIconCache); + break; + default: + throw new IllegalStateException("Unknown item type: " + info.itemType); + } - mTargetCell = estimateDropCell(x, y, 1, 1, view, cellLayout, mTargetCell); - cellLayout.onDropChild(view, mTargetCell); - CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams(); + mTargetCell = new int[2]; + if (x != -1 && y != -1) { + // when dragging and dropping, just find the closest free spot + cellLayout.findNearestVacantArea(x, y, 1, 1, mTargetCell); + } else { + cellLayout.findCellForSpan(mTargetCell, 1, 1); + } + addInScreen(view, indexOfChild(cellLayout), mTargetCell[0], + mTargetCell[1], info.spanX, info.spanY, insertAtFirst); + cellLayout.onDropChild(view); + cellLayout.animateDrop(); + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams(); - LauncherModel.addOrMoveItemInDatabase(mLauncher, info, - LauncherSettings.Favorites.CONTAINER_DESKTOP, mCurrentScreen, lp.cellX, lp.cellY); + LauncherModel.addOrMoveItemInDatabase(mLauncher, info, + LauncherSettings.Favorites.CONTAINER_DESKTOP, screen, + lp.cellX, lp.cellY); + } } - + /** * Return the current {@link CellLayout}, correctly picking the destination * screen while a scroll is in progress. */ private CellLayout getCurrentDropLayout() { - int index = mScroller.isFinished() ? mCurrentScreen : mNextScreen; + // if we're currently small, use findMatchingPageForDragOver instead + if (mIsSmall) return null; + int index = mScroller.isFinished() ? mCurrentPage : mNextPage; return (CellLayout) getChildAt(index); } /** - * {@inheritDoc} - */ - public boolean acceptDrop(DragSource source, int x, int y, - int xOffset, int yOffset, DragView dragView, Object dragInfo) { - final CellLayout layout = getCurrentDropLayout(); - final CellLayout.CellInfo cellInfo = mDragInfo; - final int spanX = cellInfo == null ? 1 : cellInfo.spanX; - final int spanY = cellInfo == null ? 1 : cellInfo.spanY; - - if (mVacantCache == null) { - final View ignoreView = cellInfo == null ? null : cellInfo.cell; - mVacantCache = layout.findAllVacantCells(null, ignoreView); - } - - return mVacantCache.findCellForSpan(mTempEstimate, spanX, spanY, false); - } - - /** - * {@inheritDoc} + * Return the current CellInfo describing our current drag; this method exists + * so that Launcher can sync this object with the correct info when the activity is created/ + * destroyed + * */ - public Rect estimateDropLocation(DragSource source, int x, int y, - int xOffset, int yOffset, DragView dragView, Object dragInfo, Rect recycle) { - final CellLayout layout = getCurrentDropLayout(); - - final CellLayout.CellInfo cellInfo = mDragInfo; - final int spanX = cellInfo == null ? 1 : cellInfo.spanX; - final int spanY = cellInfo == null ? 1 : cellInfo.spanY; - final View ignoreView = cellInfo == null ? null : cellInfo.cell; - - final Rect location = recycle != null ? recycle : new Rect(); - - // Find drop cell and convert into rectangle - int[] dropCell = estimateDropCell(x - xOffset, y - yOffset, - spanX, spanY, ignoreView, layout, mTempCell); - - if (dropCell == null) { - return null; - } - - layout.cellToPoint(dropCell[0], dropCell[1], mTempEstimate); - location.left = mTempEstimate[0]; - location.top = mTempEstimate[1]; - - layout.cellToPoint(dropCell[0] + spanX, dropCell[1] + spanY, mTempEstimate); - location.right = mTempEstimate[0]; - location.bottom = mTempEstimate[1]; - - return location; + public CellLayout.CellInfo getDragInfo() { + return mDragInfo; } /** * Calculate the nearest cell where the given object would be dropped. */ - private int[] estimateDropCell(int pixelX, int pixelY, + private int[] findNearestVacantArea(int pixelX, int pixelY, int spanX, int spanY, View ignoreView, CellLayout layout, int[] recycle) { - // Create vacant cell cache if none exists - if (mVacantCache == null) { - mVacantCache = layout.findAllVacantCells(null, ignoreView); - } + + int localPixelX = pixelX - (layout.getLeft() - mScrollX); + int localPixelY = pixelY - (layout.getTop() - mScrollY); // Find the best target drop location - return layout.findNearestVacantArea(pixelX, pixelY, - spanX, spanY, mVacantCache, recycle); + return layout.findNearestVacantArea( + localPixelX, localPixelY, spanX, spanY, ignoreView, recycle); + } + + /** + * Estimate the size that a child with the given dimensions will take in the current screen. + */ + void estimateChildSize(int minWidth, int minHeight, int[] result) { + ((CellLayout)getChildAt(mCurrentPage)).estimateChildSize(minWidth, minHeight, result); } - + void setLauncher(Launcher launcher) { mLauncher = launcher; } @@ -1217,61 +2150,90 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag } public void onDropCompleted(View target, boolean success) { - clearVacantCache(); - - if (success){ + if (success) { if (target != this && mDragInfo != null) { final CellLayout cellLayout = (CellLayout) getChildAt(mDragInfo.screen); cellLayout.removeView(mDragInfo.cell); if (mDragInfo.cell instanceof DropTarget) { mDragController.removeDropTarget((DropTarget)mDragInfo.cell); } - //final Object tag = mDragInfo.cell.getTag(); - } - } else { - if (mDragInfo != null) { - final CellLayout cellLayout = (CellLayout) getChildAt(mDragInfo.screen); - cellLayout.onDropAborted(mDragInfo.cell); + // final Object tag = mDragInfo.cell.getTag(); } + } else if (mDragInfo != null) { + ((CellLayout) getChildAt(mDragInfo.screen)).onDropChild(mDragInfo.cell); } + mDragOutline = null; mDragInfo = null; } + @Override + public void onDragViewVisible() { + ((View) mDragInfo.cell).setVisibility(View.GONE); + } + + public boolean isDropEnabled() { + return true; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + super.onRestoreInstanceState(state); + Launcher.setScreen(mCurrentPage); + } + + @Override public void scrollLeft() { - clearVacantCache(); - if (mScroller.isFinished()) { - if (mCurrentScreen > 0) snapToScreen(mCurrentScreen - 1); - } else { - if (mNextScreen > 0) snapToScreen(mNextScreen - 1); + if (!mIsSmall && !mIsInUnshrinkAnimation) { + super.scrollLeft(); } } + @Override public void scrollRight() { - clearVacantCache(); - if (mScroller.isFinished()) { - if (mCurrentScreen < getChildCount() -1) snapToScreen(mCurrentScreen + 1); - } else { - if (mNextScreen < getChildCount() -1) snapToScreen(mNextScreen + 1); + if (!mIsSmall && !mIsInUnshrinkAnimation) { + super.scrollRight(); } } - public int getScreenForView(View v) { - int result = -1; - if (v != null) { - ViewParent vp = v.getParent(); - int count = getChildCount(); - for (int i = 0; i < count; i++) { - if (vp == getChildAt(i)) { - return i; + @Override + public void onEnterScrollArea(int direction) { + if (!mIsSmall && !mIsInUnshrinkAnimation) { + mInScrollArea = true; + mPendingScrollDirection = direction; + + final int page = mCurrentPage + (direction == DragController.SCROLL_LEFT ? -1 : 1); + final CellLayout layout = (CellLayout) getChildAt(page); + + if (layout != null) { + layout.setHover(true); + + if (mDragTargetLayout != null) { + mDragTargetLayout.onDragExit(); + mDragTargetLayout = null; } } } - return result; + } + + private void clearAllHovers() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + ((CellLayout) getChildAt(i)).setHover(false); + } + } + + @Override + public void onExitScrollArea() { + if (mInScrollArea) { + mInScrollArea = false; + mPendingScrollDirection = DragController.SCROLL_NONE; + clearAllHovers(); + } } public Folder getFolderForTag(Object tag) { - int screenCount = getChildCount(); + final int screenCount = getChildCount(); for (int screen = 0; screen < screenCount; screen++) { CellLayout currentScreen = ((CellLayout) getChildAt(screen)); int count = currentScreen.getChildCount(); @@ -1280,7 +2242,7 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); if (lp.cellHSpan == 4 && lp.cellVSpan == 4 && child instanceof Folder) { Folder f = (Folder) child; - if (f.getInfo() == tag) { + if (f.getInfo() == tag && f.getInfo().opened) { return f; } } @@ -1304,23 +2266,9 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag return null; } - /** - * @return True is long presses are still allowed for the current touch - */ - public boolean allowLongPress() { - return mAllowLongPress; - } - - /** - * Set true to allow long-press events to be triggered, usually checked by - * {@link Launcher} to accept or block dpad-initiated long-presses. - */ - public void setAllowLongPress(boolean allowLongPress) { - mAllowLongPress = allowLongPress; - } void removeItems(final ArrayList<ApplicationInfo> apps) { - final int count = getChildCount(); + final int screenCount = getChildCount(); final PackageManager manager = getContext().getPackageManager(); final AppWidgetManager widgets = AppWidgetManager.getInstance(getContext()); @@ -1330,7 +2278,7 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag packageNames.add(apps.get(i).componentName.getPackageName()); } - for (int i = 0; i < count; i++) { + for (int i = 0; i < screenCount; i++) { final CellLayout layout = (CellLayout) getChildAt(i); // Avoid ANRs by treating each screen separately @@ -1338,17 +2286,17 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag public void run() { final ArrayList<View> childrenToRemove = new ArrayList<View>(); childrenToRemove.clear(); - + 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) { final ShortcutInfo info = (ShortcutInfo) tag; final Intent intent = info.intent; final ComponentName name = intent.getComponent(); - + if (Intent.ACTION_MAIN.equals(intent.getAction()) && name != null) { for (String packageName: packageNames) { if (packageName.equals(name.getPackageName())) { @@ -1363,12 +2311,12 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag final ArrayList<ShortcutInfo> toRemove = new ArrayList<ShortcutInfo>(1); final int contentsCount = contents.size(); boolean removedFromFolder = false; - + for (int k = 0; k < contentsCount; k++) { final ShortcutInfo appInfo = contents.get(k); final Intent intent = appInfo.intent; final ComponentName name = intent.getComponent(); - + if (Intent.ACTION_MAIN.equals(intent.getAction()) && name != null) { for (String packageName: packageNames) { if (packageName.equals(name.getPackageName())) { @@ -1379,11 +2327,12 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag } } } - + contents.removeAll(toRemove); if (removedFromFolder) { final Folder folder = getOpenFolder(); - if (folder != null) folder.notifyDataSetChanged(); + if (folder != null) + folder.notifyDataSetChanged(); } } else if (tag instanceof LiveFolderInfo) { final LiveFolderInfo info = (LiveFolderInfo) tag; @@ -1395,7 +2344,7 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag for (String packageName: packageNames) { if (packageName.equals(providerInfo.packageName)) { LauncherModel.deleteItemFromDatabase(mLauncher, info); - childrenToRemove.add(view); + childrenToRemove.add(view); } } } @@ -1407,13 +2356,13 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag for (String packageName: packageNames) { if (packageName.equals(provider.provider.getPackageName())) { LauncherModel.deleteItemFromDatabase(mLauncher, info); - childrenToRemove.add(view); + childrenToRemove.add(view); } } } } } - + childCount = childrenToRemove.size(); for (int j = 0; j < childCount; j++) { View child = childrenToRemove.get(j); @@ -1422,7 +2371,7 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag mDragController.removeDropTarget((DropTarget)child); } } - + if (childCount > 0) { layout.requestLayout(); layout.invalidate(); @@ -1433,10 +2382,8 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag } void updateShortcuts(ArrayList<ApplicationInfo> apps) { - final PackageManager pm = mLauncher.getPackageManager(); - - final int count = getChildCount(); - for (int i = 0; i < count; i++) { + final int screenCount = getChildCount(); + for (int i = 0; i < screenCount; i++) { final CellLayout layout = (CellLayout) getChildAt(i); int childCount = layout.getChildCount(); for (int j = 0; j < childCount; j++) { @@ -1452,7 +2399,7 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION && Intent.ACTION_MAIN.equals(intent.getAction()) && name != null) { final int appCount = apps.size(); - for (int k=0; k<appCount; k++) { + for (int k = 0; k < appCount; k++) { ApplicationInfo app = apps.get(k); if (app.componentName.equals(name)) { info.setIcon(mIconCache.getIcon(info.intent)); @@ -1468,48 +2415,29 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag } void moveToDefaultScreen(boolean animate) { - if (animate) { - snapToScreen(mDefaultScreen); + if (mIsSmall || mIsInUnshrinkAnimation) { + mLauncher.showWorkspace(animate, (CellLayout)getChildAt(mDefaultPage)); + } else if (animate) { + snapToPage(mDefaultPage); } else { - setCurrentScreen(mDefaultScreen); + setCurrentPage(mDefaultPage); } - getChildAt(mDefaultScreen).requestFocus(); + getChildAt(mDefaultPage).requestFocus(); } void setIndicators(Drawable previous, Drawable next) { mPreviousIndicator = previous; mNextIndicator = next; - previous.setLevel(mCurrentScreen); - next.setLevel(mCurrentScreen); + previous.setLevel(mCurrentPage); + next.setLevel(mCurrentPage); } - public static class SavedState extends BaseSavedState { - int currentScreen = -1; - - SavedState(Parcelable superState) { - super(superState); - } - - private SavedState(Parcel in) { - super(in); - currentScreen = in.readInt(); - } - - @Override - public void writeToParcel(Parcel out, int flags) { - super.writeToParcel(out, flags); - out.writeInt(currentScreen); - } - - public static final Parcelable.Creator<SavedState> CREATOR = - new Parcelable.Creator<SavedState>() { - public SavedState createFromParcel(Parcel in) { - return new SavedState(in); - } + @Override + public void syncPages() { + } - public SavedState[] newArray(int size) { - return new SavedState[size]; - } - }; + @Override + public void syncPageItems(int page) { } + } diff --git a/src/com/android/launcher2/allapps.rs b/src/com/android/launcher2/allapps.rs new file mode 100644 index 0000000..11851b8 --- /dev/null +++ b/src/com/android/launcher2/allapps.rs @@ -0,0 +1,385 @@ +#pragma version(1) + +#pragma rs java_package_name(com.android.launcher2) + +#include "rs_graphics.rsh" + +#define PI 3.14159f + +// Constants from Java +int COLUMNS_PER_PAGE_PORTRAIT; +int ROWS_PER_PAGE_PORTRAIT; +int COLUMNS_PER_PAGE_LANDSCAPE; +int ROWS_PER_PAGE_LANDSCAPE; + +int gIconCount; +int gSelectedIconIndex = -1; +rs_allocation gSelectedIconTexture; +rs_allocation gHomeButton; + +rs_program_fragment gPFTexNearest; +rs_program_fragment gPFTexMip; +rs_program_fragment gPFTexMipAlpha; +rs_program_vertex gPVCurve; +rs_program_store gPS; +rs_mesh gSMCell; + +rs_allocation *gIconIDs; +rs_allocation *gLabelIDs; + +typedef struct VpConsts { + rs_matrix4x4 Proj; + float4 Position; + float4 ScaleOffset; + float2 BendPos; + float2 ImgSize; +} VpConsts_t; +VpConsts_t *vpConstants; + +// Attraction to center values from page edge to page center. +static float g_AttractionTable[9] = {20.f, 20.f, 20.f, 10.f, -10.f, -20.f, -20.f, -20.f, -20.f}; +static float g_FrictionTable[9] = {10.f, 10.f, 11.f, 15.f, 15.f, 11.f, 10.f, 10.f, 10.f}; +static float g_PhysicsTableSize = 7; + +static float gZoomTarget; +float gTargetPos; +static float g_PosPage = 0.f; +static float g_PosVelocity = 0.f; +static float g_LastPositionX = 0.f; +static bool g_LastTouchDown = false; +static float g_DT; +static int g_PosMax; +static float g_Zoom = 0.f; +static float g_Animation = 1.f; +static float g_OldPosPage; +static float g_OldPosVelocity; +static float g_OldZoom; +static float g_MoveToTotalTime = 0.2f; +static float g_MoveToTime = 0.f; +static float g_MoveToOldPos = 0.f; + +static int g_Cols; +static int g_Rows; + +rs_allocation g_VPConstAlloc; + +// Drawing constants, should be parameters ====== +#define VIEW_ANGLE 1.28700222f + + +static void updateReadback() { + if ((g_OldPosPage != g_PosPage) || + (g_OldPosVelocity != g_PosVelocity) || + (g_OldZoom != g_Zoom)) { + + g_OldPosPage = g_PosPage; + g_OldPosVelocity = g_PosVelocity; + g_OldZoom = g_Zoom; + + int i[3]; + i[0] = g_PosPage * (1 << 16); + i[1] = g_PosVelocity * (1 << 16); + i[2] = g_OldZoom * (1 << 16); + rsSendToClientBlocking(1, &i[0], sizeof(i)); + } +} + +void init() { +} + +void move(float newPos) { + if (g_LastTouchDown) { + float dx = -(newPos - g_LastPositionX); + g_PosVelocity = 0; + g_PosPage += dx * 5.2f; + + float pmin = -0.49f; + float pmax = g_PosMax + 0.49f; + g_PosPage = clamp(g_PosPage, pmin, pmax); + } + g_LastTouchDown = true; + g_LastPositionX = newPos; + g_MoveToTime = 0; +} + +void moveTo(float targetPos) { + gTargetPos = targetPos; + g_MoveToTime = g_MoveToTotalTime; + g_PosVelocity = 0; + g_MoveToOldPos = g_PosPage; +} + +void setZoom(float z, /*bool*/ int animate) { + gZoomTarget = z; + if (gZoomTarget < 0.001f) { + gZoomTarget = 0; + } + if (!animate) { + g_Zoom = gZoomTarget; + } + updateReadback(); +} + +void fling(float newPos, float vel) { + move(newPos); + + g_LastTouchDown = false; + g_PosVelocity = -vel * 4; + float av = fabs(g_PosVelocity); + float minVel = 3.5f; + + minVel *= 1.f - (fabs(rsFrac(g_PosPage + 0.5f) - 0.5f) * 0.45f); + + if (av < minVel && av > 0.2f) { + if (g_PosVelocity > 0) { + g_PosVelocity = minVel; + } else { + g_PosVelocity = -minVel; + } + } + + if (g_PosPage <= 0) { + g_PosVelocity = max(0.f, g_PosVelocity); + } + if (g_PosPage > g_PosMax) { + g_PosVelocity = min(0.f, g_PosVelocity); + } +} + +// Interpolates values in the range 0..1 to a curve that eases in +// and out. +static float getInterpolation(float input) { + return (cos((input + 1) * PI) * 0.5f) + 0.5f; +} + + +static void updatePos() { + if (g_LastTouchDown) { + return; + } + + float tablePosNorm = rsFrac(g_PosPage + 0.5f); + float tablePosF = tablePosNorm * g_PhysicsTableSize; + int tablePosI = tablePosF; + float tablePosFrac = tablePosF - tablePosI; + float accel = mix(g_AttractionTable[tablePosI], + g_AttractionTable[tablePosI + 1], + tablePosFrac) * g_DT; + float friction = mix(g_FrictionTable[tablePosI], + g_FrictionTable[tablePosI + 1], + tablePosFrac) * g_DT; + + if (g_MoveToTime) { + // New position is old posiition + (total distance) * (interpolated time) + g_PosPage = g_MoveToOldPos + (gTargetPos - g_MoveToOldPos) * getInterpolation((g_MoveToTotalTime - g_MoveToTime) / g_MoveToTotalTime); + g_MoveToTime -= g_DT; + if (g_MoveToTime <= 0) { + g_MoveToTime = 0; + g_PosPage = gTargetPos; + } + return; + } + + // If our velocity is low OR acceleration is opposing it, apply it. + if (fabs(g_PosVelocity) < 4.0f || (g_PosVelocity * accel) < 0) { + g_PosVelocity += accel; + } + //RS_DEBUG(g_PosPage); + //RS_DEBUG(g_PosVelocity); + //RS_DEBUG(friction); + //RS_DEBUG(accel); + + // Normal physics + if (g_PosVelocity > 0) { + g_PosVelocity -= friction; + g_PosVelocity = max(g_PosVelocity, 0.f); + } else { + g_PosVelocity += friction; + g_PosVelocity = min(g_PosVelocity, 0.f); + } + + if ((friction > fabs(g_PosVelocity)) && (friction > fabs(accel))) { + // Special get back to center and overcome friction physics. + float t = tablePosNorm - 0.5f; + if (fabs(t) < (friction * g_DT)) { + // really close, just snap + g_PosPage = round(g_PosPage); + g_PosVelocity = 0; + } else { + if (t > 0) { + g_PosVelocity = -friction; + } else { + g_PosVelocity = friction; + } + } + } + + // Check for out of boundry conditions. + if (g_PosPage < 0 && g_PosVelocity < 0) { + float damp = 1.0f + (g_PosPage * 4); + damp = clamp(damp, 0.f, 0.9f); + g_PosVelocity *= damp; + } + if (g_PosPage > g_PosMax && g_PosVelocity > 0) { + float damp = 1.0f - ((g_PosPage - g_PosMax) * 4); + damp = clamp(damp, 0.f, 0.9f); + g_PosVelocity *= damp; + } + + g_PosPage += g_PosVelocity * g_DT; + g_PosPage = clamp(g_PosPage, -0.49f, g_PosMax + 0.49f); +} + +static void +draw_home_button() +{ + rsgBindTexture(gPFTexNearest, 0, gHomeButton); + + float w = rsgGetWidth(); + float h = rsgGetHeight(); + float tw = rsAllocationGetDimX(gHomeButton); + float th = rsAllocationGetDimY(gHomeButton); + + float x; + float y; + if (w > h) { + x = w - (tw * (1 - g_Animation)) + 20; + y = (h - th) * 0.5f; + } else { + x = (w - tw) / 2; + y = -g_Animation * th; + y -= 30; // move the house to the edge of the screen as it doesn't fill the texture. + } + + rsgDrawSpriteScreenspace(x, y, 0, tw, th); +} + +static void drawFrontGrid(float rowOffset, float p) +{ + float h = rsgGetHeight(); + float w = rsgGetWidth(); + + int intRowOffset = rowOffset; + float rowFrac = rowOffset - intRowOffset; + float colWidth = 120.f;//w / 4; + float rowHeight = colWidth + 25.f; + float yoff = 0.5f * h + 1.5f * rowHeight; + + int row, col; + int colCount = 4; + if (w > h) { + colCount = 6; + rowHeight -= 12.f; + yoff = 0.47f * h + 1.0f * rowHeight; + } + + int iconNum = (intRowOffset - 5) * colCount; + + rsgBindProgramVertex(gPVCurve); + + vpConstants->Position.z = p; + + for (row = -5; row < 15; row++) { + float y = yoff - ((-rowFrac + row) * rowHeight); + + for (col=0; col < colCount; col++) { + if (iconNum >= gIconCount) { + return; + } + + if (iconNum >= 0) { + float x = colWidth * col + (colWidth / 2); + vpConstants->Position.x = x + 0.2f; + + if (gSelectedIconIndex == iconNum && !p && rsIsObject(gSelectedIconTexture)) { + rsgBindProgramFragment(gPFTexNearest); + rsgBindTexture(gPFTexNearest, 0, gSelectedIconTexture); + vpConstants->ImgSize.x = rsAllocationGetDimX(gSelectedIconTexture); + vpConstants->ImgSize.y = rsAllocationGetDimY(gSelectedIconTexture); + vpConstants->Position.y = y - (rsAllocationGetDimY(gSelectedIconTexture) + - rsAllocationGetDimY(gIconIDs[iconNum])) * 0.5f; + rsAllocationMarkDirty(g_VPConstAlloc); + rsgDrawMesh(gSMCell); + } + + rsgBindProgramFragment(gPFTexMip); + vpConstants->ImgSize.x = rsAllocationGetDimX(gIconIDs[iconNum]); + vpConstants->ImgSize.y = rsAllocationGetDimY(gIconIDs[iconNum]); + vpConstants->Position.y = y - 0.2f; + rsAllocationMarkDirty(g_VPConstAlloc); + rsgBindTexture(gPFTexMip, 0, gIconIDs[iconNum]); + rsgDrawMesh(gSMCell); + + rsgBindProgramFragment(gPFTexMipAlpha); + vpConstants->ImgSize.x = rsAllocationGetDimX(gLabelIDs[iconNum]); + vpConstants->ImgSize.y = rsAllocationGetDimY(gLabelIDs[iconNum]); + vpConstants->Position.y = y - 64.f - 0.2f; + rsAllocationMarkDirty(g_VPConstAlloc); + rsgBindTexture(gPFTexMipAlpha, 0, gLabelIDs[iconNum]); + rsgDrawMesh(gSMCell); + } + iconNum++; + } + } +} + + +int root() +{ + // Compute dt in seconds. + // physics may break if DT is large. + g_DT = min(rsGetDt(), 0.1f); + g_VPConstAlloc = rsGetAllocation(vpConstants); + + if (g_Zoom != gZoomTarget) { + float dz = g_DT * 1.7f; + if (gZoomTarget < 0.5f) { + dz = -dz; + } + if (fabs(g_Zoom - gZoomTarget) < fabs(dz)) { + g_Zoom = gZoomTarget; + } else { + g_Zoom += dz; + } + updateReadback(); + } + g_Animation = pow(1.f - g_Zoom, 3.f); + + // Set clear value to dim the background based on the zoom position. + if ((g_Zoom < 0.001f) && (gZoomTarget < 0.001f)) { + rsgClearColor(0.0f, 0.0f, 0.0f, 0.0f); + // When we're zoomed out and not tracking motion events, reset the pos to 0. + if (!g_LastTouchDown) { + g_PosPage = 0; + } + return 0; + } else { + rsgClearColor(0.0f, 0.0f, 0.0f, g_Zoom); + } + + rsgBindProgramStore(gPS); + + // icons & labels + if (rsgGetWidth() > rsgGetHeight()) { + g_Cols = COLUMNS_PER_PAGE_LANDSCAPE; + g_Rows = ROWS_PER_PAGE_LANDSCAPE; + } else { + g_Cols = COLUMNS_PER_PAGE_PORTRAIT; + g_Rows = ROWS_PER_PAGE_PORTRAIT; + } + + g_PosMax = ((gIconCount + (g_Cols-1)) / g_Cols) - g_Rows; + if (g_PosMax < 0) g_PosMax = 0; + + updatePos(); + updateReadback(); + + // Draw the icons ======================================== + drawFrontGrid(g_PosPage, g_Animation); + + rsgBindProgramFragment(gPFTexNearest); + draw_home_button(); + return (g_PosVelocity != 0) || rsFrac(g_PosPage) || g_Zoom != gZoomTarget || (g_MoveToTime != 0); +} + + |