diff options
-rw-r--r-- | res/layout-xlarge/all_apps_paged_view_application.xml | 29 | ||||
-rw-r--r-- | res/layout-xlarge/all_apps_tabbed.xml | 39 | ||||
-rw-r--r-- | res/values/attrs.xml | 9 | ||||
-rw-r--r-- | src/com/android/launcher2/AllAppsPagedView.java | 352 | ||||
-rw-r--r-- | src/com/android/launcher2/AllAppsTabbed.java | 68 | ||||
-rw-r--r-- | src/com/android/launcher2/PagedView.java | 788 | ||||
-rw-r--r-- | src/com/android/launcher2/PagedViewCellLayout.java | 447 |
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 0000000..98c2737 --- /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 0842fd0..dbe192c 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 be27288..09fb0da 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 0000000..e0d248e --- /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 7dbb1a5..0470cee 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 0000000..26805e0 --- /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 0000000..6c9ff6d --- /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 + ")"; + } + } +} |