summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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 + ")";
+ }
+ }
+}