summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWinson Chung <winsonc@google.com>2010-08-09 13:37:56 -0700
committerWinson Chung <winsonc@google.com>2010-08-16 15:50:40 -0700
commit321e9ee68848d9e782fd557f69cc070308ffbc9c (patch)
tree0b3d38f48a16e16147b18da62e1f6d32bd3089c6
parentf0d03e4e7fd8640b6b50e163c4f555d03b874ca6 (diff)
downloadandroid_packages_apps_Trebuchet-321e9ee68848d9e782fd557f69cc070308ffbc9c.tar.gz
android_packages_apps_Trebuchet-321e9ee68848d9e782fd557f69cc070308ffbc9c.tar.bz2
android_packages_apps_Trebuchet-321e9ee68848d9e782fd557f69cc070308ffbc9c.zip
Creating generic Workspace/CellLayout for paged views in Launcher.
Adding SimpleWorkspace, SimpleCellLayout and an AllApps implementation of the SimpleWorkspace. Making SimpleWorkspace support content with smaller dimensions than the workspace dimensions itself. Temporary change to AllApps tabs styling until we get new assets for tabs in general. Change-Id: Ibe3c56603223853d232816b6695e4ddd757857ba
-rw-r--r--res/layout-xlarge/all_apps_paged_view_application.xml29
-rw-r--r--res/layout-xlarge/all_apps_tabbed.xml39
-rw-r--r--res/values/attrs.xml9
-rw-r--r--src/com/android/launcher2/AllAppsPagedView.java352
-rw-r--r--src/com/android/launcher2/AllAppsTabbed.java68
-rw-r--r--src/com/android/launcher2/PagedView.java788
-rw-r--r--src/com/android/launcher2/PagedViewCellLayout.java447
7 files changed, 1683 insertions, 49 deletions
diff --git a/res/layout-xlarge/all_apps_paged_view_application.xml b/res/layout-xlarge/all_apps_paged_view_application.xml
new file mode 100644
index 000000000..98c27377d
--- /dev/null
+++ b/res/layout-xlarge/all_apps_paged_view_application.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/name"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center_horizontal"
+
+ android:textColor="#FFFFFFFF"
+ android:shadowColor="#FF000000"
+ android:shadowDx="0.0"
+ android:shadowDy="1.0"
+
+ android:maxLines="2"
+ android:fadingEdge="horizontal" />
diff --git a/res/layout-xlarge/all_apps_tabbed.xml b/res/layout-xlarge/all_apps_tabbed.xml
index 0842fd085..dbe192ce5 100644
--- a/res/layout-xlarge/all_apps_tabbed.xml
+++ b/res/layout-xlarge/all_apps_tabbed.xml
@@ -13,44 +13,31 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<com.android.launcher2.AllAppsTabbed xmlns:android="http://schemas.android.com/apk/res/android">
+<com.android.launcher2.AllAppsTabbed
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:background="#30000000">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TabWidget
android:id="@android:id/tabs"
- android:layout_width="match_parent"
- android:layout_height="wrap_content" />
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:tabStripEnabled="false"
+ android:paddingBottom="10dp" />
<FrameLayout
android:id="@android:id/tabcontent"
android:layout_width="match_parent"
android:layout_height="match_parent">
-
- <com.android.launcher2.AllApps2D
- android:id="@+id/all_apps_2d"
+ <com.android.launcher2.AllAppsPagedView
+ android:id="@+id/all_apps_paged_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:padding="2dip">
- <GridView android:id="@+id/all_apps_2d_grid"
- android:tag="all_apps_2d_grid"
- android:scrollbars="none"
- android:drawSelectorOnTop="false"
- android:listSelector="@drawable/grid_selector"
- android:verticalSpacing="10dip"
- android:numColumns="8"
- android:fadingEdgeLength="0dip"
- android:cacheColorHint="#00000000"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_alignParentBottom="true"
- android:layout_marginBottom="8dip"
- android:layout_marginTop="8dip"
- android:nextFocusDown="@+id/all_apps_2d_home"
- android:nextFocusUp="@null"
- android:nextFocusLeft="@null"
- android:nextFocusRight="@null" />
- </com.android.launcher2.AllApps2D>
+ cellCountX="8"
+ cellCountY="4">
+ </com.android.launcher2.AllAppsPagedView>
</FrameLayout>
</LinearLayout>
</com.android.launcher2.AllAppsTabbed>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index be2728879..09fb0dace 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -55,6 +55,15 @@
<attr name="yAxisEndPadding" format="dimension" />
</declare-styleable>
+ <!-- PagedView specific attributes. These attributes are used to customize
+ a PagedView view in XML files. -->
+ <declare-styleable name="PagedView">
+ <!-- The number of horizontal cells in a page -->
+ <attr name="cellCountX" />
+ <!-- The number of vertical cells in a page -->
+ <attr name="cellCountY" />
+ </declare-styleable>
+
<!-- DeleteZone specific attributes. These attributes are used to customize
a DeleteZone view in XML files. -->
<declare-styleable name="DeleteZone">
diff --git a/src/com/android/launcher2/AllAppsPagedView.java b/src/com/android/launcher2/AllAppsPagedView.java
new file mode 100644
index 000000000..e0d248eb9
--- /dev/null
+++ b/src/com/android/launcher2/AllAppsPagedView.java
@@ -0,0 +1,352 @@
+/*
+ * 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 android.content.ComponentName;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Animation.AnimationListener;
+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,
+ PagedViewCellLayout.DimmedBitmapSetupListener {
+
+ private static final String TAG = "AllAppsPagedView";
+ private static final boolean DEBUG = false;
+
+ 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 int mCellCountX;
+ private int mCellCountY;
+
+ private final LayoutInflater mInflater;
+
+ 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
+ public void setLauncher(Launcher launcher) {
+ mLauncher = launcher;
+ }
+
+ @Override
+ public void setDragController(DragController dragger) {
+ mDragController = dragger;
+ }
+
+ public void setAppFilter(int filterType) {
+ mAppFilter = filterType;
+ mFilteredApps = rebuildFilteredApps(mApps);
+ setCurrentScreen(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;
+ } 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) {
+ PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i);
+ if (layout.indexOfChild(v) > -1) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public void onClick(View v) {
+ int childIndex = getChildIndexForGrandChild(v);
+ if (childIndex == getCurrentScreen()) {
+ final ApplicationInfo app = (ApplicationInfo) v.getTag();
+
+ AlphaAnimation anim = new AlphaAnimation(1.0f, 0.65f);
+ anim.setDuration(100);
+ anim.setFillAfter(true);
+ anim.setRepeatMode(AlphaAnimation.REVERSE);
+ anim.setRepeatCount(1);
+ anim.setAnimationListener(new AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {}
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ mLauncher.startActivitySafely(app.intent, app);
+ }
+ @Override
+ public void onAnimationEnd(Animation animation) {}
+ });
+ v.startAnimation(anim);
+ }
+ }
+
+ @Override
+ public boolean onLongClick(View v) {
+ if (!v.isInTouchMode()) {
+ return false;
+ }
+
+ ApplicationInfo app = (ApplicationInfo) v.getTag();
+ app = new ApplicationInfo(app);
+
+ mDragController.startDrag(v, this, app, DragController.DRAG_ACTION_COPY);
+ mLauncher.closeAllApps(true);
+ return true;
+ }
+
+ @Override
+ public void onDropCompleted(View target, boolean success) {
+ // do nothing
+ }
+
+ @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, new Comparator<ApplicationInfo>() {
+ @Override
+ public int compare(ApplicationInfo object1, ApplicationInfo object2) {
+ return object1.title.toString().compareTo(object2.title.toString());
+ }
+ });
+ mFilteredApps = rebuildFilteredApps(mApps);
+ invalidatePageData();
+ }
+
+ @Override
+ public void addApps(ArrayList<ApplicationInfo> list) {
+ // TODO: we need to add it in place, in alphabetical order
+ mApps.addAll(list);
+ mFilteredApps.addAll(rebuildFilteredApps(list));
+ invalidatePageData();
+ }
+
+ @Override
+ public void removeApps(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) {
+ int removeIndex = findAppByComponent(mApps, list.get(i));
+ if (removeIndex > -1) {
+ mApps.remove(removeIndex);
+ }
+ }
+ mFilteredApps = rebuildFilteredApps(list);
+ invalidatePageData();
+ }
+
+ @Override
+ public void updateApps(ArrayList<ApplicationInfo> list) {
+ removeApps(list);
+ addApps(list);
+ }
+
+ 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
+ int numPages = (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.setDimmedBitmapSetupListener(this);
+ addView(layout);
+ }
+
+ // bound the current page
+ setCurrentScreen(Math.max(0, Math.min(numPages - 1, getCurrentScreen())));
+ }
+
+ @Override
+ public void syncPageItems(int page) {
+ // 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, mFilteredApps.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) {
+ ApplicationInfo info = mFilteredApps.get(i);
+ TextView text = (TextView) mInflater.inflate(R.layout.all_apps_paged_view_application, layout, false);
+ text.setCompoundDrawablesWithIntrinsicBounds(null,
+ new BitmapDrawable(info.iconBitmap), null, null);
+ text.setText(info.title);
+ text.setTag(info);
+ text.setOnClickListener(this);
+ text.setOnLongClickListener(this);
+
+ int index = i - startIndex;
+ layout.addViewToCellLayout(text, index, i,
+ new PagedViewCellLayout.LayoutParams(index % mCellCountX, index / mCellCountX, 1, 1));
+ }
+ }
+
+ @Override
+ public void onPreUpdateDimmedBitmap(PagedViewCellLayout layout) {
+ // disable all children text for now
+ final int childCount = layout.getChildCount();
+ for (int i = 0; i < childCount; ++i) {
+ TextView text = (TextView) layout.getChildAt(i);
+ text.setText("");
+ }
+ }
+ @Override
+ public void onPostUpdateDimmedBitmap(PagedViewCellLayout layout) {
+ // re-enable all children text
+ final int childCount = layout.getChildCount();
+ for (int i = 0; i < childCount; ++i) {
+ TextView text = (TextView) layout.getChildAt(i);
+ final ApplicationInfo info = (ApplicationInfo) text.getTag();
+ text.setText(info.title);
+ }
+ }
+}
diff --git a/src/com/android/launcher2/AllAppsTabbed.java b/src/com/android/launcher2/AllAppsTabbed.java
index 7dbb1a5fa..0470cee3e 100644
--- a/src/com/android/launcher2/AllAppsTabbed.java
+++ b/src/com/android/launcher2/AllAppsTabbed.java
@@ -16,16 +16,20 @@
package com.android.launcher2;
-import com.android.launcher.R;
+import java.util.ArrayList;
import android.content.Context;
import android.content.res.Resources;
+import android.graphics.Color;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
+import android.widget.RelativeLayout;
import android.widget.TabHost;
+import android.widget.TabWidget;
+import android.widget.TextView;
-import java.util.ArrayList;
+import com.android.launcher.R;
/**
* Implements a tabbed version of AllApps2D.
@@ -39,7 +43,7 @@ public class AllAppsTabbed extends TabHost implements AllAppsView {
private static final String TAG_GAMES = "GAMES";
private static final String TAG_DOWNLOADED = "DOWNLOADED";
- private AllApps2D mAllApps2D;
+ private AllAppsPagedView mAllApps;
private Context mContext;
public AllAppsTabbed(Context context, AttributeSet attrs) {
@@ -49,18 +53,20 @@ public class AllAppsTabbed extends TabHost implements AllAppsView {
@Override
protected void onFinishInflate() {
+ // setup the tab host
+ setup();
+
try {
- mAllApps2D = (AllApps2D)findViewById(R.id.all_apps_2d);
- if (mAllApps2D == null) throw new Resources.NotFoundException();
+ 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");
}
- setup();
- // This lets us share the same view between all tabs
+ // share the same AllApps workspace across all the tabs
TabContentFactory contentFactory = new TabContentFactory() {
public View createTabContent(String tag) {
- return mAllApps2D;
+ return mAllApps;
}
};
@@ -76,51 +82,67 @@ public class AllAppsTabbed extends TabHost implements AllAppsView {
label = mContext.getString(R.string.all_apps_tab_downloaded);
addTab(newTabSpec(TAG_DOWNLOADED).setIndicator(label).setContent(contentFactory));
+ // TEMP: just styling the tab widget to be a bit nicer until we get the actual
+ // new assets
+ TabWidget tabWidget = getTabWidget();
+ for (int i = 0; i < tabWidget.getChildCount(); ++i) {
+ RelativeLayout tab = (RelativeLayout) tabWidget.getChildTabViewAt(i);
+ TextView text = (TextView) tab.getChildAt(1);
+ text.setTextSize(20.0f);
+ text.setPadding(20, 0, 20, 0);
+ text.setShadowLayer(1.0f, 0.0f, 1.0f, Color.BLACK);
+ tab.setBackgroundDrawable(null);
+ }
+
setOnTabChangedListener(new OnTabChangeListener() {
public void onTabChanged(String tabId) {
String tag = getCurrentTabTag();
if (tag == TAG_ALL) {
- mAllApps2D.filterApps(AllApps2D.AppType.ALL);
+ mAllApps.setAppFilter(AllAppsPagedView.ALL_APPS_FLAG);
} else if (tag == TAG_APPS) {
- mAllApps2D.filterApps(AllApps2D.AppType.APP);
+ mAllApps.setAppFilter(ApplicationInfo.APP_FLAG);
} else if (tag == TAG_GAMES) {
- mAllApps2D.filterApps(AllApps2D.AppType.GAME);
+ mAllApps.setAppFilter(ApplicationInfo.GAME_FLAG);
} else if (tag == TAG_DOWNLOADED) {
- mAllApps2D.filterApps(AllApps2D.AppType.DOWNLOADED);
+ mAllApps.setAppFilter(ApplicationInfo.DOWNLOADED_FLAG);
}
}
});
setCurrentTab(0);
+
+ // 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) {
- mAllApps2D.setLauncher(launcher);
+ mAllApps.setLauncher(launcher);
}
@Override
public void setDragController(DragController dragger) {
- mAllApps2D.setDragController(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);
- mAllApps2D.zoom(zoom, animate);
+ mAllApps.zoom(zoom, animate);
}
@Override
public void setVisibility(int visibility) {
super.setVisibility(visibility);
float zoom = visibility == View.VISIBLE ? 1.0f : 0.0f;
- mAllApps2D.zoom(zoom, false);
+ mAllApps.zoom(zoom, false);
}
@Override
public boolean isVisible() {
- return mAllApps2D.isVisible();
+ return mAllApps.isVisible();
}
@Override
@@ -130,31 +152,31 @@ public class AllAppsTabbed extends TabHost implements AllAppsView {
@Override
public void setApps(ArrayList<ApplicationInfo> list) {
- mAllApps2D.setApps(list);
+ mAllApps.setApps(list);
}
@Override
public void addApps(ArrayList<ApplicationInfo> list) {
- mAllApps2D.addApps(list);
+ mAllApps.addApps(list);
}
@Override
public void removeApps(ArrayList<ApplicationInfo> list) {
- mAllApps2D.removeApps(list);
+ mAllApps.removeApps(list);
}
@Override
public void updateApps(ArrayList<ApplicationInfo> list) {
- mAllApps2D.updateApps(list);
+ mAllApps.updateApps(list);
}
@Override
public void dumpState() {
- mAllApps2D.dumpState();
+ mAllApps.dumpState();
}
@Override
public void surrender() {
- mAllApps2D.surrender();
+ mAllApps.surrender();
}
}
diff --git a/src/com/android/launcher2/PagedView.java b/src/com/android/launcher2/PagedView.java
new file mode 100644
index 000000000..26805e0b5
--- /dev/null
+++ b/src/com/android/launcher2/PagedView.java
@@ -0,0 +1,788 @@
+/*
+ * 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 android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+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.widget.Scroller;
+
+/**
+ * An abstraction of the original Workspace which supports browsing through a
+ * sequential list of "pages" (or PagedViewCellLayouts).
+ */
+public abstract class PagedView extends ViewGroup {
+ private static final String TAG = "PagedView";
+ 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 = 500;
+
+ // the min drag distance for a fling to register, to prevent random screen shifts
+ private static final int MIN_LENGTH_FOR_FLING = 50;
+
+ private boolean mFirstLayout = true;
+
+ private int mCurrentScreen;
+ private int mNextScreen = INVALID_SCREEN;
+ private Scroller mScroller;
+ private VelocityTracker mVelocityTracker;
+
+ private float mDownMotionX;
+ private float mLastMotionX;
+ private float mLastMotionY;
+
+ private final static int TOUCH_STATE_REST = 0;
+ private final static int TOUCH_STATE_SCROLLING = 1;
+ private final static int TOUCH_STATE_PREV_PAGE = 2;
+ private final static int TOUCH_STATE_NEXT_PAGE = 3;
+
+ private int mTouchState = TOUCH_STATE_REST;
+
+ private OnLongClickListener mLongClickListener;
+
+ private boolean mAllowLongPress = true;
+
+ private int mTouchSlop;
+ private int mPagingTouchSlop;
+ private int mMaximumVelocity;
+
+ private static final int INVALID_POINTER = -1;
+
+ private int mActivePointerId = INVALID_POINTER;
+
+ private ScreenSwitchListener mScreenSwitchListener;
+
+ private boolean mDimmedPagesDirty;
+
+ public interface ScreenSwitchListener {
+ void onScreenSwitch(View newScreen, int newScreenIndex);
+ }
+
+ /**
+ * Constructor
+ *
+ * @param context The application's context.
+ */
+ 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);
+
+ setHapticFeedbackEnabled(false);
+ initWorkspace();
+ }
+
+ /**
+ * Initializes various states for this workspace.
+ */
+ private void initWorkspace() {
+ mScroller = new Scroller(getContext());
+ mCurrentScreen = 0;
+
+ final ViewConfiguration configuration = ViewConfiguration.get(getContext());
+ mTouchSlop = configuration.getScaledTouchSlop();
+ mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
+ mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+ }
+
+ public void setScreenSwitchListener(ScreenSwitchListener screenSwitchListener) {
+ mScreenSwitchListener = screenSwitchListener;
+ if (mScreenSwitchListener != null) {
+ mScreenSwitchListener.onScreenSwitch(getScreenAt(mCurrentScreen), mCurrentScreen);
+ }
+ }
+
+ /**
+ * Returns the index of the currently displayed screen.
+ *
+ * @return The index of the currently displayed screen.
+ */
+ int getCurrentScreen() {
+ return mCurrentScreen;
+ }
+
+ int getScreenCount() {
+ return getChildCount();
+ }
+
+ View getScreenAt(int index) {
+ return getChildAt(index);
+ }
+
+ int getScrollWidth() {
+ return getWidth();
+ }
+
+ /**
+ * Sets the current screen.
+ *
+ * @param currentScreen
+ */
+ void setCurrentScreen(int currentScreen) {
+ if (!mScroller.isFinished()) mScroller.abortAnimation();
+ if (getChildCount() == 0) return;
+
+ mCurrentScreen = Math.max(0, Math.min(currentScreen, getScreenCount() - 1));
+ scrollTo(getChildOffset(mCurrentScreen) - getRelativeChildOffset(mCurrentScreen), 0);
+ invalidate();
+ notifyScreenSwitchListener();
+ }
+
+ private void notifyScreenSwitchListener() {
+ if (mScreenSwitchListener != null) {
+ mScreenSwitchListener.onScreenSwitch(getScreenAt(mCurrentScreen), mCurrentScreen);
+ }
+ }
+
+ /**
+ * 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 = getScreenCount();
+ for (int i = 0; i < count; i++) {
+ getScreenAt(i).setOnLongClickListener(l);
+ }
+ }
+
+ @Override
+ public void computeScroll() {
+ if (mScroller.computeScrollOffset()) {
+ scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
+ postInvalidate();
+ } else if (mNextScreen != INVALID_SCREEN) {
+ mCurrentScreen = Math.max(0, Math.min(mNextScreen, getScreenCount() - 1));
+ notifyScreenSwitchListener();
+ mNextScreen = INVALID_SCREEN;
+ }
+ }
+
+ @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.");
+ }
+
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+ if (heightMode != MeasureSpec.EXACTLY) {
+ throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
+ }
+
+ // The children are given the same width and height as the workspace
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ setMeasuredDimension(widthSize, heightSize);
+
+ if (mFirstLayout) {
+ setHorizontalScrollBarEnabled(false);
+ scrollTo(mCurrentScreen * widthSize, 0);
+ setHorizontalScrollBarEnabled(true);
+ mFirstLayout = false;
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ final int childCount = getChildCount();
+ int childLeft = 0;
+ if (childCount > 0) {
+ childLeft = (getMeasuredWidth() - getChildAt(0).getMeasuredWidth()) / 2;
+ }
+
+ for (int i = 0; i < childCount; 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;
+ }
+ }
+ }
+
+ protected void invalidateDimmedPages() {
+ mDimmedPagesDirty = true;
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ if (mDimmedPagesDirty || (mTouchState == TOUCH_STATE_SCROLLING) ||
+ !mScroller.isFinished()) {
+ int screenCenter = mScrollX + (getMeasuredWidth() / 2);
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; ++i) {
+ PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i);
+ int childWidth = layout.getMeasuredWidth();
+ int halfChildWidth = (childWidth / 2);
+ int childCenter = getChildOffset(i) + halfChildWidth;
+ int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
+ float dimAlpha = 0.0f;
+ if (distanceFromScreenCenter < halfChildWidth) {
+ dimAlpha = 0.0f;
+ } else if (distanceFromScreenCenter > childWidth) {
+ dimAlpha = 1.0f;
+ } else {
+ dimAlpha = (float) (distanceFromScreenCenter - halfChildWidth) / halfChildWidth;
+ dimAlpha = (dimAlpha * dimAlpha);
+ }
+ layout.setDimmedBitmapAlpha(Math.max(0.0f, Math.min(1.0f, dimAlpha)));
+ }
+ }
+ 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;
+ }
+ return false;
+ }
+
+ @Override
+ protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
+ int focusableScreen;
+ if (mNextScreen != INVALID_SCREEN) {
+ focusableScreen = mNextScreen;
+ } else {
+ focusableScreen = mCurrentScreen;
+ }
+ View v = getScreenAt(focusableScreen);
+ if (v != null) {
+ v.requestFocus(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() < getScreenCount() - 1) {
+ snapToScreen(getCurrentScreen() + 1);
+ return true;
+ }
+ }
+ return super.dispatchUnhandledMove(focused, direction);
+ }
+
+ @Override
+ public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
+ if (mCurrentScreen >= 0 && mCurrentScreen < getScreenCount()) {
+ getScreenAt(mCurrentScreen).addFocusables(views, direction);
+ }
+ if (direction == View.FOCUS_LEFT) {
+ if (mCurrentScreen > 0) {
+ getScreenAt(mCurrentScreen - 1).addFocusables(views, direction);
+ }
+ } else if (direction == View.FOCUS_RIGHT){
+ if (mCurrentScreen < getScreenCount() - 1) {
+ getScreenAt(mCurrentScreen + 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 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 = getScreenAt(mCurrentScreen);
+ 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 currentScreen = getChildAt(mCurrentScreen);
+ currentScreen.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.
+ */
+
+ /*
+ * 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.
+ */
+ determineScrollingStart(ev);
+ break;
+ }
+
+ 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.
+ */
+ mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
+
+ // check if this can be the beginning of a tap on the side of the screens
+ // to scroll the current page
+ if ((mTouchState != TOUCH_STATE_PREV_PAGE) &&
+ (mTouchState != TOUCH_STATE_NEXT_PAGE)) {
+ if (getChildCount() > 0) {
+ int relativeChildLeft = getChildOffset(0);
+ int relativeChildRight = relativeChildLeft + getChildAt(0).getMeasuredWidth();
+ if (x < relativeChildLeft) {
+ mTouchState = TOUCH_STATE_PREV_PAGE;
+ } else if (x > relativeChildRight) {
+ mTouchState = TOUCH_STATE_NEXT_PAGE;
+ }
+ }
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ // Release the drag
+ 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;
+ }
+
+ /*
+ * Determines if we should change the touch state to start scrolling after the
+ * user moves their touch point too far.
+ */
+ private 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 (xPaged) {
+ // Scroll if the user moved far enough along the X axis
+ mTouchState = TOUCH_STATE_SCROLLING;
+ mLastMotionX = x;
+ }
+ // 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 = getScreenAt(mCurrentScreen);
+ currentScreen.cancelLongPress();
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(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);
+ 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;
+
+ int sx = getScrollX();
+ if (deltaX < 0) {
+ if (sx > 0) {
+ scrollBy(Math.max(-sx, deltaX), 0);
+ }
+ } else if (deltaX > 0) {
+ final int lastChildIndex = getChildCount() - 1;
+ final int availableToScroll = getChildOffset(lastChildIndex) -
+ getRelativeChildOffset(lastChildIndex) - sx;
+ if (availableToScroll > 0) {
+ scrollBy(Math.min(availableToScroll, deltaX), 0);
+ }
+ } else {
+ awakenScrollBars();
+ }
+ } else if ((mTouchState == TOUCH_STATE_PREV_PAGE) ||
+ (mTouchState == TOUCH_STATE_NEXT_PAGE)) {
+ 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);
+ boolean isfling = Math.abs(mDownMotionX - x) > MIN_LENGTH_FOR_FLING;
+
+ if (isfling && velocityX > SNAP_VELOCITY && mCurrentScreen > 0) {
+ snapToScreen(mCurrentScreen - 1);
+ } else if (isfling && velocityX < -SNAP_VELOCITY &&
+ mCurrentScreen < getChildCount() - 1) {
+ snapToScreen(mCurrentScreen + 1);
+ } else {
+ snapToDestination();
+ }
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ } else if (mTouchState == TOUCH_STATE_PREV_PAGE) {
+ // at this point we have not moved beyond the touch slop
+ // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
+ // we can just page
+ int nextScreen = Math.max(0, mCurrentScreen - 1);
+ if (nextScreen != mCurrentScreen) {
+ snapToScreen(nextScreen);
+ } else {
+ snapToDestination();
+ }
+ } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) {
+ // at this point we have not moved beyond the touch slop
+ // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
+ // we can just page
+ int nextScreen = Math.min(getChildCount() - 1, mCurrentScreen + 1);
+ if (nextScreen != mCurrentScreen) {
+ snapToScreen(nextScreen);
+ } else {
+ snapToDestination();
+ }
+ }
+ mTouchState = TOUCH_STATE_REST;
+ mActivePointerId = INVALID_POINTER;
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ mTouchState = TOUCH_STATE_REST;
+ mActivePointerId = INVALID_POINTER;
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ break;
+ }
+
+ return true;
+ }
+
+ 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 screen = indexOfChild(child);
+ if (screen >= 0 && !isInTouchMode()) {
+ snapToScreen(screen);
+ }
+ }
+
+ 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();
+ }
+ return offset;
+ }
+
+ protected void snapToDestination() {
+ int minDistanceFromScreenCenter = getMeasuredWidth();
+ int minDistanceFromScreenCenterIndex = -1;
+ int screenCenter = mScrollX + (getMeasuredWidth() / 2);
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; ++i) {
+ PagedViewCellLayout layout = (PagedViewCellLayout) 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;
+ }
+ }
+ snapToScreen(minDistanceFromScreenCenterIndex, 1000);
+ }
+
+ void snapToScreen(int whichScreen) {
+ snapToScreen(whichScreen, 1000);
+ }
+
+ void snapToScreen(int whichScreen, int duration) {
+ whichScreen = Math.max(0, Math.min(whichScreen, getScreenCount() - 1));
+
+ mNextScreen = whichScreen;
+
+ int newX = getChildOffset(whichScreen) - getRelativeChildOffset(whichScreen);
+ final int sX = getScrollX();
+ final int delta = newX - sX;
+ awakenScrollBars(duration);
+ if (duration == 0) {
+ duration = Math.abs(delta);
+ }
+
+ if (!mScroller.isFinished()) mScroller.abortAnimation();
+ mScroller.startScroll(sX, 0, delta, 0, duration);
+ invalidate();
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final SavedState state = new SavedState(super.onSaveInstanceState());
+ state.currentScreen = mCurrentScreen;
+ return state;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState savedState = (SavedState) state;
+ super.onRestoreInstanceState(savedState.getSuperState());
+ if (savedState.currentScreen != -1) {
+ mCurrentScreen = savedState.currentScreen;
+ }
+ }
+
+ public void scrollLeft() {
+ if (mScroller.isFinished()) {
+ if (mCurrentScreen > 0) snapToScreen(mCurrentScreen - 1);
+ } else {
+ if (mNextScreen > 0) snapToScreen(mNextScreen - 1);
+ }
+ }
+
+ public void scrollRight() {
+ if (mScroller.isFinished()) {
+ if (mCurrentScreen < getChildCount() -1) snapToScreen(mCurrentScreen + 1);
+ } else {
+ if (mNextScreen < getChildCount() -1) snapToScreen(mNextScreen + 1);
+ }
+ }
+
+ 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;
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * @return True is long presses are still allowed for the current touch
+ */
+ public boolean allowLongPress() {
+ return mAllowLongPress;
+ }
+
+ 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);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ public abstract void syncPages();
+ public abstract void syncPageItems(int page);
+ public void invalidatePageData() {
+ syncPages();
+ for (int i = 0; i < getChildCount(); ++i) {
+ syncPageItems(i);
+ }
+ invalidateDimmedPages();
+ requestLayout();
+ }
+}
diff --git a/src/com/android/launcher2/PagedViewCellLayout.java b/src/com/android/launcher2/PagedViewCellLayout.java
new file mode 100644
index 000000000..6c9ff6dbe
--- /dev/null
+++ b/src/com/android/launcher2/PagedViewCellLayout.java
@@ -0,0 +1,447 @@
+/*
+ * 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 {
+ public interface DimmedBitmapSetupListener {
+ public void onPreUpdateDimmedBitmap(PagedViewCellLayout layout);
+ public void onPostUpdateDimmedBitmap(PagedViewCellLayout layout);
+ }
+
+ static final String TAG = "PagedViewCellLayout";
+
+ // we make the dimmed bitmap smaller than the screen itself for memory + perf reasons
+ static final float DIMMED_BITMAP_SCALE = 0.75f;
+
+ // a dimmed version of the layout for rendering when in the periphery
+ private Bitmap mDimmedBitmap;
+ private Canvas mDimmedBitmapCanvas;
+ private float mDimmedBitmapAlpha;
+ private boolean mDimmedBitmapDirty;
+ private final Paint mDimmedBitmapPaint = new Paint();
+ private final Rect mLayoutRect = new Rect();
+ private final Rect mDimmedBitmapRect = new Rect();
+
+ private int mCellCountX;
+ private int mCellCountY;
+ private int mCellWidth;
+ private int mCellHeight;
+ private static int sDefaultCellDimensions = 96;
+
+ private DimmedBitmapSetupListener mDimmedBitmapSetupListener;
+
+ 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);
+
+ // enable drawing if we have to display a dimmed version of this layout
+ setWillNotDraw(false);
+ setAlwaysDrawnWithCacheEnabled(false);
+
+ // setup default cell parameters
+ mCellWidth = mCellHeight = sDefaultCellDimensions;
+ mCellCountX = LauncherModel.getCellCountX();
+ mCellCountY = LauncherModel.getCellCountY();
+
+ mDimmedBitmapPaint.setFilterBitmap(true);
+ }
+
+ public void setDimmedBitmapSetupListener(DimmedBitmapSetupListener listener) {
+ mDimmedBitmapSetupListener = listener;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (mDimmedBitmap != null && mDimmedBitmapAlpha > 0.0f) {
+ if (mDimmedBitmapDirty) {
+ updateDimmedBitmap();
+ mDimmedBitmapDirty = false;
+ }
+ mDimmedBitmapPaint.setAlpha((int) (mDimmedBitmapAlpha * 255));
+
+ canvas.drawBitmap(mDimmedBitmap, mDimmedBitmapRect, mLayoutRect, mDimmedBitmapPaint);
+ }
+ }
+
+ @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 - mDimmedBitmapAlpha);
+
+ addView(child, index, lp);
+
+ // next time we draw the dimmed bitmap we need to update it
+ mDimmedBitmapDirty = true;
+ invalidate();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void removeView(View view) {
+ super.removeView(view);
+
+ // next time we draw the dimmed bitmap we need to update it
+ mDimmedBitmapDirty = true;
+ invalidate();
+ }
+
+ @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;
+ }
+ */
+ widthGap = heightGap = minGap;
+
+ int newWidth = mPaddingLeft + mPaddingRight + (mCellCountX * cellWidth) +
+ ((mCellCountX - 1) * minGap);
+ int newHeight = mPaddingTop + mPaddingBottom + (mCellCountY * cellHeight) +
+ ((mCellCountY - 1) * minGap);
+
+ 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();
+
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ PagedViewCellLayout.LayoutParams lp =
+ (PagedViewCellLayout.LayoutParams) child.getLayoutParams();
+
+ int childLeft = lp.x;
+ int childTop = lp.y;
+ child.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height);
+ }
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ mLayoutRect.set(0, 0, w, h);
+ mDimmedBitmapRect.set(0, 0, (int) (DIMMED_BITMAP_SCALE * w), (int) (DIMMED_BITMAP_SCALE * h));
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ return super.onTouchEvent(event) || true;
+ }
+
+ @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);
+ }
+ }
+
+ public void setCellCount(int xCount, int yCount) {
+ mCellCountX = xCount;
+ mCellCountY = yCount;
+ requestLayout();
+ }
+
+ public float getDimmedBitmapAlpha() {
+ return mDimmedBitmapAlpha;
+ }
+
+ public void setDimmedBitmapAlpha(float alpha) {
+ // If we're dimming the screen after it was not dimmed, refresh
+ // to allow for updated widgets. We don't continually refresh it
+ // after this point, however, as an optimization
+ if (mDimmedBitmapAlpha == 0.0f && alpha > 0.0f) {
+ updateDimmedBitmap();
+ }
+ mDimmedBitmapAlpha = alpha;
+ setChildrenAlpha(1.0f - mDimmedBitmapAlpha);
+ }
+
+ public void updateDimmedBitmap() {
+ if (mDimmedBitmapSetupListener != null) {
+ mDimmedBitmapSetupListener.onPreUpdateDimmedBitmap(this);
+ }
+
+ if (mDimmedBitmap == null) {
+ mDimmedBitmap = Bitmap.createBitmap((int) (getWidth() * DIMMED_BITMAP_SCALE),
+ (int) (getHeight() * DIMMED_BITMAP_SCALE), Bitmap.Config.ARGB_8888);
+ mDimmedBitmapCanvas = new Canvas(mDimmedBitmap);
+ mDimmedBitmapCanvas.scale(DIMMED_BITMAP_SCALE, DIMMED_BITMAP_SCALE);
+ }
+ // clear the canvas
+ mDimmedBitmapCanvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
+
+ // draw the screen into the bitmap
+ // just for drawing to the bitmap, make all the items on the screen opaque
+ setChildrenAlpha(1.0f);
+ dispatchDraw(mDimmedBitmapCanvas);
+ setChildrenAlpha(1.0f - mDimmedBitmapAlpha);
+
+ // make the bitmap 'dimmed' ie colored regions are dark grey,
+ // the rest is light grey
+ // We draw grey to the whole bitmap, but filter where we draw based on
+ // what regions are transparent or not (SRC_OUT), causing the intended effect
+
+ // First, draw light grey everywhere in the background (currently transparent) regions
+ // This will leave the regions with the widgets as mostly transparent
+ mDimmedBitmapCanvas.drawColor(Color.argb(80, 0, 0, 0), PorterDuff.Mode.SRC_IN);
+
+ if (mDimmedBitmapSetupListener != null) {
+ mDimmedBitmapSetupListener.onPostUpdateDimmedBitmap(this);
+ }
+ }
+
+ private void setChildrenAlpha(float alpha) {
+ for (int i = 0; i < getChildCount(); i++) {
+ getChildAt(i).setAlpha(alpha);
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ @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;
+
+ // 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(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 String toString() {
+ return "(" + this.cellX + ", " + this.cellY + ")";
+ }
+ }
+}