From cc0408d2269fb2e265a3b108b55073980e64820f Mon Sep 17 00:00:00 2001 From: Yvonne Wong Date: Fri, 20 Nov 2015 10:49:59 -0800 Subject: Reimplement CM Settings Overview Panel in the new Launcher Part 1 - Adds vertical sliding panel and animations associated with opening and closing the panel - Adds the views for settings and animation for the drawer arrow - Enables hiding workspace icon labels, hiding drawer icon labels, scrolling wallpaper, and larger icons - Changes how ragged grid custom icon sizes gets defined Change-Id: I1a82215a09486b4770494e665e598efdbabd1d3e --- res/drawable-hdpi/ic_default_screen.png | Bin 0 -> 2046 bytes res/drawable-hdpi/ic_default_screen_pressed.png | Bin 0 -> 1993 bytes res/drawable-mdpi/ic_default_screen.png | Bin 0 -> 1597 bytes res/drawable-mdpi/ic_default_screen_pressed.png | Bin 0 -> 1626 bytes res/drawable-xhdpi/ic_default_screen.png | Bin 0 -> 2323 bytes res/drawable-xhdpi/ic_default_screen_pressed.png | Bin 0 -> 2385 bytes res/drawable-xxhdpi/ic_default_screen.png | Bin 0 -> 3187 bytes res/drawable-xxhdpi/ic_default_screen_pressed.png | Bin 0 -> 3237 bytes res/drawable/above_shadow.xml | 8 + res/drawable/below_shadow.xml | 8 + res/drawable/default_screen_button.xml | 19 + res/drawable/launcheranimatedarrow_00000.png | Bin 0 -> 661 bytes res/drawable/launcheranimatedarrow_00001.png | Bin 0 -> 749 bytes res/drawable/launcheranimatedarrow_00002.png | Bin 0 -> 705 bytes res/drawable/launcheranimatedarrow_00003.png | Bin 0 -> 746 bytes res/drawable/launcheranimatedarrow_00004.png | Bin 0 -> 747 bytes res/drawable/launcheranimatedarrow_00005.png | Bin 0 -> 664 bytes res/drawable/launcheranimatedarrow_00006.png | Bin 0 -> 1007 bytes res/drawable/launcheranimatedarrow_00007.png | Bin 0 -> 999 bytes res/drawable/launcheranimatedarrow_00008.png | Bin 0 -> 710 bytes res/drawable/launcheranimatedarrow_00009.png | Bin 0 -> 968 bytes res/drawable/launcheranimatedarrow_00010.png | Bin 0 -> 990 bytes res/drawable/launcheranimatedarrow_00011.png | Bin 0 -> 671 bytes res/drawable/launcheranimatedarrow_00012.png | Bin 0 -> 744 bytes res/drawable/launcheranimatedarrow_00013.png | Bin 0 -> 750 bytes res/drawable/launcheranimatedarrow_00014.png | Bin 0 -> 747 bytes res/drawable/launcheranimatedarrow_00015.png | Bin 0 -> 736 bytes res/drawable/launcheranimatedarrow_00016.png | Bin 0 -> 668 bytes res/drawable/listitem_bg.xml | 6 + res/drawable/listitem_text.xml | 6 + res/drawable/transition_arrow.xml | 21 + res/drawable/transition_arrow_reverse.xml | 21 + res/layout/overview_panel.xml | 175 ++- res/layout/settings_pane_list_header.xml | 19 + res/layout/settings_pane_list_item.xml | 38 + res/values/attrs.xml | 10 + res/values/cm_strings.xml | 36 + res/values/colors.xml | 4 + res/values/dimens.xml | 11 +- res/values/preferences_defaults.xml | 9 + src/com/android/launcher3/BubbleTextView.java | 14 +- src/com/android/launcher3/CellLayout.java | 5 +- src/com/android/launcher3/DeviceProfile.java | 4 +- src/com/android/launcher3/Folder.java | 14 +- src/com/android/launcher3/FolderPagedView.java | 6 + .../android/launcher3/InvariantDeviceProfile.java | 42 +- src/com/android/launcher3/Launcher.java | 123 +- src/com/android/launcher3/LauncherAppState.java | 4 + src/com/android/launcher3/LauncherModel.java | 1 + src/com/android/launcher3/OverviewPanel.java | 32 + .../android/launcher3/OverviewSettingsPanel.java | 74 ++ .../android/launcher3/VerticalSlidingPanel.java | 1317 ++++++++++++++++++++ src/com/android/launcher3/Workspace.java | 47 +- .../launcher3/allapps/AllAppsContainerView.java | 5 + .../launcher3/allapps/AllAppsGridAdapter.java | 31 +- .../android/launcher3/list/AutoScrollListView.java | 117 ++ .../launcher3/list/CompositeCursorAdapter.java | 532 ++++++++ .../launcher3/list/PinnedHeaderListAdapter.java | 137 ++ .../launcher3/list/PinnedHeaderListView.java | 565 +++++++++ .../list/SettingsPinnedHeaderAdapter.java | 307 +++++ .../launcher3/settings/SettingsProvider.java | 84 ++ .../launcher3/InvariantDeviceProfileTest.java | 2 +- 62 files changed, 3756 insertions(+), 98 deletions(-) create mode 100644 res/drawable-hdpi/ic_default_screen.png create mode 100644 res/drawable-hdpi/ic_default_screen_pressed.png create mode 100644 res/drawable-mdpi/ic_default_screen.png create mode 100644 res/drawable-mdpi/ic_default_screen_pressed.png create mode 100644 res/drawable-xhdpi/ic_default_screen.png create mode 100644 res/drawable-xhdpi/ic_default_screen_pressed.png create mode 100644 res/drawable-xxhdpi/ic_default_screen.png create mode 100644 res/drawable-xxhdpi/ic_default_screen_pressed.png create mode 100644 res/drawable/above_shadow.xml create mode 100644 res/drawable/below_shadow.xml create mode 100644 res/drawable/default_screen_button.xml create mode 100644 res/drawable/launcheranimatedarrow_00000.png create mode 100644 res/drawable/launcheranimatedarrow_00001.png create mode 100644 res/drawable/launcheranimatedarrow_00002.png create mode 100644 res/drawable/launcheranimatedarrow_00003.png create mode 100644 res/drawable/launcheranimatedarrow_00004.png create mode 100644 res/drawable/launcheranimatedarrow_00005.png create mode 100644 res/drawable/launcheranimatedarrow_00006.png create mode 100644 res/drawable/launcheranimatedarrow_00007.png create mode 100644 res/drawable/launcheranimatedarrow_00008.png create mode 100644 res/drawable/launcheranimatedarrow_00009.png create mode 100644 res/drawable/launcheranimatedarrow_00010.png create mode 100644 res/drawable/launcheranimatedarrow_00011.png create mode 100644 res/drawable/launcheranimatedarrow_00012.png create mode 100644 res/drawable/launcheranimatedarrow_00013.png create mode 100644 res/drawable/launcheranimatedarrow_00014.png create mode 100644 res/drawable/launcheranimatedarrow_00015.png create mode 100644 res/drawable/launcheranimatedarrow_00016.png create mode 100644 res/drawable/listitem_bg.xml create mode 100644 res/drawable/listitem_text.xml create mode 100644 res/drawable/transition_arrow.xml create mode 100644 res/drawable/transition_arrow_reverse.xml create mode 100644 res/layout/settings_pane_list_header.xml create mode 100644 res/layout/settings_pane_list_item.xml create mode 100644 res/values/preferences_defaults.xml create mode 100644 src/com/android/launcher3/OverviewPanel.java create mode 100644 src/com/android/launcher3/OverviewSettingsPanel.java create mode 100644 src/com/android/launcher3/VerticalSlidingPanel.java create mode 100644 src/com/android/launcher3/list/AutoScrollListView.java create mode 100644 src/com/android/launcher3/list/CompositeCursorAdapter.java create mode 100644 src/com/android/launcher3/list/PinnedHeaderListAdapter.java create mode 100644 src/com/android/launcher3/list/PinnedHeaderListView.java create mode 100644 src/com/android/launcher3/list/SettingsPinnedHeaderAdapter.java create mode 100644 src/com/android/launcher3/settings/SettingsProvider.java diff --git a/res/drawable-hdpi/ic_default_screen.png b/res/drawable-hdpi/ic_default_screen.png new file mode 100644 index 000000000..41dcf8f6e Binary files /dev/null and b/res/drawable-hdpi/ic_default_screen.png differ diff --git a/res/drawable-hdpi/ic_default_screen_pressed.png b/res/drawable-hdpi/ic_default_screen_pressed.png new file mode 100644 index 000000000..7779058c5 Binary files /dev/null and b/res/drawable-hdpi/ic_default_screen_pressed.png differ diff --git a/res/drawable-mdpi/ic_default_screen.png b/res/drawable-mdpi/ic_default_screen.png new file mode 100644 index 000000000..8a2c1e1c0 Binary files /dev/null and b/res/drawable-mdpi/ic_default_screen.png differ diff --git a/res/drawable-mdpi/ic_default_screen_pressed.png b/res/drawable-mdpi/ic_default_screen_pressed.png new file mode 100644 index 000000000..20606ce62 Binary files /dev/null and b/res/drawable-mdpi/ic_default_screen_pressed.png differ diff --git a/res/drawable-xhdpi/ic_default_screen.png b/res/drawable-xhdpi/ic_default_screen.png new file mode 100644 index 000000000..735332ae5 Binary files /dev/null and b/res/drawable-xhdpi/ic_default_screen.png differ diff --git a/res/drawable-xhdpi/ic_default_screen_pressed.png b/res/drawable-xhdpi/ic_default_screen_pressed.png new file mode 100644 index 000000000..11dede0a0 Binary files /dev/null and b/res/drawable-xhdpi/ic_default_screen_pressed.png differ diff --git a/res/drawable-xxhdpi/ic_default_screen.png b/res/drawable-xxhdpi/ic_default_screen.png new file mode 100644 index 000000000..253bd2d4d Binary files /dev/null and b/res/drawable-xxhdpi/ic_default_screen.png differ diff --git a/res/drawable-xxhdpi/ic_default_screen_pressed.png b/res/drawable-xxhdpi/ic_default_screen_pressed.png new file mode 100644 index 000000000..5367b7abd Binary files /dev/null and b/res/drawable-xxhdpi/ic_default_screen_pressed.png differ diff --git a/res/drawable/above_shadow.xml b/res/drawable/above_shadow.xml new file mode 100644 index 000000000..99db324ab --- /dev/null +++ b/res/drawable/above_shadow.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/res/drawable/below_shadow.xml b/res/drawable/below_shadow.xml new file mode 100644 index 000000000..fc70073a9 --- /dev/null +++ b/res/drawable/below_shadow.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/res/drawable/default_screen_button.xml b/res/drawable/default_screen_button.xml new file mode 100644 index 000000000..ad66137b0 --- /dev/null +++ b/res/drawable/default_screen_button.xml @@ -0,0 +1,19 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/launcheranimatedarrow_00000.png b/res/drawable/launcheranimatedarrow_00000.png new file mode 100644 index 000000000..2ed7fe9a7 Binary files /dev/null and b/res/drawable/launcheranimatedarrow_00000.png differ diff --git a/res/drawable/launcheranimatedarrow_00001.png b/res/drawable/launcheranimatedarrow_00001.png new file mode 100644 index 000000000..f3707e07c Binary files /dev/null and b/res/drawable/launcheranimatedarrow_00001.png differ diff --git a/res/drawable/launcheranimatedarrow_00002.png b/res/drawable/launcheranimatedarrow_00002.png new file mode 100644 index 000000000..3549389d0 Binary files /dev/null and b/res/drawable/launcheranimatedarrow_00002.png differ diff --git a/res/drawable/launcheranimatedarrow_00003.png b/res/drawable/launcheranimatedarrow_00003.png new file mode 100644 index 000000000..891e86c42 Binary files /dev/null and b/res/drawable/launcheranimatedarrow_00003.png differ diff --git a/res/drawable/launcheranimatedarrow_00004.png b/res/drawable/launcheranimatedarrow_00004.png new file mode 100644 index 000000000..7cfb1ef8c Binary files /dev/null and b/res/drawable/launcheranimatedarrow_00004.png differ diff --git a/res/drawable/launcheranimatedarrow_00005.png b/res/drawable/launcheranimatedarrow_00005.png new file mode 100644 index 000000000..121f4d516 Binary files /dev/null and b/res/drawable/launcheranimatedarrow_00005.png differ diff --git a/res/drawable/launcheranimatedarrow_00006.png b/res/drawable/launcheranimatedarrow_00006.png new file mode 100644 index 000000000..3a38e71f7 Binary files /dev/null and b/res/drawable/launcheranimatedarrow_00006.png differ diff --git a/res/drawable/launcheranimatedarrow_00007.png b/res/drawable/launcheranimatedarrow_00007.png new file mode 100644 index 000000000..e81a719fd Binary files /dev/null and b/res/drawable/launcheranimatedarrow_00007.png differ diff --git a/res/drawable/launcheranimatedarrow_00008.png b/res/drawable/launcheranimatedarrow_00008.png new file mode 100644 index 000000000..bd6f40981 Binary files /dev/null and b/res/drawable/launcheranimatedarrow_00008.png differ diff --git a/res/drawable/launcheranimatedarrow_00009.png b/res/drawable/launcheranimatedarrow_00009.png new file mode 100644 index 000000000..c7cb60daf Binary files /dev/null and b/res/drawable/launcheranimatedarrow_00009.png differ diff --git a/res/drawable/launcheranimatedarrow_00010.png b/res/drawable/launcheranimatedarrow_00010.png new file mode 100644 index 000000000..1bf30dcc3 Binary files /dev/null and b/res/drawable/launcheranimatedarrow_00010.png differ diff --git a/res/drawable/launcheranimatedarrow_00011.png b/res/drawable/launcheranimatedarrow_00011.png new file mode 100644 index 000000000..3cb598806 Binary files /dev/null and b/res/drawable/launcheranimatedarrow_00011.png differ diff --git a/res/drawable/launcheranimatedarrow_00012.png b/res/drawable/launcheranimatedarrow_00012.png new file mode 100644 index 000000000..58070de06 Binary files /dev/null and b/res/drawable/launcheranimatedarrow_00012.png differ diff --git a/res/drawable/launcheranimatedarrow_00013.png b/res/drawable/launcheranimatedarrow_00013.png new file mode 100644 index 000000000..810d0a229 Binary files /dev/null and b/res/drawable/launcheranimatedarrow_00013.png differ diff --git a/res/drawable/launcheranimatedarrow_00014.png b/res/drawable/launcheranimatedarrow_00014.png new file mode 100644 index 000000000..3f9e51861 Binary files /dev/null and b/res/drawable/launcheranimatedarrow_00014.png differ diff --git a/res/drawable/launcheranimatedarrow_00015.png b/res/drawable/launcheranimatedarrow_00015.png new file mode 100644 index 000000000..348bfbbc6 Binary files /dev/null and b/res/drawable/launcheranimatedarrow_00015.png differ diff --git a/res/drawable/launcheranimatedarrow_00016.png b/res/drawable/launcheranimatedarrow_00016.png new file mode 100644 index 000000000..5b0b28649 Binary files /dev/null and b/res/drawable/launcheranimatedarrow_00016.png differ diff --git a/res/drawable/listitem_bg.xml b/res/drawable/listitem_bg.xml new file mode 100644 index 000000000..be1fedbe5 --- /dev/null +++ b/res/drawable/listitem_bg.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/res/drawable/listitem_text.xml b/res/drawable/listitem_text.xml new file mode 100644 index 000000000..9637fd308 --- /dev/null +++ b/res/drawable/listitem_text.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/res/drawable/transition_arrow.xml b/res/drawable/transition_arrow.xml new file mode 100644 index 000000000..540db93d8 --- /dev/null +++ b/res/drawable/transition_arrow.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/drawable/transition_arrow_reverse.xml b/res/drawable/transition_arrow_reverse.xml new file mode 100644 index 000000000..855f08fb4 --- /dev/null +++ b/res/drawable/transition_arrow_reverse.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/layout/overview_panel.xml b/res/layout/overview_panel.xml index 1f02dce3c..5698c86a9 100644 --- a/res/layout/overview_panel.xml +++ b/res/layout/overview_panel.xml @@ -14,53 +14,130 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - - - - - + + - - \ No newline at end of file + android:layout_gravity="center_horizontal" + android:background="@color/slideup_panel_bg_color" + android:paddingTop="@dimen/overview_panel_top_padding" > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/settings_pane_list_header.xml b/res/layout/settings_pane_list_header.xml new file mode 100644 index 000000000..2429b9b81 --- /dev/null +++ b/res/layout/settings_pane_list_header.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/res/layout/settings_pane_list_item.xml b/res/layout/settings_pane_list_item.xml new file mode 100644 index 000000000..58b2de862 --- /dev/null +++ b/res/layout/settings_pane_list_item.xml @@ -0,0 +1,38 @@ + + + + + + \ No newline at end of file diff --git a/res/values/attrs.xml b/res/values/attrs.xml index d7f9ef4fa..787e5ca23 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -134,4 +134,14 @@ + + + + + + + + + + diff --git a/res/values/cm_strings.xml b/res/values/cm_strings.xml index 4ec60147f..e31fed119 100644 --- a/res/values/cm_strings.xml +++ b/res/values/cm_strings.xml @@ -18,9 +18,45 @@ Trebuchet + HOME SCREEN SETTINGS + DRAWER SETTINGS + APP SETTINGS + + + ON + OFF + DISABLED + Google Play + + Scroll wallpaper + + + Grid size + Comfortable + Cozy + Condensed + Custom (%1$d \u00d7 %2$d) + Select custom size + + + Search bar + + + Larger icons + + + Icon labels + Show + Hide + + + Protected apps + + + A search activity could not be found! diff --git a/res/values/colors.xml b/res/values/colors.xml index b70e1f895..0cad1cefc 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -36,6 +36,7 @@ #FFF #FFF5F5F5 #76000000 + #FF374248 #FFFFFFFF @@ -53,6 +54,9 @@ #C4C4C4 #263238 + + #FF6cd2ea + @android:color/white @android:color/darker_gray #CC14191E diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 799ea9803..8c64ef0c5 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -145,7 +145,7 @@ 4dp 2dp - 20dp + 8dp 2dp @@ -158,4 +158,13 @@ 300 100 + + 175dp + 20dp + 50dp + 30dp + 16dp + 60dp + 50dp + diff --git a/res/values/preferences_defaults.xml b/res/values/preferences_defaults.xml new file mode 100644 index 000000000..9eb5ca58f --- /dev/null +++ b/res/values/preferences_defaults.xml @@ -0,0 +1,9 @@ + + + true + true + false + false + false + false + diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 205c113a7..c15f07de9 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -43,6 +43,7 @@ import android.widget.TextView; import com.android.launcher3.IconCache.IconLoadRequest; import com.android.launcher3.model.PackageItemInfo; +import com.android.launcher3.settings.SettingsProvider; /** * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan @@ -86,7 +87,7 @@ public class BubbleTextView extends TextView private final boolean mDeferShadowGenerationOnTouch; private final boolean mCustomShadowsEnabled; private final boolean mLayoutHorizontal; - private int mIconSize; + private final int mIconSize; private int mTextColor; private boolean mStayPressed; @@ -131,6 +132,13 @@ public class BubbleTextView extends TextView setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx); defaultIconSize = grid.allAppsIconSizePx; } + boolean useCompactDrawer = SettingsProvider.getBoolean(context, + SettingsProvider.SETTINGS_UI_DRAWER_COMPACT, + R.bool.preferences_interface_drawer_compact_default); + if (!useCompactDrawer) { + defaultIconSize = getResources() + .getDimensionPixelSize(R.dimen.all_apps_icon_size_ragged); + } mIconSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconSizeOverride, defaultIconSize); @@ -435,10 +443,6 @@ public class BubbleTextView extends TextView if (mBackground != null) mBackground.setCallback(null); } - public void setIconSize(int iconSize) { - mIconSize = iconSize; - } - @Override public void setTextColor(int color) { mTextColor = color; diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index a99d791bd..daf26411d 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -615,9 +615,8 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { final LayoutParams lp = params; // Hotseat icons - remove text - if (child instanceof BubbleTextView) { - BubbleTextView bubbleChild = (BubbleTextView) child; - bubbleChild.setTextVisibility(!mIsHotseat); + if (mIsHotseat && child instanceof BubbleTextView) { + ((BubbleTextView) child).setTextVisibility(false); } child.setScaleX(getChildrenScale()); diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index f77b34862..84b6835f8 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -468,7 +468,7 @@ public class DeviceProfile { } // Layout the Overview Mode - ViewGroup overviewMode = launcher.getOverviewPanel(); + /*ViewGroup overviewMode = launcher.getOverviewPanel(); if (overviewMode != null) { int overviewButtonBarHeight = getOverviewModeButtonBarHeight(); lp = (FrameLayout.LayoutParams) overviewMode.getLayoutParams(); @@ -505,7 +505,7 @@ public class DeviceProfile { } } } - } + }*/ } private int getCurrentWidth() { diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java index 994d7d30d..1e0827e54 100644 --- a/src/com/android/launcher3/Folder.java +++ b/src/com/android/launcher3/Folder.java @@ -63,6 +63,7 @@ import com.android.launcher3.FolderInfo.FolderListener; import com.android.launcher3.UninstallDropTarget.UninstallSource; import com.android.launcher3.Workspace.ItemOperator; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.AccessibilityDragSource; +import com.android.launcher3.settings.SettingsProvider; import com.android.launcher3.util.Thunk; import com.android.launcher3.util.UiThreadCircularReveal; @@ -229,6 +230,13 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mFolderName.setInputType(mFolderName.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_WORDS); + boolean hideLabels = SettingsProvider.getBoolean(mLauncher, + SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, + R.bool.preferences_interface_homescreen_hide_icon_labels_default); + if (hideLabels) { + mFolderName.setVisibility(View.GONE); + } + mFooter = findViewById(R.id.folder_footer); // We find out how tall footer wants to be (it is set to wrap_content), so that @@ -326,7 +334,11 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // Convert to a string here to ensure that no other state associated with the text field // gets saved. String newTitle = mFolderName.getText().toString(); - mInfo.setTitle(newTitle); + if (!SettingsProvider.getBoolean(mLauncher, + SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, + R.bool.preferences_interface_homescreen_hide_icon_labels_default)) { + mInfo.setTitle(newTitle); + } LauncherModel.updateItemInDatabase(mLauncher, mInfo); if (commit) { diff --git a/src/com/android/launcher3/FolderPagedView.java b/src/com/android/launcher3/FolderPagedView.java index cc9c5738a..a7940d552 100644 --- a/src/com/android/launcher3/FolderPagedView.java +++ b/src/com/android/launcher3/FolderPagedView.java @@ -30,6 +30,7 @@ import android.view.animation.OvershootInterpolator; import com.android.launcher3.FocusHelper.PagedFolderKeyEventListener; import com.android.launcher3.PageIndicator.PageMarkerResources; import com.android.launcher3.Workspace.ItemOperator; +import com.android.launcher3.settings.SettingsProvider; import com.android.launcher3.util.Thunk; import java.util.ArrayList; @@ -214,6 +215,11 @@ public class FolderPagedView extends PagedView { textView.setOnClickListener(mFolder); textView.setOnLongClickListener(mFolder); textView.setOnFocusChangeListener(mFocusIndicatorView); + if (SettingsProvider.getBoolean(mFolder.mLauncher, + SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, + R.bool.preferences_interface_homescreen_hide_icon_labels_default)) { + textView.setTextVisibility(false); + } textView.setOnKeyListener(mKeyListener); textView.setLayoutParams(new CellLayout.LayoutParams( diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java index ae204c40c..6d4d95292 100644 --- a/src/com/android/launcher3/InvariantDeviceProfile.java +++ b/src/com/android/launcher3/InvariantDeviceProfile.java @@ -24,6 +24,7 @@ import android.util.DisplayMetrics; import android.view.Display; import android.view.WindowManager; +import com.android.launcher3.settings.SettingsProvider; import com.android.launcher3.util.Thunk; import java.util.ArrayList; @@ -132,7 +133,8 @@ public class InvariantDeviceProfile { minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y), dm); ArrayList closestProfiles = - findClosestDeviceProfiles(minWidthDps, minHeightDps, getPredefinedDeviceProfiles()); + findClosestDeviceProfiles(minWidthDps, minHeightDps, + getPredefinedDeviceProfiles(context)); InvariantDeviceProfile interpolatedDeviceProfileOut = invDistWeightedInterpolate(minWidthDps, minHeightDps, closestProfiles); @@ -169,35 +171,49 @@ public class InvariantDeviceProfile { smallSide, largeSide, false /* isLandscape */); } - ArrayList getPredefinedDeviceProfiles() { + ArrayList getPredefinedDeviceProfiles(Context context) { + boolean useLargeIcons = SettingsProvider.getBoolean(context, + SettingsProvider.SETTINGS_UI_GENERAL_ICONS_LARGE, + R.bool.preferences_interface_general_icons_large_default); ArrayList predefinedDeviceProfiles = new ArrayList<>(); // width, height, #rows, #columns, #folder rows, #folder columns, // iconSize, iconTextSize, #hotseat, #hotseatIconSize, defaultLayoutId. predefinedDeviceProfiles.add(new InvariantDeviceProfile("Super Short Stubby", - 255, 300, 2, 3, 2, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); + 255, 300, 2, 3, 2, 3, 3, (useLargeIcons? DEFAULT_ICON_SIZE_DP: 48), 13, 3, + (useLargeIcons? DEFAULT_ICON_SIZE_DP: 48), R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Shorter Stubby", - 255, 400, 3, 3, 3, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); + 255, 400, 3, 3, 3, 3, 3, (useLargeIcons? DEFAULT_ICON_SIZE_DP: 48), 13, 3, + (useLargeIcons? DEFAULT_ICON_SIZE_DP: 48), R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Short Stubby", - 275, 420, 3, 4, 3, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); + 275, 420, 3, 4, 3, 4, 4, (useLargeIcons? DEFAULT_ICON_SIZE_DP: 48), 13, 5, + (useLargeIcons? DEFAULT_ICON_SIZE_DP: 48), R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Stubby", - 255, 450, 3, 4, 3, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); + 255, 450, 3, 4, 3, 4, 4, (useLargeIcons? DEFAULT_ICON_SIZE_DP: 48), 13, 5, + (useLargeIcons? DEFAULT_ICON_SIZE_DP: 48), R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus S", - 296, 491.33f, 4, 4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); + 296, 491.33f, 4, 4, 4, 4, 4, (useLargeIcons? DEFAULT_ICON_SIZE_DP : 48), 13, 5, + (useLargeIcons? DEFAULT_ICON_SIZE_DP: 48), R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 4", - 335, 567, 4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); + 335, 567, 4, 4, 4, 4, 4, (useLargeIcons ? 70 : DEFAULT_ICON_SIZE_DP), 13, 5, + (useLargeIcons? 68 : 56), R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 5", - 359, 567, 4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); + 359, 567, 4, 4, 4, 4, 4, (useLargeIcons ? 70 : DEFAULT_ICON_SIZE_DP), 13, 5, + (useLargeIcons? 68 : 56), R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Large Phone", - 406, 694, 5, 5, 4, 4, 4, 64, 14.4f, 5, 56, R.xml.default_workspace_5x5)); + 406, 694, 5, 5, 4, 4, 4, (useLargeIcons ? 76 : 64), 14.4f, 5, + (useLargeIcons ? 68 : 56), R.xml.default_workspace_5x5)); // The tablet profile is odd in that the landscape orientation // also includes the nav bar on the side predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 7", - 575, 904, 5, 6, 4, 5, 4, 72, 14.4f, 7, 60, R.xml.default_workspace_5x6)); + 575, 904, 5, 6, 4, 5, 4, (useLargeIcons ? 88 : 72), 14.4f, 7, + (useLargeIcons ? 72 : 60), R.xml.default_workspace_5x6)); // Larger tablet profiles always have system bars on the top & bottom predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 10", - 727, 1207, 5, 6, 4, 5, 4, 76, 14.4f, 7, 64, R.xml.default_workspace_5x6)); + 727, 1207, 5, 6, 4, 5, 4, (useLargeIcons ? 92 : 76), 14.4f, 7, + (useLargeIcons ? 76 : 64), R.xml.default_workspace_5x6)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("20-inch Tablet", - 1527, 2527, 7, 7, 6, 6, 4, 100, 20, 7, 72, R.xml.default_workspace_4x4)); + 1527, 2527, 7, 7, 6, 6, 4, (useLargeIcons ? 124 : 100), 20, 7, + (useLargeIcons ? 84 : 72), R.xml.default_workspace_4x4)); return predefinedDeviceProfiles; } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 1976ca982..759ca041b 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -54,6 +54,7 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.Rect; +import android.graphics.drawable.AnimationDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -105,6 +106,7 @@ import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.model.WidgetsModel; +import com.android.launcher3.settings.SettingsProvider; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.LongArrayMap; import com.android.launcher3.util.Thunk; @@ -261,6 +263,7 @@ public class Launcher extends Activity @Thunk Hotseat mHotseat; private ViewGroup mOverviewPanel; + OverviewSettingsPanel mOverviewSettingsPanel; private View mAllAppsButton; private View mWidgetsButton; @@ -355,6 +358,9 @@ public class Launcher extends Activity // the press state and keep this reference to reset the press state when we return to launcher. private BubbleTextView mWaitingForResume; + // Preferences + private boolean mHideIconLabels; + protected static HashMap sCustomAppWidgets = new HashMap(); @@ -430,17 +436,11 @@ public class Launcher extends Activity LauncherAppState.setApplicationContext(getApplicationContext()); LauncherAppState app = LauncherAppState.getInstance(); - // Load configuration-specific DeviceProfile - mDeviceProfile = getResources().getConfiguration().orientation - == Configuration.ORIENTATION_LANDSCAPE ? - app.getInvariantDeviceProfile().landscapeProfile - : app.getInvariantDeviceProfile().portraitProfile; + initializeDeviceProfile(app); mSharedPrefs = getSharedPreferences(LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); mIsSafeModeEnabled = getPackageManager().isSafeMode(); - mModel = app.setLauncher(this); - mIconCache = app.getIconCache(); mDragController = new DragController(this); mInflater = getLayoutInflater(); @@ -1391,7 +1391,11 @@ public class Launcher extends Activity mHotseat.setOnLongClickListener(this); } + // Setup the overview panel mOverviewPanel = (ViewGroup) findViewById(R.id.overview_panel); + mOverviewSettingsPanel = new OverviewSettingsPanel(this); + mOverviewSettingsPanel.initializeAdapter(); + mWidgetsButton = findViewById(R.id.widget_button); mWidgetsButton.setOnClickListener(new OnClickListener() { @Override @@ -1429,6 +1433,36 @@ public class Launcher extends Activity settingsButton.setVisibility(View.GONE); } + View defaultScreenButton = findViewById(R.id.default_screen_button); + defaultScreenButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View arg0) { + if (!mWorkspace.isSwitchingState()) { + onClickDefaultScreenButton(arg0); + } + } + }); + defaultScreenButton.setOnTouchListener(getHapticFeedbackTouchListener()); + + final VerticalSlidingPanel verticalSlidingPanel = ((VerticalSlidingPanel) mOverviewPanel); + verticalSlidingPanel.setPanelSlideListener(new SettingsPanelSlideListener()); + verticalSlidingPanel.setEnableDragViewTouchEvents(true); + + View settingsPaneHeader = mOverviewPanel.findViewById(R.id.settings_pane_header); + if (settingsPaneHeader != null) { + verticalSlidingPanel.setDragView(settingsPaneHeader); + settingsPaneHeader.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (verticalSlidingPanel.isExpanded()) { + verticalSlidingPanel.collapsePane(); + } else { + verticalSlidingPanel.expandPane(); + } + } + }); + } + mOverviewPanel.setAlpha(0f); // Setup the workspace @@ -1517,6 +1551,7 @@ public class Launcher extends Activity BubbleTextView favorite = (BubbleTextView) mInflater.inflate(R.layout.app_icon, parent, false); favorite.applyFromShortcutInfo(info, mIconCache); + favorite.setTextVisibility(!mHideIconLabels); favorite.setCompoundDrawablePadding(mDeviceProfile.iconDrawablePaddingPx); favorite.setOnClickListener(this); favorite.setOnFocusChangeListener(mFocusHandler); @@ -1660,6 +1695,38 @@ public class Launcher extends Activity } }; + public void initializeDeviceProfile(LauncherAppState app) { + // Load configuration-specific DeviceProfile + mDeviceProfile = getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE ? + app.getInvariantDeviceProfile().landscapeProfile + : app.getInvariantDeviceProfile().portraitProfile; + + mModel = app.setLauncher(this); + mIconCache = app.getIconCache(); + + mHideIconLabels = SettingsProvider.getBoolean(this, + SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, + R.bool.preferences_interface_homescreen_hide_icon_labels_default); + } + + public void reloadLauncher() + { + // Re-initialize device profile + LauncherAppState app = LauncherAppState.getInstance(); + app.initInvariantDeviceProfile(); + initializeDeviceProfile(app); + + mDeviceProfile.layout(this); + + // Reload + mModel.resetLoadedState(true, true); + mModel.startLoader(mWorkspace.getRestorePage(), LauncherModel.LOADER_FLAG_NONE); + mWorkspace.updateCustomContentVisibility(); + + mAppsView.reset(); + } + @Override public void onAttachedToWindow() { super.onAttachedToWindow(); @@ -2405,6 +2472,9 @@ public class Launcher extends Activity // Create the view FolderIcon newFolder = FolderIcon.fromXml(R.layout.folder_icon, this, layout, folderInfo, mIconCache); + if (container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { + newFolder.setTextVisible(!mHideIconLabels); + } mWorkspace.addInScreen(newFolder, container, screenId, cellX, cellY, 1, 1, isWorkspaceLocked()); // Force measure the new folder icon @@ -2802,6 +2872,11 @@ public class Launcher extends Activity } } + protected void onClickDefaultScreenButton(View v) { + if (LOGD) Log.d(TAG, "onClickDefaultScreenButton"); + // TODO + } + public View.OnTouchListener getHapticFeedbackTouchListener() { if (mHapticFeedbackTouchListener == null) { mHapticFeedbackTouchListener = new View.OnTouchListener() { @@ -3855,6 +3930,7 @@ public class Launcher extends Activity view = FolderIcon.fromXml(R.layout.folder_icon, this, (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()), (FolderInfo) item, mIconCache); + ((FolderIcon) view).setTextVisible(!mHideIconLabels); break; default: throw new RuntimeException("Invalid Item Type"); @@ -4781,6 +4857,39 @@ public class Launcher extends Activity }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); } } + + class SettingsPanelSlideListener extends VerticalSlidingPanel.SimplePanelSlideListener { + ImageView mAnimatedArrow; + + public SettingsPanelSlideListener() { + super(); + mAnimatedArrow = (ImageView) mOverviewPanel.findViewById(R.id.settings_drag_arrow); + } + + @Override + public void onPanelCollapsed(View panel) { + mAnimatedArrow.setBackgroundResource(R.drawable.transition_arrow_reverse); + + AnimationDrawable frameAnimation = (AnimationDrawable) mAnimatedArrow.getBackground(); + frameAnimation.start(); + + /*if (mLauncher.updateGridIfNeeded()) { + Workspace workspace = mLauncher.getWorkspace(); + if (workspace.isInOverviewMode()) { + workspace.setChildrenOutlineAlpha(1.0f); + mLauncher.mSearchDropTargetBar.hideSearchBar(false); + } + }*/ + } + + @Override + public void onPanelExpanded(View panel) { + mAnimatedArrow.setBackgroundResource(R.drawable.transition_arrow); + + AnimationDrawable frameAnimation = (AnimationDrawable) mAnimatedArrow.getBackground(); + frameAnimation.start(); + } + } } interface DebugIntents { diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index d87ad67e5..d515f05e5 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -174,6 +174,10 @@ public class LauncherAppState { return mInvariantDeviceProfile; } + public void initInvariantDeviceProfile() { + mInvariantDeviceProfile = new InvariantDeviceProfile(sContext); + } + public static boolean isDogfoodBuild() { return getInstance().mBuildInfo.isDogfoodBuild(); } diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index b5922c6a3..e3170e93b 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -94,6 +94,7 @@ public class LauncherModel extends BroadcastReceiver public static final int LOADER_FLAG_NONE = 0; public static final int LOADER_FLAG_CLEAR_WORKSPACE = 1 << 0; public static final int LOADER_FLAG_MIGRATE_SHORTCUTS = 1 << 1; + public static final int LOADER_FLAG_RESIZE_GRID = 1 << 2; private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons private static final long INVALID_SCREEN_ID = -1L; diff --git a/src/com/android/launcher3/OverviewPanel.java b/src/com/android/launcher3/OverviewPanel.java new file mode 100644 index 000000000..2fdca6330 --- /dev/null +++ b/src/com/android/launcher3/OverviewPanel.java @@ -0,0 +1,32 @@ +package com.android.launcher3; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +public class OverviewPanel extends VerticalSlidingPanel implements Insettable { + public OverviewPanel(Context context) { + super(context); + } + + public OverviewPanel(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public OverviewPanel(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void setInsets(Rect insets) { + LinearLayout layout = (LinearLayout) + findViewById(R.id.settings_container); + FrameLayout.LayoutParams lp = + (FrameLayout.LayoutParams) layout.getLayoutParams(); + lp.bottomMargin = insets.bottom; + layout.setLayoutParams(lp); + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/OverviewSettingsPanel.java b/src/com/android/launcher3/OverviewSettingsPanel.java new file mode 100644 index 000000000..b4ba06a4d --- /dev/null +++ b/src/com/android/launcher3/OverviewSettingsPanel.java @@ -0,0 +1,74 @@ +package com.android.launcher3; + +import android.content.res.Resources; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.widget.ListView; +import com.android.launcher3.list.PinnedHeaderListView; +import com.android.launcher3.list.SettingsPinnedHeaderAdapter; + +public class OverviewSettingsPanel { + public static final int HOME_SETTINGS_POSITION = 0; + public static final int DRAWER_SETTINGS_POSITION = 1; + public static final int APP_SETTINGS_POSITION = 2; + + private Launcher mLauncher; + private SettingsPinnedHeaderAdapter mSettingsAdapter; + private PinnedHeaderListView mListView; + + OverviewSettingsPanel(Launcher launcher) { + mLauncher = launcher; + } + + // One time initialization of the SettingsPinnedHeaderAdapter + public void initializeAdapter() { + // Settings pane Listview + mListView = (PinnedHeaderListView) mLauncher + .findViewById(R.id.settings_home_screen_listview); + mListView.setOverScrollMode(ListView.OVER_SCROLL_NEVER); + Resources res = mLauncher.getResources(); + String[] headers = new String[] { + res.getString(R.string.home_screen_settings), + res.getString(R.string.drawer_settings), + res.getString(R.string.app_settings)}; + + String[] values = new String[]{ + res.getString(R.string.home_screen_search_text), + res.getString(R.string.icon_labels), + res.getString(R.string.scrolling_wallpaper), + res.getString(R.string.grid_size_text)}; + + String[] valuesDrawer = new String[] { + res.getString(R.string.icon_labels)}; + + String[] valuesApp = new String[] { + res.getString(R.string.larger_icons_text), + res.getString(R.string.protected_app_settings)}; + + mSettingsAdapter = new SettingsPinnedHeaderAdapter(mLauncher); + mSettingsAdapter.setHeaders(headers); + mSettingsAdapter.addPartition(false, true); + mSettingsAdapter.addPartition(false, true); + mSettingsAdapter.addPartition(false, true); + mSettingsAdapter.mPinnedHeaderCount = headers.length; + + mSettingsAdapter.changeCursor(HOME_SETTINGS_POSITION, createCursor(headers[0], values)); + mSettingsAdapter.changeCursor(DRAWER_SETTINGS_POSITION, createCursor(headers[1], + valuesDrawer)); + mSettingsAdapter.changeCursor(APP_SETTINGS_POSITION, createCursor(headers[2], valuesApp)); + mListView.setAdapter(mSettingsAdapter); + } + + private Cursor createCursor(String header, String[] values) { + MatrixCursor cursor = new MatrixCursor(new String[]{"_id", header}); + int count = values.length; + for (int i = 0; i < count; i++) { + cursor.addRow(new Object[]{i, values[i]}); + } + return cursor; + } + + public void notifyDataSetInvalidated() { + mSettingsAdapter.notifyDataSetInvalidated(); + } +} diff --git a/src/com/android/launcher3/VerticalSlidingPanel.java b/src/com/android/launcher3/VerticalSlidingPanel.java new file mode 100644 index 000000000..0ebbebc72 --- /dev/null +++ b/src/com/android/launcher3/VerticalSlidingPanel.java @@ -0,0 +1,1317 @@ +package com.android.launcher3; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.widget.ViewDragHelper; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.SoundEffectConstants; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.Animation; +import android.view.animation.TranslateAnimation; + +public class VerticalSlidingPanel extends ViewGroup { + private static final String TAG = VerticalSlidingPanel.class.getSimpleName(); + + /** + * Default peeking out panel height + */ + private static final int DEFAULT_PANEL_HEIGHT = 68; // dp; + + /** + * Default height of the shadow above the peeking out panel + */ + private static final int DEFAULT_SHADOW_HEIGHT = 4; // dp; + + /** + * If no fade color is given by default it will fade to 80% gray. + */ + private static final int DEFAULT_FADE_COLOR = 0x99000000; + + /** + * Default Minimum velocity that will be detected as a fling + */ + private static final int DEFAULT_MIN_FLING_VELOCITY = 400; // dips per second + /** + * Default is set to false because that is how it was written + */ + private static final boolean DEFAULT_OVERLAY_FLAG = false; + /** + * Default attributes for layout + */ + private static final int[] DEFAULT_ATTRS = new int[] { + android.R.attr.gravity + }; + + /** + * Minimum velocity that will be detected as a fling + */ + private int mMinFlingVelocity = DEFAULT_MIN_FLING_VELOCITY; + + /** + * The fade color used for the panel covered by the slider. 0 = no fading. + */ + private int mCoveredFadeColor = DEFAULT_FADE_COLOR; + + /** + * Default parallax length of the main view + */ + private static final int DEFAULT_PARALLAX_OFFSET = 0; + + /** + * The paint used to dim the main layout when sliding + */ + private final Paint mCoveredFadePaint = new Paint(); + + /** + * Drawable used to draw the shadow between panes. + */ + private final Drawable mShadowDrawable; + + /** + * The size of the overhang in pixels. + */ + private int mPanelHeight = -1; + + /** + * The size of the shadow in pixels. + */ + private int mShadowHeight = -1; + + /** + * Parallax offset + */ + private int mParallaxOffset = -1; + + /** + * True if the collapsed panel should be dragged up. + */ + private boolean mIsSlidingUp; + + /** + * True if a panel can slide with the current measurements + */ + private boolean mCanSlide; + + /** + * Panel overlays the windows instead of putting it underneath it. + */ + private boolean mOverlayContent = DEFAULT_OVERLAY_FLAG; + + /** + * If provided, the panel can be dragged by only this view. Otherwise, the entire panel can be + * used for dragging. + */ + private View mDragView; + + /** + * If provided, the panel can be dragged by only this view. Otherwise, the entire panel can be + * used for dragging. + */ + private int mDragViewResId = -1; + + /** + * The child view that can slide, if any. + */ + private View mSlideableView; + + /** + * The main view + */ + private View mMainView; + + /** + * Current state of the slideable view. + */ + private enum SlideState { + EXPANDED, + COLLAPSED, + ANCHORED + } + private SlideState mSlideState = SlideState.COLLAPSED; + + /** + * How far the panel is offset from its expanded position. + * range [0, 1] where 0 = expanded, 1 = collapsed. + */ + private float mSlideOffset; + + /** + * How far in pixels the slideable panel may move. + */ + private int mSlideRange; + + /** + * A panel view is locked into internal scrolling or another condition that + * is preventing a drag. + */ + private boolean mIsUnableToDrag; + + /** + * Flag indicating that sliding feature is enabled\disabled + */ + private boolean mIsSlidingEnabled; + + /** + * Flag indicating if a drag view can have its own touch events. If set + * to true, a drag view can scroll horizontally and have its own click listener. + * + * Default is set to false. + */ + private boolean mIsUsingDragViewTouchEvents; + + /** + * Threshold to tell if there was a scroll touch event. + */ + private final int mScrollTouchSlop; + + private float mInitialMotionX; + private float mInitialMotionY; + private float mAnchorPoint = 0.f; + private TranslateAnimation mAnimation; + + private PanelSlideListener mPanelSlideListener; + + private final ViewDragHelper mDragHelper; + + /** + * Stores whether or not the pane was expanded the last time it was slideable. + * If expand/collapse operations are invoked this state is modified. Used by + * instance state save/restore. + */ + private boolean mFirstLayout = true; + + private final Rect mTmpRect = new Rect(); + + /** + * Listener for monitoring events about sliding panes. + */ + public interface PanelSlideListener { + /** + * Called when a sliding pane's position changes. + * @param panel The child view that was moved + * @param slideOffset The new offset of this sliding pane within its range, from 0-1 + */ + public void onPanelSlide(View panel, float slideOffset); + /** + * Called when a sliding pane becomes slid completely collapsed. The pane may or may not + * be interactive at this point depending on if it's shown or hidden + * @param panel The child view that was slid to an collapsed position, revealing other panes + */ + public void onPanelCollapsed(View panel); + + /** + * Called when a sliding pane becomes slid completely expanded. The pane is now guaranteed + * to be interactive. It may now obscure other views in the layout. + * @param panel The child view that was slid to a expanded position + */ + public void onPanelExpanded(View panel); + + public void onPanelAnchored(View panel); + } + + /** + * No-op stubs for {@link PanelSlideListener}. If you only want to implement a subset + * of the listener methods you can extend this instead of implement the full interface. + */ + public static class SimplePanelSlideListener implements PanelSlideListener { + @Override + public void onPanelSlide(View panel, float slideOffset) { + } + @Override + public void onPanelCollapsed(View panel) { + } + @Override + public void onPanelExpanded(View panel) { + } + @Override + public void onPanelAnchored(View panel) { + } + } + + public VerticalSlidingPanel(Context context) { + this(context, null); + } + + public VerticalSlidingPanel(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public VerticalSlidingPanel(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + if (attrs != null) { + TypedArray defAttrs = context.obtainStyledAttributes(attrs, DEFAULT_ATTRS); + + if (defAttrs != null) { + int gravity = defAttrs.getInt(0, Gravity.NO_GRAVITY); + if (gravity != Gravity.TOP && gravity != Gravity.BOTTOM) { + throw new IllegalArgumentException("gravity must be set to either top or bottom"); + } + mIsSlidingUp = gravity == Gravity.BOTTOM; + } + + defAttrs.recycle(); + + TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.VerticalSlidingPanel); + + if (ta != null) { + mPanelHeight = ta.getDimensionPixelSize(R.styleable.VerticalSlidingPanel_panelHeight, -1); + mShadowHeight = ta.getDimensionPixelSize(R.styleable.VerticalSlidingPanel_shadowHeight, -1); + mParallaxOffset = ta.getDimensionPixelSize(R.styleable.VerticalSlidingPanel_parallaxOffset, -1); + + mMinFlingVelocity = ta.getInt(R.styleable.VerticalSlidingPanel_flingVelocity, DEFAULT_MIN_FLING_VELOCITY); + mCoveredFadeColor = ta.getColor(R.styleable.VerticalSlidingPanel_fadeColor, DEFAULT_FADE_COLOR); + + mDragViewResId = ta.getResourceId(R.styleable.VerticalSlidingPanel_dragView, -1); + + mOverlayContent = ta.getBoolean(R.styleable.VerticalSlidingPanel_overlay,DEFAULT_OVERLAY_FLAG); + } + + ta.recycle(); + } + + final float density = context.getResources().getDisplayMetrics().density; + if (mPanelHeight == -1) { + mPanelHeight = (int) (DEFAULT_PANEL_HEIGHT * density + 0.5f); + } + if (mShadowHeight == -1) { + mShadowHeight = (int) (DEFAULT_SHADOW_HEIGHT * density + 0.5f); + } + if (mParallaxOffset == -1) { + mParallaxOffset = (int) (DEFAULT_PARALLAX_OFFSET * density); + } + // If the shadow height is zero, don't show the shadow + if (mShadowHeight > 0) { + if (mIsSlidingUp) { + mShadowDrawable = getResources().getDrawable(R.drawable.above_shadow, + context.getTheme()); + } else { + mShadowDrawable = getResources().getDrawable(R.drawable.below_shadow, + context.getTheme()); + } + + } else { + mShadowDrawable = null; + } + + setWillNotDraw(false); + + mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback()); + mDragHelper.setMinVelocity(mMinFlingVelocity * density); + + mCanSlide = true; + mIsSlidingEnabled = true; + + ViewConfiguration vc = ViewConfiguration.get(context); + mScrollTouchSlop = vc.getScaledTouchSlop(); + } + + /** + * Set the Drag View after the view is inflated + */ + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + if (mDragViewResId != -1) { + mDragView = findViewById(mDragViewResId); + } + } + + /** + * Set the color used to fade the pane covered by the sliding pane out when the pane + * will become fully covered in the expanded state. + * + * @param color An ARGB-packed color value + */ + public void setCoveredFadeColor(int color) { + mCoveredFadeColor = color; + invalidate(); + } + + /** + * @return The ARGB-packed color value used to fade the fixed pane + */ + public int getCoveredFadeColor() { + return mCoveredFadeColor; + } + + /** + * Set sliding enabled flag + * @param enabled flag value + */ + public void setSlidingEnabled(boolean enabled) { + mIsSlidingEnabled = enabled; + } + + /** + * Set the collapsed panel height in pixels + * + * @param val A height in pixels + */ + public void setPanelHeight(int val) { + mPanelHeight = val; + requestLayout(); + } + + /** + * @return The current collapsed panel height + */ + public int getPanelHeight() { + return mPanelHeight; + } + + /** + * @return The current parallax offset + */ + public int getCurrentParallaxOffset() { + int offset = (int)(mParallaxOffset * (1 - mSlideOffset)); + return mIsSlidingUp ? -offset : offset; + } + + /** + * Sets the panel slide listener + * @param listener + */ + public void setPanelSlideListener(PanelSlideListener listener) { + mPanelSlideListener = listener; + } + + /** + * Set the draggable view portion. Use to null, to allow the whole panel to be draggable + * + * @param dragView A view that will be used to drag the panel. + */ + public void setDragView(View dragView) { + mDragView = dragView; + } + + /** + * Set an anchor point where the panel can stop during sliding + * + * @param anchorPoint A value between 0 and 1, determining the position of the anchor point + * starting from the top of the layout. + */ + public void setAnchorPoint(float anchorPoint) { + if (anchorPoint > 0 && anchorPoint < 1) + mAnchorPoint = anchorPoint; + } + + /** + * Sets whether or not the panel overlays the content + * @param overlayed + */ + public void setOverlayed(boolean overlayed) { + mOverlayContent = overlayed; + } + + /** + * Check if the panel is set as an overlay. + */ + public boolean isOverlayed() { + return mOverlayContent; + } + + void dispatchOnPanelSlide(View panel) { + if (mPanelSlideListener != null) { + mPanelSlideListener.onPanelSlide(panel, mSlideOffset); + } + } + + void dispatchOnPanelExpanded(View panel) { + if (mPanelSlideListener != null) { + mPanelSlideListener.onPanelExpanded(panel); + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + void dispatchOnPanelCollapsed(View panel) { + if (mPanelSlideListener != null) { + mPanelSlideListener.onPanelCollapsed(panel); + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + void dispatchOnPanelAnchored(View panel) { + if (mPanelSlideListener != null) { + mPanelSlideListener.onPanelAnchored(panel); + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + void updateObscuredViewVisibility() { + if (getChildCount() == 0) { + return; + } + final int leftBound = getPaddingLeft(); + final int rightBound = getWidth() - getPaddingRight(); + final int topBound = getPaddingTop(); + final int bottomBound = getHeight() - getPaddingBottom(); + final int left; + final int right; + final int top; + final int bottom; + if (mSlideableView != null && hasOpaqueBackground(mSlideableView)) { + left = mSlideableView.getLeft(); + right = mSlideableView.getRight(); + top = mSlideableView.getTop(); + bottom = mSlideableView.getBottom(); + } else { + left = right = top = bottom = 0; + } + View child = getChildAt(0); + final int clampedChildLeft = Math.max(leftBound, child.getLeft()); + final int clampedChildTop = Math.max(topBound, child.getTop()); + final int clampedChildRight = Math.min(rightBound, child.getRight()); + final int clampedChildBottom = Math.min(bottomBound, child.getBottom()); + final int vis; + if (clampedChildLeft >= left && clampedChildTop >= top && + clampedChildRight <= right && clampedChildBottom <= bottom) { + vis = INVISIBLE; + } else { + vis = VISIBLE; + } + child.setVisibility(vis); + } + + void setAllChildrenVisible() { + for (int i = 0, childCount = getChildCount(); i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == INVISIBLE) { + child.setVisibility(VISIBLE); + } + } + } + + private static boolean hasOpaqueBackground(View v) { + final Drawable bg = v.getBackground(); + if (bg != null) { + return bg.getOpacity() == PixelFormat.OPAQUE; + } + return false; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mFirstLayout = true; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mFirstLayout = true; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthMode != MeasureSpec.EXACTLY) { + throw new IllegalStateException("Width must have an exact value or MATCH_PARENT"); + } else if (heightMode != MeasureSpec.EXACTLY) { + throw new IllegalStateException("Height must have an exact value or MATCH_PARENT"); + } + + int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom(); + int panelHeight = mPanelHeight; + + final int childCount = getChildCount(); + + if (childCount > 2) { + Log.e(TAG, "onMeasure: More than two child views are not supported."); + } else if (getChildAt(1) != null && getChildAt(1).getVisibility() == GONE) { + panelHeight = 0; + } + + // We'll find the current one below. + mSlideableView = null; + mCanSlide = false; + + // First pass. Measure based on child LayoutParams width/height. + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + int height = layoutHeight; + if (child.getVisibility() == GONE) { + lp.dimWhenOffset = false; + continue; + } + + if (i == 1) { + lp.slideable = true; + lp.dimWhenOffset = true; + mSlideableView = child; + mCanSlide = true; + } else { + if (!mOverlayContent) { + height -= panelHeight; + } + mMainView = child; + } + + int childWidthSpec; + if (lp.width == LayoutParams.WRAP_CONTENT) { + childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST); + } else if (lp.width == LayoutParams.MATCH_PARENT) { + childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); + } else { + childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); + } + + int childHeightSpec; + if (lp.height == LayoutParams.WRAP_CONTENT) { + childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST); + } else if (lp.height == LayoutParams.MATCH_PARENT) { + childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); + } + + child.measure(childWidthSpec, childHeightSpec); + } + + setMeasuredDimension(widthSize, heightSize); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int paddingLeft = getPaddingLeft(); + final int paddingTop = getPaddingTop(); + final int slidingTop = getSlidingTop(); + + final int childCount = getChildCount(); + + if (mFirstLayout) { + switch (mSlideState) { + case EXPANDED: + mSlideOffset = mCanSlide ? 0.f : 1.f; + break; + case ANCHORED: + mSlideOffset = mCanSlide ? mAnchorPoint : 1.f; + break; + default: + mSlideOffset = 1.f; + break; + } + } + + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child.getVisibility() == GONE) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + final int childHeight = child.getMeasuredHeight(); + + if (lp.slideable) { + mSlideRange = childHeight - mPanelHeight; + } + + int childTop; + if (mIsSlidingUp) { + childTop = lp.slideable ? slidingTop + (int) (mSlideRange * mSlideOffset) : paddingTop; + } else { + childTop = lp.slideable ? slidingTop - (int) (mSlideRange * mSlideOffset) : paddingTop; + if (!lp.slideable && !mOverlayContent) { + childTop += mPanelHeight; + } + } + final int childBottom = childTop + childHeight; + final int childLeft = paddingLeft; + final int childRight = childLeft + child.getMeasuredWidth(); + + child.layout(childLeft, childTop, childRight, childBottom); + } + + if (mFirstLayout) { + updateObscuredViewVisibility(); + } + + mFirstLayout = false; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + // Recalculate sliding panes and their details + if (h != oldh) { + mFirstLayout = true; + } + } + + /** + * Set if the drag view can have its own touch events. If set + * to true, a drag view can scroll horizontally and have its own click listener. + * + * Default is set to false. + */ + public void setEnableDragViewTouchEvents(boolean enabled) { + mIsUsingDragViewTouchEvents = enabled; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + final int action = MotionEventCompat.getActionMasked(ev); + + if (mAnimation != null || !mCanSlide || !mIsSlidingEnabled || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) { + mDragHelper.cancel(); + return super.onInterceptTouchEvent(ev); + } + + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + mDragHelper.cancel(); + return false; + } + + final float x = ev.getX(); + final float y = ev.getY(); + boolean interceptTap = false; + + switch (action) { + case MotionEvent.ACTION_DOWN: { + mIsUnableToDrag = false; + mInitialMotionX = x; + mInitialMotionY = y; + if (isDragViewUnder((int) x, (int) y) && !mIsUsingDragViewTouchEvents) { + interceptTap = true; + } + break; + } + + case MotionEvent.ACTION_MOVE: { + final float adx = Math.abs(x - mInitialMotionX); + final float ady = Math.abs(y - mInitialMotionY); + final int dragSlop = mDragHelper.getTouchSlop(); + + // Handle any horizontal scrolling on the drag view. + if (mIsUsingDragViewTouchEvents) { + if (adx > mScrollTouchSlop && ady < mScrollTouchSlop) { + return super.onInterceptTouchEvent(ev); + } + // Intercept the touch if the drag view has any vertical scroll. + // onTouchEvent will determine if the view should drag vertically. + else if (ady > mScrollTouchSlop) { + interceptTap = isDragViewUnder((int) x, (int) y); + } + } + + if ((ady > dragSlop && adx > ady) || !isDragViewUnder((int) x, (int) y)) { + mDragHelper.cancel(); + mIsUnableToDrag = true; + return false; + } + break; + } + } + + final boolean interceptForDrag = mDragHelper.shouldInterceptTouchEvent(ev); + + return interceptForDrag; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (!mCanSlide || !mIsSlidingEnabled || mAnimation != null) { + return super.onTouchEvent(ev); + } + + mDragHelper.processTouchEvent(ev); + + final int action = ev.getAction(); + boolean wantTouchEvents = true; + + switch (action & MotionEventCompat.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + + //Fix to allow both SettingPanel Drag and Workspace Drag + if (mSlideState == SlideState.COLLAPSED) { + if (y < mSlideableView.getTop()) { + return false; + } + } + + mInitialMotionX = x; + mInitialMotionY = y; + break; + } + + case MotionEvent.ACTION_UP: { + final float x = ev.getX(); + final float y = ev.getY(); + final float dx = x - mInitialMotionX; + final float dy = y - mInitialMotionY; + final int slop = mDragHelper.getTouchSlop(); + View dragView = mDragView != null ? mDragView : mSlideableView; + if (dx * dx + dy * dy < slop * slop && + isDragViewUnder((int) x, (int) y)) { + dragView.playSoundEffect(SoundEffectConstants.CLICK); + if (!isExpanded() && !isAnchored()) { + expandPane(mAnchorPoint); + } else { + collapsePane(); + } + break; + } + break; + } + } + + return wantTouchEvents; + } + + private boolean isDragViewUnder(int x, int y) { + View dragView = mDragView != null ? mDragView : mSlideableView; + if (dragView == null) return false; + int[] viewLocation = new int[2]; + dragView.getLocationOnScreen(viewLocation); + int[] parentLocation = new int[2]; + this.getLocationOnScreen(parentLocation); + int screenX = parentLocation[0] + x; + int screenY = parentLocation[1] + y; + return screenX >= viewLocation[0] && screenX < viewLocation[0] + dragView.getWidth() && + screenY >= viewLocation[1] && screenY < viewLocation[1] + dragView.getHeight(); + } + + private boolean expandPane(View pane, int initialVelocity, float mSlideOffset) { + if (mFirstLayout || smoothSlideTo(mSlideOffset, initialVelocity)) { + return true; + } + return false; + } + + private boolean collapsePane(View pane, int initialVelocity) { + if (mFirstLayout || smoothSlideTo(1.f, initialVelocity)) { + return true; + } + return false; + } + + private int getSlidingTop() { + if (mSlideableView != null) { + return mIsSlidingUp + ? getMeasuredHeight() - getPaddingBottom() - mSlideableView.getMeasuredHeight() + : getPaddingTop(); + } + + return getMeasuredHeight() - getPaddingBottom(); + } + + /** + * Collapse the sliding pane if it is currently slideable. If first layout + * has already completed this will animate. + * + * @return true if the pane was slideable and is now collapsed/in the process of collapsing + */ + public boolean collapsePane() { + return collapsePane(mSlideableView, 0); + } + + /** + * Expand the sliding pane if it is currently slideable. If first layout + * has already completed this will animate. + * + * @return true if the pane was slideable and is now expanded/in the process of expading + */ + public boolean expandPane() { + return expandPane(0); + } + + /** + * Partially expand the sliding pane up to a specific offset + * + * @param mSlideOffset Value between 0 and 1, where 0 is completely expanded. + * @return true if the pane was slideable and is now expanded/in the process of expading + */ + public boolean expandPane(float mSlideOffset) { + if (!isPaneVisible()) { + showPane(); + } + return expandPane(mSlideableView, 0, mSlideOffset); + } + + /** + * Check if the layout is completely expanded. + * + * @return true if sliding panels are completely expanded + */ + public boolean isExpanded() { + return mSlideState == SlideState.EXPANDED; + } + + /** + * Check if the layout is anchored in an intermediate point. + * + * @return true if sliding panels are anchored + */ + public boolean isAnchored() { + return mSlideState == SlideState.ANCHORED; + } + + /** + * Check if the content in this layout cannot fully fit side by side and therefore + * the content pane can be slid back and forth. + * + * @return true if content in this layout can be expanded + */ + public boolean isSlideable() { + return mCanSlide; + } + + public boolean isPaneVisible() { + if (getChildCount() < 2) { + return false; + } + View slidingPane = getChildAt(1); + return slidingPane.getVisibility() == View.VISIBLE; + } + + public void showPane() { + if (getChildCount() < 2) { + return; + } + final View slidingPane = getChildAt(1); + mAnimation = new TranslateAnimation(0, 0, (mIsSlidingUp ? 1 : -1) * getPanelHeight(), 0); + mAnimation.setDuration(400); + mAnimation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + slidingPane.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationEnd(Animation animation) { + requestLayout(); + mAnimation = null; + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + slidingPane.startAnimation(mAnimation); + } + + public void hidePane() { + if (mSlideableView == null) { + return; + } + mAnimation = new TranslateAnimation(0, 0, 0, (mIsSlidingUp ? 1 : -1) * getPanelHeight()); + mAnimation.setDuration(500); + mAnimation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + + } + + @Override + public void onAnimationEnd(Animation animation) { + mSlideableView.setVisibility(View.GONE); + requestLayout(); + mAnimation = null; + } + + @Override + public void onAnimationRepeat(Animation animation) { + + } + }); + mSlideableView.startAnimation(mAnimation); + } + + private void onPanelDragged(int newTop) { + final int topBound = getSlidingTop(); + mSlideOffset = mIsSlidingUp + ? (float) (newTop - topBound) / mSlideRange + : (float) (topBound - newTop) / mSlideRange; + dispatchOnPanelSlide(mSlideableView); + + if (mParallaxOffset > 0) { + int mainViewOffset = getCurrentParallaxOffset(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mMainView.setTranslationY(mainViewOffset); + } else { + mMainView.animate().translationY(mainViewOffset); + } + } + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + boolean result; + final int save = canvas.save(Canvas.CLIP_SAVE_FLAG); + + boolean drawScrim = false; + + if (mCanSlide && !lp.slideable && mSlideableView != null) { + // Clip against the slider; no sense drawing what will immediately be covered, + // Unless the panel is set to overlay content + if (!mOverlayContent) { + canvas.getClipBounds(mTmpRect); + if (mIsSlidingUp) { + mTmpRect.bottom = Math.min(mTmpRect.bottom, mSlideableView.getTop()); + } else { + mTmpRect.top = Math.max(mTmpRect.top, mSlideableView.getBottom()); + } + canvas.clipRect(mTmpRect); + } + if (mSlideOffset < 1) { + drawScrim = true; + } + } + + result = super.drawChild(canvas, child, drawingTime); + canvas.restoreToCount(save); + + if (drawScrim) { + final int baseAlpha = (mCoveredFadeColor & 0xff000000) >>> 24; + final int imag = (int) (baseAlpha * (1 - mSlideOffset)); + final int color = imag << 24 | (mCoveredFadeColor & 0xffffff); + mCoveredFadePaint.setColor(color); + canvas.drawRect(mTmpRect, mCoveredFadePaint); + } + + return result; + } + + /** + * Smoothly animate mDraggingPane to the target X position within its range. + * + * @param slideOffset position to animate to + * @param velocity initial velocity in case of fling, or 0. + */ + boolean smoothSlideTo(float slideOffset, int velocity) { + if (!mCanSlide) { + // Nothing to do. + return false; + } + + final int topBound = getSlidingTop(); + int y = mIsSlidingUp + ? (int) (topBound + slideOffset * mSlideRange) + : (int) (topBound - slideOffset * mSlideRange); + + if (mDragHelper.smoothSlideViewTo(mSlideableView, mSlideableView.getLeft(), y)) { + setAllChildrenVisible(); + ViewCompat.postInvalidateOnAnimation(this); + return true; + } + return false; + } + + @Override + public void computeScroll() { + if (mDragHelper.continueSettling(true)) { + if (!mCanSlide) { + mDragHelper.abort(); + return; + } + + ViewCompat.postInvalidateOnAnimation(this); + } + } + + @Override + public void draw(Canvas c) { + super.draw(c); + + if (mSlideableView == null) { + // No need to draw a shadow if we don't have one. + return; + } + + final int right = mSlideableView.getRight(); + final int top; + final int bottom; + if (mIsSlidingUp) { + top = mSlideableView.getTop() - mShadowHeight; + bottom = mSlideableView.getTop(); + } else { + top = mSlideableView.getBottom(); + bottom = mSlideableView.getBottom() + mShadowHeight; + } + final int left = mSlideableView.getLeft(); + + if (mShadowDrawable != null) { + mShadowDrawable.setBounds(left, top, right, bottom); + mShadowDrawable.draw(c); + } + } + + /** + * Tests scrollability within child views of v given a delta of dx. + * + * @param v View to test for horizontal scrollability + * @param checkV Whether the view v passed should itself be checked for scrollability (true), + * or just its children (false). + * @param dx Delta scrolled in pixels + * @param x X coordinate of the active touch point + * @param y Y coordinate of the active touch point + * @return true if child views of v can be scrolled by delta of dx. + */ + protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { + if (v instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) v; + final int scrollX = v.getScrollX(); + final int scrollY = v.getScrollY(); + final int count = group.getChildCount(); + // Count backwards - let topmost views consume scroll distance first. + for (int i = count - 1; i >= 0; i--) { + final View child = group.getChildAt(i); + if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && + y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && + canScroll(child, true, dx, x + scrollX - child.getLeft(), + y + scrollY - child.getTop())) { + return true; + } + } + } + return checkV && ViewCompat.canScrollHorizontally(v, -dx); + } + + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof MarginLayoutParams + ? new LayoutParams((MarginLayoutParams) p) + : new LayoutParams(p); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + + SavedState ss = new SavedState(superState); + ss.mSlideState = mSlideState; + + return ss; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + mSlideState = ss.mSlideState; + } + + private class DragHelperCallback extends ViewDragHelper.Callback { + + @Override + public boolean tryCaptureView(View child, int pointerId) { + if (mIsUnableToDrag) { + return false; + } + + return ((LayoutParams) child.getLayoutParams()).slideable; + } + + @Override + public void onViewDragStateChanged(int state) { + int anchoredTop = (int)(mAnchorPoint*mSlideRange); + + if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) { + if (mSlideOffset == 0) { + if (mSlideState != SlideState.EXPANDED) { + updateObscuredViewVisibility(); + dispatchOnPanelExpanded(mSlideableView); + mSlideState = SlideState.EXPANDED; + } + } else if (mSlideOffset == (float)anchoredTop/(float)mSlideRange) { + if (mSlideState != SlideState.ANCHORED) { + updateObscuredViewVisibility(); + dispatchOnPanelAnchored(mSlideableView); + mSlideState = SlideState.ANCHORED; + } + } else if (mSlideState != SlideState.COLLAPSED) { + dispatchOnPanelCollapsed(mSlideableView); + mSlideState = SlideState.COLLAPSED; + } + } + } + + @Override + public void onViewCaptured(View capturedChild, int activePointerId) { + // Make all child views visible in preparation for sliding things around + setAllChildrenVisible(); + } + + @Override + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { + onPanelDragged(top); + invalidate(); + } + + @Override + public void onViewReleased(View releasedChild, float xvel, float yvel) { + int top = mIsSlidingUp + ? getSlidingTop() + : getSlidingTop() - mSlideRange; + + if (mAnchorPoint != 0) { + int anchoredTop; + float anchorOffset; + if (mIsSlidingUp) { + anchoredTop = (int)(mAnchorPoint*mSlideRange); + anchorOffset = (float)anchoredTop/(float)mSlideRange; + } else { + anchoredTop = mPanelHeight - (int)(mAnchorPoint*mSlideRange); + anchorOffset = (float)(mPanelHeight - anchoredTop)/(float)mSlideRange; + } + + if (yvel > 0 || (yvel == 0 && mSlideOffset >= (1f+anchorOffset)/2)) { + top += mSlideRange; + } else if (yvel == 0 && mSlideOffset < (1f+anchorOffset)/2 + && mSlideOffset >= anchorOffset/2) { + top += mSlideRange * mAnchorPoint; + } + + } else if (yvel > 0 || (yvel == 0 && mSlideOffset > 0.5f)) { + top += mSlideRange; + } + + mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top); + invalidate(); + } + + @Override + public int getViewVerticalDragRange(View child) { + return mSlideRange; + } + + @Override + public int clampViewPositionVertical(View child, int top, int dy) { + final int topBound; + final int bottomBound; + if (mIsSlidingUp) { + topBound = getSlidingTop(); + bottomBound = topBound + mSlideRange; + } else { + bottomBound = getPaddingTop(); + topBound = bottomBound - mSlideRange; + } + + return Math.min(Math.max(top, topBound), bottomBound); + } + } + + public static class LayoutParams extends ViewGroup.MarginLayoutParams { + private static final int[] ATTRS = new int[] { + android.R.attr.layout_weight + }; + + /** + * True if this pane is the slideable pane in the layout. + */ + boolean slideable; + + /** + * True if this view should be drawn dimmed + * when it's been offset from its default position. + */ + boolean dimWhenOffset; + + Paint dimPaint; + + public LayoutParams() { + super(MATCH_PARENT, MATCH_PARENT); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(android.view.ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(MarginLayoutParams source) { + super(source); + } + + public LayoutParams(LayoutParams source) { + super(source); + } + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + + final TypedArray a = c.obtainStyledAttributes(attrs, ATTRS); + a.recycle(); + } + + } + + static class SavedState extends BaseSavedState { + SlideState mSlideState; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + try { + mSlideState = Enum.valueOf(SlideState.class, in.readString()); + } catch (IllegalArgumentException e) { + mSlideState = SlideState.COLLAPSED; + } + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeString(mSlideState.toString()); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 856e3b88a..59c870a8d 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -68,6 +68,7 @@ import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.AccessibilityDragSource; import com.android.launcher3.accessibility.OverviewScreenAccessibilityDelegate; import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.settings.SettingsProvider; import com.android.launcher3.util.LongArrayMap; import com.android.launcher3.util.Thunk; import com.android.launcher3.util.WallpaperUtils; @@ -217,6 +218,7 @@ public class Workspace extends PagedView private boolean mWorkspaceFadeInAdjacentScreens; WallpaperOffsetInterpolator mWallpaperOffset; + private boolean mScrollWallpaper; @Thunk boolean mWallpaperIsLiveWallpaper; @Thunk int mNumPagesForWallpaperParallax; @Thunk float mLastSetWallpaperOffsetSteps = 0; @@ -290,6 +292,8 @@ public class Workspace extends PagedView } }; + private boolean mHideIconLabels; + /** * Used to inflate the Workspace from XML. * @@ -312,6 +316,10 @@ public class Workspace extends PagedView mOutlineHelper = HolographicOutlineHelper.obtain(context); + mHideIconLabels = SettingsProvider.getBoolean(context, + SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, + R.bool.preferences_interface_homescreen_hide_icon_labels_default); + mLauncher = (Launcher) context; mStateTransitionAnimation = new WorkspaceStateTransitionAnimation(mLauncher, this); final Resources res = getResources(); @@ -964,6 +972,8 @@ public class Workspace extends PagedView */ void addInScreen(View child, long container, long screenId, int x, int y, int spanX, int spanY, boolean insert, boolean computeXYFromRank) { + reloadSettings(); + if (container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { if (getScreenWithId(screenId) == null) { Log.e(TAG, "Skipping child, screenId " + screenId + " not found"); @@ -994,9 +1004,10 @@ public class Workspace extends PagedView screenId = mLauncher.getHotseat().getOrderInHotseat(x, y); } } else { - // Show folder title if not in the hotseat if (child instanceof FolderIcon) { - ((FolderIcon) child).setTextVisible(true); + ((FolderIcon) child).setTextVisible(!mHideIconLabels); + } else if (child instanceof BubbleTextView) { + ((BubbleTextView) child).setTextVisibility(!mHideIconLabels); } layout = getScreenWithId(screenId); child.setOnKeyListener(new IconKeyEventListener()); @@ -1533,7 +1544,8 @@ public class Workspace extends PagedView @Override public void computeScroll() { super.computeScroll(); - mWallpaperOffset.syncWithScroll(); + + if (mScrollWallpaper) mWallpaperOffset.syncWithScroll(); } @Override @@ -1702,6 +1714,8 @@ public class Workspace extends PagedView } } + setScrollingWallpaper(); + // Update wallpaper dimensions if they were changed since last onResume // (we also always set the wallpaper dimensions in the constructor) if (LauncherAppState.getInstance().hasWallpaperChangedSinceLastCheck()) { @@ -1715,7 +1729,8 @@ public class Workspace extends PagedView @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) { + if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount() + && mScrollWallpaper) { mWallpaperOffset.syncWithScroll(); mWallpaperOffset.jumpToFinal(); } @@ -3523,6 +3538,7 @@ public class Workspace extends PagedView case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: view = FolderIcon.fromXml(R.layout.folder_icon, mLauncher, cellLayout, (FolderInfo) info, mIconCache); + ((FolderIcon) view).setTextVisible(!mHideIconLabels); break; default: throw new IllegalStateException("Unknown item type: " + info.itemType); @@ -4482,6 +4498,29 @@ public class Workspace extends PagedView sourceData.putInt(Stats.SOURCE_EXTRA_CONTAINER_PAGE, getCurrentPage()); } + private void reloadSettings() { + mHideIconLabels = SettingsProvider.getBoolean(mLauncher, + SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, + R.bool.preferences_interface_homescreen_hide_icon_labels_default); + + setScrollingWallpaper(); + } + + /** + * Gets the preference for whether to apply scrolling wallpaper effect or not and applies the + * preference. + */ + private void setScrollingWallpaper() { + mScrollWallpaper = SettingsProvider.getBoolean(mLauncher, + SettingsProvider.SETTINGS_UI_HOMESCREEN_SCROLLING_WALLPAPER_SCROLL, + R.bool.preferences_interface_homescreen_scrolling_wallpaper_scroll_default); + if (!mScrollWallpaper) { + if (mWindowToken != null) mWallpaperManager.setWallpaperOffsets(mWindowToken, 0f, 0.5f); + } else { + mWallpaperOffset.syncWithScroll(); + } + } + /** * Used as a workaround to ensure that the AppWidgetService receives the * PACKAGE_ADDED broadcast before updating widgets. diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java index bff7752af..76f47c572 100644 --- a/src/com/android/launcher3/allapps/AllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java @@ -229,6 +229,11 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc updateScrubber(); } + public void reset() { + List apps = mApps.getApps(); + updateApps(apps); + } + /** * Updates existing apps in the list */ diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java index a48390732..17d106731 100644 --- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java +++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java @@ -41,6 +41,7 @@ import com.android.launcher3.BubbleTextView; import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.Utilities; +import com.android.launcher3.settings.SettingsProvider; import com.android.launcher3.util.Thunk; import java.util.HashMap; @@ -356,7 +357,6 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter= firstPosition && position <= lastPosition) { + return; // Already on screen + } + + final int offset = (int) (getHeight() * PREFERRED_SELECTION_OFFSET_FROM_TOP); + if (!mSmoothScrollRequested) { + setSelectionFromTop(position, offset); + + // Since we have changed the scrolling position, we need to redo child layout + // Calling "requestLayout" in the middle of a layout pass has no effect, + // so we call layoutChildren explicitly + super.layoutChildren(); + + } else { + // We will first position the list a couple of screens before or after + // the new selection and then scroll smoothly to it. + int twoScreens = (lastPosition - firstPosition) * 2; + int preliminaryPosition; + if (position < firstPosition) { + preliminaryPosition = position + twoScreens; + if (preliminaryPosition >= getCount()) { + preliminaryPosition = getCount() - 1; + } + if (preliminaryPosition < firstPosition) { + setSelection(preliminaryPosition); + super.layoutChildren(); + } + } else { + preliminaryPosition = position - twoScreens; + if (preliminaryPosition < 0) { + preliminaryPosition = 0; + } + if (preliminaryPosition > lastPosition) { + setSelection(preliminaryPosition); + super.layoutChildren(); + } + } + + + smoothScrollToPositionFromTop(position, offset); + } + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/list/CompositeCursorAdapter.java b/src/com/android/launcher3/list/CompositeCursorAdapter.java new file mode 100644 index 000000000..b163c501c --- /dev/null +++ b/src/com/android/launcher3/list/CompositeCursorAdapter.java @@ -0,0 +1,532 @@ +/* + * 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.launcher3.list; + +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; + +import java.util.ArrayList; + +/** + * A general purpose adapter that is composed of multiple cursors. It just + * appends them in the order they are added. + */ +public abstract class CompositeCursorAdapter extends BaseAdapter { + + private static final int INITIAL_CAPACITY = 2; + + public static class Partition { + boolean showIfEmpty; + boolean hasHeader; + + Cursor cursor; + int idColumnIndex; + int count; + + public Partition(boolean showIfEmpty, boolean hasHeader) { + this.showIfEmpty = showIfEmpty; + this.hasHeader = hasHeader; + } + + /** + * True if the directory should be shown even if no contacts are found. + */ + public boolean getShowIfEmpty() { + return showIfEmpty; + } + + public boolean getHasHeader() { + return hasHeader; + } + } + + private final Context mContext; + private ArrayList mPartitions; + private int mCount = 0; + private boolean mCacheValid = true; + private boolean mNotificationsEnabled = true; + private boolean mNotificationNeeded; + + public CompositeCursorAdapter(Context context) { + this(context, INITIAL_CAPACITY); + } + + public CompositeCursorAdapter(Context context, int initialCapacity) { + mContext = context; + mPartitions = new ArrayList(); + } + + public Context getContext() { + return mContext; + } + + /** + * Registers a partition. The cursor for that partition can be set later. + * Partitions should be added in the order they are supposed to appear in the + * list. + */ + public void addPartition(boolean showIfEmpty, boolean hasHeader) { + addPartition(new Partition(showIfEmpty, hasHeader)); + } + + public void addPartition(Partition partition) { + mPartitions.add(partition); + invalidate(); + notifyDataSetChanged(); + } + + public void addPartition(int location, Partition partition) { + mPartitions.add(location, partition); + invalidate(); + notifyDataSetChanged(); + } + + public void removePartition(int partitionIndex) { + Cursor cursor = mPartitions.get(partitionIndex).cursor; + if (cursor != null && !cursor.isClosed()) { + cursor.close(); + } + mPartitions.remove(partitionIndex); + invalidate(); + notifyDataSetChanged(); + } + + /** + * Removes cursors for all partitions. + */ + // TODO: Is this really what this is supposed to do? Just remove the cursors? Not close them? + // Not remove the partitions themselves? Isn't this leaking? + + public void clearPartitions() { + for (Partition partition : mPartitions) { + partition.cursor = null; + } + invalidate(); + notifyDataSetChanged(); + } + + /** + * Closes all cursors and removes all partitions. + */ + public void close() { + for (Partition partition : mPartitions) { + Cursor cursor = partition.cursor; + if (cursor != null && !cursor.isClosed()) { + cursor.close(); + } + } + mPartitions.clear(); + invalidate(); + notifyDataSetChanged(); + } + + public void setHasHeader(int partitionIndex, boolean flag) { + mPartitions.get(partitionIndex).hasHeader = flag; + invalidate(); + } + + public void setShowIfEmpty(int partitionIndex, boolean flag) { + mPartitions.get(partitionIndex).showIfEmpty = flag; + invalidate(); + } + + public Partition getPartition(int partitionIndex) { + return mPartitions.get(partitionIndex); + } + + protected void invalidate() { + mCacheValid = false; + } + + public int getPartitionCount() { + return mPartitions.size(); + } + + protected void ensureCacheValid() { + if (mCacheValid) { + return; + } + + mCount = 0; + for (Partition partition : mPartitions) { + Cursor cursor = partition.cursor; + int count = cursor != null ? cursor.getCount() : 0; + if (partition.hasHeader) { + if (count != 0 || partition.showIfEmpty) { + count++; + } + } + partition.count = count; + mCount += count; + } + + mCacheValid = true; + } + + /** + * Returns true if the specified partition was configured to have a header. + */ + public boolean hasHeader(int partition) { + return mPartitions.get(partition).hasHeader; + } + + /** + * Returns the total number of list items in all partitions. + */ + public int getCount() { + ensureCacheValid(); + return mCount; + } + + /** + * Returns the cursor for the given partition + */ + public Cursor getCursor(int partition) { + return mPartitions.get(partition).cursor; + } + + /** + * Changes the cursor for an individual partition. + */ + public void changeCursor(int partition, Cursor cursor) { + Cursor prevCursor = mPartitions.get(partition).cursor; + if (prevCursor != cursor) { + if (prevCursor != null && !prevCursor.isClosed()) { + prevCursor.close(); + } + mPartitions.get(partition).cursor = cursor; + if (cursor != null) { + mPartitions.get(partition).idColumnIndex = cursor.getColumnIndex("_id"); + } + invalidate(); + notifyDataSetChanged(); + } + } + + /** + * Returns true if the specified partition has no cursor or an empty cursor. + */ + public boolean isPartitionEmpty(int partition) { + Cursor cursor = mPartitions.get(partition).cursor; + return cursor == null || cursor.getCount() == 0; + } + + /** + * Given a list position, returns the index of the corresponding partition. + */ + public int getPartitionForPosition(int position) { + ensureCacheValid(); + int start = 0; + for (int i = 0, n = mPartitions.size(); i < n; i++) { + int end = start + mPartitions.get(i).count; + if (position >= start && position < end) { + return i; + } + start = end; + } + return -1; + } + + /** + * Given a list position, return the offset of the corresponding item in its + * partition. The header, if any, will have offset -1. + */ + public int getOffsetInPartition(int position) { + ensureCacheValid(); + int start = 0; + for (Partition partition : mPartitions) { + int end = start + partition.count; + if (position >= start && position < end) { + int offset = position - start; + if (partition.hasHeader) { + offset--; + } + return offset; + } + start = end; + } + return -1; + } + + /** + * Returns the first list position for the specified partition. + */ + public int getPositionForPartition(int partition) { + ensureCacheValid(); + int position = 0; + for (int i = 0; i < partition; i++) { + position += mPartitions.get(i).count; + } + return position; + } + + @Override + public int getViewTypeCount() { + return getItemViewTypeCount() + 1; + } + + /** + * Returns the overall number of item view types across all partitions. An + * implementation of this method needs to ensure that the returned count is + * consistent with the values returned by {@link #getItemViewType(int,int)}. + */ + public int getItemViewTypeCount() { + return 1; + } + + /** + * Returns the view type for the list item at the specified position in the + * specified partition. + */ + protected int getItemViewType(int partition, int position) { + return 1; + } + + @Override + public int getItemViewType(int position) { + ensureCacheValid(); + int start = 0; + for (int i = 0, n = mPartitions.size(); i < n; i++) { + int end = start + mPartitions.get(i).count; + if (position >= start && position < end) { + int offset = position - start; + if (mPartitions.get(i).hasHeader) { + offset--; + } + if (offset == -1) { + return IGNORE_ITEM_VIEW_TYPE; + } else { + return getItemViewType(i, offset); + } + } + start = end; + } + + throw new ArrayIndexOutOfBoundsException(position); + } + + public View getView(int position, View convertView, ViewGroup parent) { + ensureCacheValid(); + int start = 0; + for (int i = 0, n = mPartitions.size(); i < n; i++) { + int end = start + mPartitions.get(i).count; + if (position >= start && position < end) { + int offset = position - start; + if (mPartitions.get(i).hasHeader) { + offset--; + } + View view; + if (offset == -1) { + view = getHeaderView(i, mPartitions.get(i).cursor, convertView, parent); + } else { + if (!mPartitions.get(i).cursor.moveToPosition(offset)) { + throw new IllegalStateException("Couldn't move cursor to position " + + offset); + } + view = getView(i, mPartitions.get(i).cursor, offset, convertView, parent); + } + if (view == null) { + throw new NullPointerException("View should not be null, partition: " + i + + " position: " + offset); + } + return view; + } + start = end; + } + + throw new ArrayIndexOutOfBoundsException(position); + } + + /** + * Returns the header view for the specified partition, creating one if needed. + */ + protected View getHeaderView(int partition, Cursor cursor, View convertView, + ViewGroup parent) { + View view = convertView != null + ? convertView + : newHeaderView(mContext, partition, cursor, parent); + bindHeaderView(view, partition, cursor); + return view; + } + + /** + * Creates the header view for the specified partition. + */ + protected View newHeaderView(Context context, int partition, Cursor cursor, + ViewGroup parent) { + return null; + } + + /** + * Binds the header view for the specified partition. + */ + protected void bindHeaderView(View view, int partition, Cursor cursor) { + } + + /** + * Returns an item view for the specified partition, creating one if needed. + */ + protected View getView(int partition, Cursor cursor, int position, View convertView, + ViewGroup parent) { + View view; + if (convertView != null) { + view = convertView; + } else { + view = newView(mContext, partition, cursor, position, parent); + } + bindView(view, partition, cursor, position); + return view; + } + + /** + * Creates an item view for the specified partition and position. Position + * corresponds directly to the current cursor position. + */ + protected abstract View newView(Context context, int partition, Cursor cursor, int position, + ViewGroup parent); + + /** + * Binds an item view for the specified partition and position. Position + * corresponds directly to the current cursor position. + */ + protected abstract void bindView(View v, int partition, Cursor cursor, int position); + + /** + * Returns a pre-positioned cursor for the specified list position. + */ + public Object getItem(int position) { + ensureCacheValid(); + int start = 0; + for (Partition mPartition : mPartitions) { + int end = start + mPartition.count; + if (position >= start && position < end) { + int offset = position - start; + if (mPartition.hasHeader) { + offset--; + } + if (offset == -1) { + return null; + } + Cursor cursor = mPartition.cursor; + cursor.moveToPosition(offset); + return cursor; + } + start = end; + } + + return null; + } + + /** + * Returns the item ID for the specified list position. + */ + public long getItemId(int position) { + ensureCacheValid(); + int start = 0; + for (Partition mPartition : mPartitions) { + int end = start + mPartition.count; + if (position >= start && position < end) { + int offset = position - start; + if (mPartition.hasHeader) { + offset--; + } + if (offset == -1) { + return 0; + } + if (mPartition.idColumnIndex == -1) { + return 0; + } + + Cursor cursor = mPartition.cursor; + if (cursor == null || cursor.isClosed() || !cursor.moveToPosition(offset)) { + return 0; + } + return cursor.getLong(mPartition.idColumnIndex); + } + start = end; + } + + return 0; + } + + /** + * Returns false if any partition has a header. + */ + @Override + public boolean areAllItemsEnabled() { + for (Partition mPartition : mPartitions) { + if (mPartition.hasHeader) { + return false; + } + } + return true; + } + + /** + * Returns true for all items except headers. + */ + @Override + public boolean isEnabled(int position) { + ensureCacheValid(); + int start = 0; + for (int i = 0, n = mPartitions.size(); i < n; i++) { + int end = start + mPartitions.get(i).count; + if (position >= start && position < end) { + int offset = position - start; + if (mPartitions.get(i).hasHeader && offset == 0) { + return false; + } else { + return isEnabled(i, offset); + } + } + start = end; + } + + return false; + } + + /** + * Returns true if the item at the specified offset of the specified + * partition is selectable and clickable. + */ + protected boolean isEnabled(int partition, int position) { + return true; + } + + /** + * Enable or disable data change notifications. It may be a good idea to + * disable notifications before making changes to several partitions at once. + */ + public void setNotificationsEnabled(boolean flag) { + mNotificationsEnabled = flag; + if (flag && mNotificationNeeded) { + notifyDataSetChanged(); + } + } + + @Override + public void notifyDataSetChanged() { + if (mNotificationsEnabled) { + mNotificationNeeded = false; + super.notifyDataSetChanged(); + } else { + mNotificationNeeded = true; + } + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/list/PinnedHeaderListAdapter.java b/src/com/android/launcher3/list/PinnedHeaderListAdapter.java new file mode 100644 index 000000000..cc053e18f --- /dev/null +++ b/src/com/android/launcher3/list/PinnedHeaderListAdapter.java @@ -0,0 +1,137 @@ +/* + * 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.launcher3.list; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +/** + * A subclass of {@link CompositeCursorAdapter} that manages pinned partition headers. + */ +public abstract class PinnedHeaderListAdapter extends CompositeCursorAdapter + implements PinnedHeaderListView.PinnedHeaderAdapter { + + public static final int PARTITION_HEADER_TYPE = 0; + + private boolean mPinnedPartitionHeadersEnabled; + private boolean mHeaderVisibility[]; + + public PinnedHeaderListAdapter(Context context) { + super(context); + } + + public PinnedHeaderListAdapter(Context context, int initialCapacity) { + super(context, initialCapacity); + } + + public boolean getPinnedPartitionHeadersEnabled() { + return mPinnedPartitionHeadersEnabled; + } + + public void setPinnedPartitionHeadersEnabled(boolean flag) { + this.mPinnedPartitionHeadersEnabled = flag; + } + + @Override + public int getPinnedHeaderCount() { + if (mPinnedPartitionHeadersEnabled) { + return getPartitionCount(); + } else { + return 0; + } + } + + protected boolean isPinnedPartitionHeaderVisible(int partition) { + return getPinnedPartitionHeadersEnabled() && hasHeader(partition) + && !isPartitionEmpty(partition); + } + + /** + * The default implementation creates the same type of view as a normal + * partition header. + */ + @Override + public View getPinnedHeaderView(int partition, View convertView, ViewGroup parent) { + if (hasHeader(partition)) { + View view = null; + if (convertView != null) { + Integer headerType = (Integer)convertView.getTag(); + if (headerType != null && headerType == PARTITION_HEADER_TYPE) { + view = convertView; + } + } + if (view == null) { + view = newHeaderView(getContext(), partition, null, parent); + view.setTag(PARTITION_HEADER_TYPE); + view.setFocusable(false); + view.setEnabled(false); + } + bindHeaderView(view, partition, getCursor(partition)); + view.setLayoutDirection(parent.getLayoutDirection()); + return view; + } else { + return null; + } + } + + @Override + public void configurePinnedHeaders(PinnedHeaderListView listView) { + if (!getPinnedPartitionHeadersEnabled()) { + return; + } + + int size = getPartitionCount(); + boolean unCached = false; + // Cache visibility bits, because we will need them several times later on + if (mHeaderVisibility == null || mHeaderVisibility.length != size) { + mHeaderVisibility = new boolean[size]; + unCached = true; + } + for (int i = 0; i < size; i++) { + boolean visible = isPinnedPartitionHeaderVisible(i); + mHeaderVisibility[i] = visible; + if (!visible) { + listView.setHeaderInvisible(i, true); + } + } + + int headerViewsCount = listView.getHeaderViewsCount(); + + // Starting at the top, find and pin headers for partitions preceding the visible one(s) + int topHeaderHeight = 0; + for (int i = 0; i < size; i++) { + if (mHeaderVisibility[i]) { + int position = listView.getPositionAt(topHeaderHeight) - headerViewsCount; + int partition = getPartitionForPosition(position); + if (i > partition) { + break; + } + + if (!unCached){ + listView.setHeaderPinnedAtTop(i, topHeaderHeight, false); + topHeaderHeight += listView.getPinnedHeaderHeight(i); + } + + } + } + } + + @Override + public int getScrollPositionForHeader(int viewIndex) { + return getPositionForPartition(viewIndex); + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/list/PinnedHeaderListView.java b/src/com/android/launcher3/list/PinnedHeaderListView.java new file mode 100644 index 000000000..58e8791ab --- /dev/null +++ b/src/com/android/launcher3/list/PinnedHeaderListView.java @@ -0,0 +1,565 @@ +/* + * 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.launcher3.list; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AbsListView.OnScrollListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ListAdapter; + +/** + * A ListView that maintains a header pinned at the top of the list. The + * pinned header can be pushed up and dissolved as needed. + */ +public class PinnedHeaderListView extends AutoScrollListView + implements OnScrollListener, OnItemSelectedListener { + + /** + * Adapter interface. The list adapter must implement this interface. + */ + public interface PinnedHeaderAdapter { + + /** + * Returns the overall number of pinned headers, visible or not. + */ + int getPinnedHeaderCount(); + + /** + * Creates or updates the pinned header view. + */ + View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent); + + /** + * Configures the pinned headers to match the visible list items. The + * adapter should call {@link PinnedHeaderListView#setHeaderPinnedAtTop}, + * {@link PinnedHeaderListView#setHeaderPinnedAtBottom}, + * {@link PinnedHeaderListView#setFadingHeader} or + * {@link PinnedHeaderListView#setHeaderInvisible}, for each header that + * needs to change its position or visibility. + */ + void configurePinnedHeaders(PinnedHeaderListView listView); + + /** + * Returns the list position to scroll to if the pinned header is touched. + * Return -1 if the list does not need to be scrolled. + */ + int getScrollPositionForHeader(int viewIndex); + } + + private static final int MAX_ALPHA = 255; + private static final int TOP = 0; + private static final int BOTTOM = 1; + private static final int FADING = 2; + + private static final int DEFAULT_ANIMATION_DURATION = 20; + + private static final int DEFAULT_SMOOTH_SCROLL_DURATION = 100; + + private static final class PinnedHeader { + View view; + boolean visible; + int y; + int height; + int alpha; + int state; + + boolean animating; + boolean targetVisible; + int sourceY; + int targetY; + long targetTime; + } + + private PinnedHeaderAdapter mAdapter; + private int mSize; + private PinnedHeader[] mHeaders; + private RectF mBounds = new RectF(); + private Rect mClipRect = new Rect(); + private OnScrollListener mOnScrollListener; + private OnItemSelectedListener mOnItemSelectedListener; + private int mScrollState; + + private boolean mScrollToSectionOnHeaderTouch = false; + private boolean mHeaderTouched = false; + + private int mAnimationDuration = DEFAULT_ANIMATION_DURATION; + private boolean mAnimating; + private long mAnimationTargetTime; + private int mHeaderPaddingStart; + private int mHeaderWidth; + + public PinnedHeaderListView(Context context) { + this(context, null, android.R.attr.listViewStyle); + } + + public PinnedHeaderListView(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.listViewStyle); + } + + public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + super.setOnScrollListener(this); + super.setOnItemSelectedListener(this); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mHeaderPaddingStart = getPaddingStart(); + mHeaderWidth = r - l - mHeaderPaddingStart - getPaddingEnd(); + } + + public void setPinnedHeaderAnimationDuration(int duration) { + mAnimationDuration = duration; + } + + @Override + public void setAdapter(ListAdapter adapter) { + mAdapter = (PinnedHeaderAdapter)adapter; + super.setAdapter(adapter); + } + + @Override + public void setOnScrollListener(OnScrollListener onScrollListener) { + mOnScrollListener = onScrollListener; + super.setOnScrollListener(this); + } + + @Override + public void setOnItemSelectedListener(OnItemSelectedListener listener) { + mOnItemSelectedListener = listener; + super.setOnItemSelectedListener(this); + } + + public void setScrollToSectionOnHeaderTouch(boolean value) { + mScrollToSectionOnHeaderTouch = value; + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + if (mAdapter != null) { + int count = mAdapter.getPinnedHeaderCount(); + if (count != mSize) { + mSize = count; + if (mHeaders == null) { + mHeaders = new PinnedHeader[mSize]; + } else if (mHeaders.length < mSize) { + PinnedHeader[] headers = mHeaders; + mHeaders = new PinnedHeader[mSize]; + System.arraycopy(headers, 0, mHeaders, 0, headers.length); + } + } + + for (int i = 0; i < mSize; i++) { + if (mHeaders[i] == null) { + mHeaders[i] = new PinnedHeader(); + } + mHeaders[i].view = mAdapter.getPinnedHeaderView(i, mHeaders[i].view, this); + } + + mAnimationTargetTime = System.currentTimeMillis() + mAnimationDuration; + mAdapter.configurePinnedHeaders(this); + invalidateIfAnimating(); + + } + if (mOnScrollListener != null) { + mOnScrollListener.onScroll(this, firstVisibleItem, visibleItemCount, totalItemCount); + } + } + + @Override + protected float getTopFadingEdgeStrength() { + // Disable vertical fading at the top when the pinned header is present + return mSize > 0 ? 0 : super.getTopFadingEdgeStrength(); + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + mScrollState = scrollState; + if (mOnScrollListener != null) { + mOnScrollListener.onScrollStateChanged(this, scrollState); + } + } + + /** + * Ensures that the selected item is positioned below the top-pinned headers + * and above the bottom-pinned ones. + */ + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + int height = getHeight(); + + int windowTop = 0; + int windowBottom = height; + + for (int i = 0; i < mSize; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible) { + if (header.state == TOP) { + windowTop = header.y + header.height; + } else if (header.state == BOTTOM) { + windowBottom = header.y; + break; + } + } + } + + View selectedView = getSelectedView(); + if (selectedView != null) { + if (selectedView.getTop() < windowTop) { + setSelectionFromTop(position, windowTop); + } else if (selectedView.getBottom() > windowBottom) { + setSelectionFromTop(position, windowBottom - selectedView.getHeight()); + } + } + + if (mOnItemSelectedListener != null) { + mOnItemSelectedListener.onItemSelected(parent, view, position, id); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + if (mOnItemSelectedListener != null) { + mOnItemSelectedListener.onNothingSelected(parent); + } + } + + public int getPinnedHeaderHeight(int viewIndex) { + ensurePinnedHeaderLayout(viewIndex); + return mHeaders[viewIndex].view.getHeight(); + } + + /** + * Set header to be pinned at the top. + * + * @param viewIndex index of the header view + * @param y is position of the header in pixels. + * @param animate true if the transition to the new coordinate should be animated + */ + public void setHeaderPinnedAtTop(int viewIndex, int y, boolean animate) { + ensurePinnedHeaderLayout(viewIndex); + PinnedHeader header = mHeaders[viewIndex]; + header.visible = true; + header.y = y; + header.state = TOP; + + // TODO perhaps we should animate at the top as well + header.animating = false; + } + + /** + * Set header to be pinned at the bottom. + * + * @param viewIndex index of the header view + * @param y is position of the header in pixels. + * @param animate true if the transition to the new coordinate should be animated + */ + public void setHeaderPinnedAtBottom(int viewIndex, int y, boolean animate) { + ensurePinnedHeaderLayout(viewIndex); + PinnedHeader header = mHeaders[viewIndex]; + header.state = BOTTOM; + if (header.animating) { + header.targetTime = mAnimationTargetTime; + header.sourceY = header.y; + header.targetY = y; + } else if (animate && (header.y != y || !header.visible)) { + if (header.visible) { + header.sourceY = header.y; + } else { + header.visible = true; + header.sourceY = y + header.height; + } + header.animating = true; + header.targetVisible = true; + header.targetTime = mAnimationTargetTime; + header.targetY = y; + } else { + header.visible = true; + header.y = y; + } + } + + /** + * Set header to be pinned at the top of the first visible item. + * + * @param viewIndex index of the header view + * @param position is position of the header in pixels. + */ + public void setFadingHeader(int viewIndex, int position, boolean fade) { + ensurePinnedHeaderLayout(viewIndex); + + View child = getChildAt(position - getFirstVisiblePosition()); + if (child == null) return; + + PinnedHeader header = mHeaders[viewIndex]; + header.visible = true; + header.state = FADING; + header.alpha = MAX_ALPHA; + header.animating = false; + + int top = getTotalTopPinnedHeaderHeight(); + header.y = top; + if (fade) { + int bottom = child.getBottom() - top; + int headerHeight = header.height; + if (bottom < headerHeight) { + int portion = bottom - headerHeight; + header.alpha = MAX_ALPHA * (headerHeight + portion) / headerHeight; + header.y = top + portion; + } + } + } + + /** + * Makes header invisible. + * + * @param viewIndex index of the header view + * @param animate true if the transition to the new coordinate should be animated + */ + public void setHeaderInvisible(int viewIndex, boolean animate) { + PinnedHeader header = mHeaders[viewIndex]; + if (header.visible && (animate || header.animating) && header.state == BOTTOM) { + header.sourceY = header.y; + if (!header.animating) { + header.visible = true; + header.targetY = getBottom() + header.height; + } + header.animating = true; + header.targetTime = mAnimationTargetTime; + header.targetVisible = false; + } else { + header.visible = false; + } + } + + private void ensurePinnedHeaderLayout(int viewIndex) { + View view = mHeaders[viewIndex].view; + if (view.isLayoutRequested()) { + int widthSpec = View.MeasureSpec.makeMeasureSpec(mHeaderWidth, View.MeasureSpec.EXACTLY); + int heightSpec; + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + if (layoutParams != null && layoutParams.height > 0) { + heightSpec = View.MeasureSpec + .makeMeasureSpec(layoutParams.height, View.MeasureSpec.EXACTLY); + } else { + heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + } + view.measure(widthSpec, heightSpec); + int height = view.getMeasuredHeight(); + mHeaders[viewIndex].height = height; + view.layout(0, 0, mHeaderWidth, height); + } + } + + /** + * Returns the sum of heights of headers pinned to the top. + */ + public int getTotalTopPinnedHeaderHeight() { + for (int i = mSize; --i >= 0;) { + PinnedHeader header = mHeaders[i]; + if (header.visible && header.state == TOP) { + return header.y + header.height; + } + } + return 0; + } + + /** + * Returns the list item position at the specified y coordinate. + */ + public int getPositionAt(int y) { + do { + int position = pointToPosition(getPaddingLeft() + 1, y); + if (position != -1) { + return position; + } + // If position == -1, we must have hit a separator. Let's examine + // a nearby pixel + y--; + } while (y > 0); + return 0; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + mHeaderTouched = false; + if (super.onInterceptTouchEvent(ev)) { + return true; + } + + if (mScrollState == SCROLL_STATE_IDLE) { + final int y = (int)ev.getY(); + final int x = (int)ev.getX(); + for (int i = mSize; --i >= 0;) { + PinnedHeader header = mHeaders[i]; + // For RTL layouts, this also takes into account that the scrollbar is on the left + // side. + final int padding = getPaddingLeft(); + if (header.visible && header.y <= y && header.y + header.height > y && + x >= padding && padding + mHeaderWidth >= x) { + mHeaderTouched = true; + if (mScrollToSectionOnHeaderTouch && + ev.getAction() == MotionEvent.ACTION_DOWN) { + return smoothScrollToPartition(i); + } else { + return true; + } + } + } + } + + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mHeaderTouched) { + if (ev.getAction() == MotionEvent.ACTION_UP) { + mHeaderTouched = false; + } + return true; + } + return super.onTouchEvent(ev); + }; + + private boolean smoothScrollToPartition(int partition) { + final int position = mAdapter.getScrollPositionForHeader(partition); + if (position == -1) { + return false; + } + + int offset = 0; + for (int i = 0; i < partition; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible) { + offset += header.height; + } + } + smoothScrollToPositionFromTop(position + getHeaderViewsCount(), offset, + DEFAULT_SMOOTH_SCROLL_DURATION); + return true; + } + + private void invalidateIfAnimating() { + mAnimating = false; + for (int i = 0; i < mSize; i++) { + if (mHeaders[i].animating) { + mAnimating = true; + invalidate(); + return; + } + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + long currentTime = mAnimating ? System.currentTimeMillis() : 0; + + int top = 0; + int bottom = getBottom(); + boolean hasVisibleHeaders = false; + for (int i = 0; i < mSize; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible) { + hasVisibleHeaders = true; + if (header.state == BOTTOM && header.y < bottom) { + bottom = header.y; + } else if (header.state == TOP || header.state == FADING) { + int newTop = header.y + header.height; + if (newTop > top) { + top = newTop; + } + } + } + } + + if (hasVisibleHeaders) { + canvas.save(); + mClipRect.set(0, top, getWidth(), bottom); + canvas.clipRect(mClipRect); + } + + super.dispatchDraw(canvas); + + if (hasVisibleHeaders) { + canvas.restore(); + + // First draw top headers, then the bottom ones to handle the Z axis correctly + for (int i = mSize; --i >= 0;) { + PinnedHeader header = mHeaders[i]; + if (header.visible && (header.state == TOP || header.state == FADING)) { + drawHeader(canvas, header, currentTime); + } + } + + for (int i = 0; i < mSize; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible && header.state == BOTTOM) { + drawHeader(canvas, header, currentTime); + } + } + } + + invalidateIfAnimating(); + } + + private void drawHeader(Canvas canvas, PinnedHeader header, long currentTime) { + if (header.animating) { + int timeLeft = (int)(header.targetTime - currentTime); + if (timeLeft <= 0) { + header.y = header.targetY; + header.visible = header.targetVisible; + header.animating = false; + } else { + header.y = header.targetY + (header.sourceY - header.targetY) * timeLeft + / mAnimationDuration; + } + } + if (header.visible) { + View view = header.view; + int saveCount = canvas.save(); + canvas.translate(isLayoutRtl() ? + getWidth() - mHeaderPaddingStart - mHeaderWidth : mHeaderPaddingStart, + header.y); + if (header.state == FADING) { + mBounds.set(0, 0, mHeaderWidth, view.getHeight()); + canvas.saveLayerAlpha(mBounds, header.alpha, Canvas.ALL_SAVE_FLAG); + } + view.draw(canvas); + canvas.restoreToCount(saveCount); + } + } + + /** + * Note: this is a reimplementation of View.isLayoutRtl() since that is currently hidden api. + */ + public boolean isLayoutRtl() { + return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/list/SettingsPinnedHeaderAdapter.java b/src/com/android/launcher3/list/SettingsPinnedHeaderAdapter.java new file mode 100644 index 000000000..aa2f45171 --- /dev/null +++ b/src/com/android/launcher3/list/SettingsPinnedHeaderAdapter.java @@ -0,0 +1,307 @@ +package com.android.launcher3.list; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Typeface; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.TextView; +import com.android.launcher3.Launcher; +import com.android.launcher3.OverviewSettingsPanel; +import com.android.launcher3.R; +import com.android.launcher3.settings.SettingsProvider; + +public class SettingsPinnedHeaderAdapter extends PinnedHeaderListAdapter { + private Launcher mLauncher; + private Context mContext; + + class SettingsPosition { + int partition = 0; + int position = 0; + + SettingsPosition (int partition, int position) { + this.partition = partition; + this.position = position; + } + } + + public SettingsPinnedHeaderAdapter(Context context) { + super(context); + mLauncher = (Launcher) context; + mContext = context; + } + + private String[] mHeaders; + public int mPinnedHeaderCount; + + public void setHeaders(String[] headers) { + this.mHeaders = headers; + } + + @Override + protected View newHeaderView(Context context, int partition, Cursor cursor, + ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(context); + return inflater.inflate(R.layout.settings_pane_list_header, null); + } + + @Override + protected void bindHeaderView(View view, int partition, Cursor cursor) { + TextView textView = (TextView) view.findViewById(R.id.item_name); + textView.setText(mHeaders[partition]); + textView.setTypeface(textView.getTypeface(), Typeface.BOLD); + + textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); + } + + @Override + protected View newView(Context context, int partition, Cursor cursor, int position, + ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(context); + return inflater.inflate(R.layout.settings_pane_list_item, null); + } + + @Override + protected void bindView(View v, int partition, Cursor cursor, int position) { + TextView text = (TextView)v.findViewById(R.id.item_name); + // RTL + Configuration config = mLauncher.getResources().getConfiguration(); + if (config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + text.setGravity(Gravity.RIGHT); + } + + String title = cursor.getString(1); + text.setText(title); + + v.setTag(new SettingsPosition(partition, position)); + + Resources res = mLauncher.getResources(); + + + boolean current = false; + String state = ""; + + switch (partition) { + case OverviewSettingsPanel.HOME_SETTINGS_POSITION: + switch (position) { + /*case 0: + current = mLauncher.isSearchBarEnabled(); + state = current ? res.getString(R.string.setting_state_on) + : res.getString(R.string.setting_state_off); + ((TextView) v.findViewById(R.id.item_state)).setText(state); + break;*/ + case 1: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, + R.bool.preferences_interface_homescreen_hide_icon_labels_default); + state = current ? res.getString(R.string.icon_labels_hide) + : res.getString(R.string.icon_labels_show); + ((TextView) v.findViewById(R.id.item_state)).setText(state); + break; + case 2: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_HOMESCREEN_SCROLLING_WALLPAPER_SCROLL, + R.bool.preferences_interface_homescreen_scrolling_wallpaper_scroll_default); + state = current ? res.getString(R.string.setting_state_on) + : res.getString(R.string.setting_state_off); + ((TextView) v.findViewById(R.id.item_state)).setText(state); + break; + /*case 3: + updateDynamicGridSizeSettingsItem(v); + break;*/ + default: + ((TextView) v.findViewById(R.id.item_state)).setText(""); + } + break; + case OverviewSettingsPanel.DRAWER_SETTINGS_POSITION: + switch (position) { + case 0: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_DRAWER_HIDE_ICON_LABELS, + R.bool.preferences_interface_drawer_hide_icon_labels_default); + state = current ? res.getString(R.string.icon_labels_hide) + : res.getString(R.string.icon_labels_show); + ((TextView) v.findViewById(R.id.item_state)).setText(state); + break; + default: + ((TextView) v.findViewById(R.id.item_state)).setText(""); + } + break; + default: + switch (position) { + case 0: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_GENERAL_ICONS_LARGE, + R.bool.preferences_interface_general_icons_large_default); + state = current ? res.getString(R.string.setting_state_on) + : res.getString(R.string.setting_state_off); + ((TextView) v.findViewById(R.id.item_state)).setText(state); + break; + default: + ((TextView) v.findViewById(R.id.item_state)).setText(""); + } + } + + v.setOnClickListener(mSettingsItemListener); + } + + @Override + public View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(getContext()); + View view = inflater.inflate(R.layout.settings_pane_list_header, parent, false); + view.setFocusable(false); + view.setEnabled(false); + bindHeaderView(view, viewIndex, null); + return view; + } + + @Override + public int getPinnedHeaderCount() { + return mPinnedHeaderCount; + } + + /*public void updateDynamicGridSizeSettingsItem(View v) { + DeviceProfile.GridSize gridSize = DeviceProfile.GridSize.getModeForValue( + SettingsProvider.getIntCustomDefault(mLauncher, + SettingsProvider.SETTINGS_UI_DYNAMIC_GRID_SIZE, 0)); + String state = ""; + + switch (gridSize) { + case Comfortable: + state = mLauncher.getResources().getString(R.string.grid_size_comfortable); + break; + case Cozy: + state = mLauncher.getResources().getString(R.string.grid_size_cozy); + break; + case Condensed: + state = mLauncher.getResources().getString(R.string.grid_size_condensed); + break; + case Custom: + int rows = SettingsProvider.getIntCustomDefault(mLauncher, + SettingsProvider.SETTINGS_UI_HOMESCREEN_ROWS, 0); + int columns = SettingsProvider.getIntCustomDefault(mLauncher, + SettingsProvider.SETTINGS_UI_HOMESCREEN_COLUMNS, 0); + state = rows + " " + "\u00d7" + " " + columns; + break; + } + ((TextView) v.findViewById(R.id.item_state)).setText(state); + }*/ + + OnClickListener mSettingsItemListener = new OnClickListener() { + + @Override + public void onClick(View v) { + int partition = ((SettingsPosition) v.getTag()).partition; + int position = ((SettingsPosition) v.getTag()).position; + + switch (partition) { + case OverviewSettingsPanel.HOME_SETTINGS_POSITION: + switch (position) { + /*case 0: + updateSearchBarVisibility(v); + mLauncher.setUpdateDynamicGrid(false); + break;*/ + case 1: + onIconLabelsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, + R.bool.preferences_interface_homescreen_hide_icon_labels_default); + mLauncher.reloadLauncher(); + break; + case 2: + onSettingsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_HOMESCREEN_SCROLLING_WALLPAPER_SCROLL, + R.bool.preferences_interface_homescreen_scrolling_wallpaper_scroll_default); + mLauncher.reloadLauncher(); + break; + /*case 3: + mLauncher.onClickDynamicGridSizeButton(); + break;*/ + } + break; + case OverviewSettingsPanel.DRAWER_SETTINGS_POSITION: + switch (position) { + case 0: + onIconLabelsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_DRAWER_HIDE_ICON_LABELS, + R.bool.preferences_interface_drawer_hide_icon_labels_default); + mLauncher.reloadLauncher(); + break; + } + break; + default: + switch (position) { + case 0: + onSettingsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_GENERAL_ICONS_LARGE, + R.bool.preferences_interface_general_icons_large_default); + mLauncher.reloadLauncher(); + break; + /*case 1: + Intent intent = new Intent(); + intent.setClassName(OverviewSettingsPanel.ANDROID_SETTINGS, + OverviewSettingsPanel.ANDROID_PROTECTED_APPS); + mLauncher.startActivity(intent); + break;*/ + } + } + + View defaultHome = mLauncher.findViewById(R.id.default_home_screen_panel); + defaultHome.setVisibility(getCursor(0).getCount() > 1 ? View.VISIBLE : View.GONE); + } + }; + + /*private void updateSearchBarVisibility(View v) { + boolean isSearchEnabled = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_HOMESCREEN_SEARCH, + R.bool.preferences_interface_homescreen_search_default); + + if (!isSearchEnabled) { + if (!Utilities.searchActivityExists(mContext)) { + Toast.makeText(mContext, mContext.getString(R.string.search_activity_not_found), + Toast.LENGTH_SHORT).show(); + return; + } + } + + onSettingsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_HOMESCREEN_SEARCH, + R.bool.preferences_interface_homescreen_search_default); + }*/ + + private void onSettingsBooleanChanged(View v, String key, int res) { + boolean current = SettingsProvider.getBoolean( + mContext, key, res); + + // Set new state + SettingsProvider.putBoolean(mContext, key, !current); + SettingsProvider.putBoolean(mContext, SettingsProvider.SETTINGS_CHANGED, true); + + String state = current ? mLauncher.getResources().getString( + R.string.setting_state_off) : mLauncher.getResources().getString( + R.string.setting_state_on); + ((TextView) v.findViewById(R.id.item_state)).setText(state); + } + + private void onIconLabelsBooleanChanged(View v, String key, int res) { + boolean current = SettingsProvider.getBoolean( + mContext, key, res); + + // Set new state + SettingsProvider.putBoolean(mContext, key, !current); + SettingsProvider.putBoolean(mContext, SettingsProvider.SETTINGS_CHANGED, true); + + String state = current ? mLauncher.getResources().getString( + R.string.icon_labels_show) : mLauncher.getResources().getString( + R.string.icon_labels_hide); + ((TextView) v.findViewById(R.id.item_state)).setText(state); + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/settings/SettingsProvider.java b/src/com/android/launcher3/settings/SettingsProvider.java new file mode 100644 index 000000000..a90478cdf --- /dev/null +++ b/src/com/android/launcher3/settings/SettingsProvider.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.launcher3.settings; + +import android.content.Context; +import android.content.SharedPreferences; + +public final class SettingsProvider { + public static final String SETTINGS_KEY = "trebuchet_preferences"; + public static final String SETTINGS_CHANGED = "settings_changed"; + + public static final String SETTINGS_UI_HOMESCREEN_DEFAULT_SCREEN_ID = "ui_homescreen_default_screen_id"; + public static final String SETTINGS_UI_HOMESCREEN_SEARCH = "ui_homescreen_search"; + public static final String SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS = "ui_homescreen_general_hide_icon_labels"; + public static final String SETTINGS_UI_HOMESCREEN_SCROLLING_WALLPAPER_SCROLL = "ui_homescreen_scrolling_wallpaper_scroll"; + public static final String SETTINGS_UI_DYNAMIC_GRID_SIZE = "ui_dynamic_grid_size"; + public static final String SETTINGS_UI_HOMESCREEN_ROWS = "ui_homescreen_rows"; + public static final String SETTINGS_UI_HOMESCREEN_COLUMNS = "ui_homescreen_columns"; + public static final String SETTINGS_UI_DRAWER_HIDE_ICON_LABELS = "ui_drawer_hide_icon_labels"; + public static final String SETTINGS_UI_DRAWER_COMPACT = "ui_drawer_compact"; + public static final String SETTINGS_UI_GENERAL_ICONS_LARGE = "ui_general_icons_large"; + + public static SharedPreferences get(Context context) { + return context.getSharedPreferences(SETTINGS_KEY, Context.MODE_PRIVATE); + } + + public static int getIntCustomDefault(Context context, String key, int def) { + return get(context).getInt(key, def); + } + + public static int getInt(Context context, String key, int resource) { + return getIntCustomDefault(context, key, context.getResources().getInteger(resource)); + } + + public static long getLongCustomDefault(Context context, String key, long def) { + return get(context).getLong(key, def); + } + + public static long getLong(Context context, String key, int resource) { + return getLongCustomDefault(context, key, context.getResources().getInteger(resource)); + } + + public static boolean getBooleanCustomDefault(Context context, String key, boolean def) { + return get(context).getBoolean(key, def); + } + + public static boolean getBoolean(Context context, String key, int resource) { + return getBooleanCustomDefault(context, key, context.getResources().getBoolean(resource)); + } + + public static String getStringCustomDefault(Context context, String key, String def) { + return get(context).getString(key, def); + } + + public static String getString(Context context, String key, int resource) { + return getStringCustomDefault(context, key, context.getResources().getString(resource)); + } + + public static void putString(Context context, String key, String value) { + get(context).edit().putString(key, value).commit(); + } + + public static void putInt(Context context, String key, int value) { + get(context).edit().putInt(key, value).commit(); + } + + public static void putBoolean(Context context, String key, boolean value) { + get(context).edit().putBoolean(key, value).commit(); + } +} diff --git a/tests/src/com/android/launcher3/InvariantDeviceProfileTest.java b/tests/src/com/android/launcher3/InvariantDeviceProfileTest.java index 1bc7c1190..dc3e05e9d 100644 --- a/tests/src/com/android/launcher3/InvariantDeviceProfileTest.java +++ b/tests/src/com/android/launcher3/InvariantDeviceProfileTest.java @@ -42,7 +42,7 @@ public class InvariantDeviceProfileTest extends AndroidTestCase { protected void setUp() throws Exception { super.setUp(); mInvariantProfile = new InvariantDeviceProfile(getContext()); - mPredefinedDeviceProfiles = mInvariantProfile.getPredefinedDeviceProfiles(); + mPredefinedDeviceProfiles = mInvariantProfile.getPredefinedDeviceProfiles(getContext()); } @Override -- cgit v1.2.3