diff options
25 files changed, 1295 insertions, 478 deletions
diff --git a/Android.mk b/Android.mk index 5267469b3..385380861 100644 --- a/Android.mk +++ b/Android.mk @@ -37,6 +37,9 @@ LOCAL_PROTOC_OPTIMIZE_TYPE := nano LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/protos/ LOCAL_SDK_VERSION := 21 +LOCAL_STATIC_JAVA_LIBRARIES := \ + android-support-v4 \ + android-support-v7-recyclerview LOCAL_PACKAGE_NAME := Launcher3 #LOCAL_CERTIFICATE := shared diff --git a/res/drawable/apps_list_fastscroll_bg.xml b/res/drawable/apps_list_fastscroll_bg.xml new file mode 100644 index 000000000..4ec18488b --- /dev/null +++ b/res/drawable/apps_list_fastscroll_bg.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 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. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="@color/apps_view_scrollbar_thumb_color" /> + <size + android:width="48dp" + android:height="48dp" /> + <corners android:radius="4dp" /> +</shape>
\ No newline at end of file diff --git a/res/drawable/apps_list_scrollbar_thumb.xml b/res/drawable/apps_list_scrollbar_thumb.xml new file mode 100644 index 000000000..ddd65b231 --- /dev/null +++ b/res/drawable/apps_list_scrollbar_thumb.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 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. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="@color/apps_view_scrollbar_thumb_color" /> + <size android:width="4dp"/> +</shape>
\ No newline at end of file diff --git a/res/drawable/apps_list_search_bg.xml b/res/drawable/apps_list_search_bg.xml new file mode 100644 index 000000000..eda33a918 --- /dev/null +++ b/res/drawable/apps_list_search_bg.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 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. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="#ffffff" /> + <corners + android:topLeftRadius="3dp" + android:topRightRadius="3dp" /> +</shape>
\ No newline at end of file diff --git a/res/layout-sw600dp/apps_view.xml b/res/layout-sw600dp/apps_view.xml index 1f773b307..3bb6ec505 100644 --- a/res/layout-sw600dp/apps_view.xml +++ b/res/layout-sw600dp/apps_view.xml @@ -23,12 +23,12 @@ android:descendantFocusability="afterDescendants"> <include layout="@layout/apps_list_reveal_view" - android:layout_width="420dp" - android:layout_height="match_parent" + android:layout_width="@dimen/apps_container_width" + android:layout_height="540dp" android:layout_gravity="center" /> <include layout="@layout/apps_list_view" - android:layout_width="420dp" - android:layout_height="match_parent" + android:layout_width="@dimen/apps_container_width" + android:layout_height="540dp" android:layout_gravity="center" /> </com.android.launcher3.AppsContainerView>
\ No newline at end of file diff --git a/res/layout/apps_grid_row_icon_view.xml b/res/layout/apps_grid_row_icon_view.xml index 11c8eeb4d..81e74b985 100644 --- a/res/layout/apps_grid_row_icon_view.xml +++ b/res/layout/apps_grid_row_icon_view.xml @@ -13,10 +13,17 @@ See the License for the specific language governing permissions and limitations under the License. --> - <com.android.launcher3.BubbleTextView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:launcher="http://schemas.android.com/apk/res-auto" style="@style/WorkspaceIcon.AppsCustomize" - android:id="@+id/application_icon" + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="left|center_vertical" + android:paddingTop="8dp" + android:paddingBottom="8dp" android:focusable="true" - android:background="@drawable/focusable_view_bg" /> + android:background="@drawable/focusable_view_bg" + launcher:deferShadowGeneration="true" /> + diff --git a/res/layout/apps_grid_row_view.xml b/res/layout/apps_grid_row_view.xml deleted file mode 100644 index bce43bc1b..000000000 --- a/res/layout/apps_grid_row_view.xml +++ /dev/null @@ -1,38 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2015 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. ---> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="@dimen/apps_view_row_height" - android:paddingTop="12dp" - android:paddingBottom="12dp" - android:orientation="horizontal" - android:focusable="true" - android:background="@drawable/focusable_view_bg" - android:descendantFocusability="afterDescendants"> - <TextView - android:id="@+id/section" - android:layout_width="48dp" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical" - android:paddingRight="8dp" - android:paddingBottom="12dp" - android:gravity="right" - android:textColor="#1ca195" - android:textSize="16sp" - android:textAllCaps="true" - android:focusable="false" /> -</LinearLayout>
\ No newline at end of file diff --git a/res/layout/apps_list_reveal_view.xml b/res/layout/apps_list_reveal_view.xml index 4a26787c8..19e462bee 100644 --- a/res/layout/apps_list_reveal_view.xml +++ b/res/layout/apps_list_reveal_view.xml @@ -15,7 +15,7 @@ --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/all_apps_transition_overlay" + android:id="@+id/apps_view_transition_overlay" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" diff --git a/res/layout/apps_list_row_icon_view.xml b/res/layout/apps_list_row_icon_view.xml index 607af9b0b..867dbdc99 100644 --- a/res/layout/apps_list_row_icon_view.xml +++ b/res/layout/apps_list_row_icon_view.xml @@ -20,9 +20,10 @@ style="@style/WorkspaceIcon.AppsCustomize" android:id="@+id/application_icon" android:layout_width="match_parent" - android:layout_height="wrap_content" + android:layout_height="match_parent" android:focusable="true" android:background="@drawable/focusable_view_bg" launcher:iconPaddingOverride="24dp" launcher:textSizeOverride="16dp" - launcher:layoutHorizontal="true" /> + launcher:layoutHorizontal="true" + launcher:deferShadowGeneration="true" /> diff --git a/res/layout/apps_list_row_view.xml b/res/layout/apps_list_row_view.xml index c4dcd0018..83c175bb8 100644 --- a/res/layout/apps_list_row_view.xml +++ b/res/layout/apps_list_row_view.xml @@ -26,9 +26,8 @@ android:layout_width="64dp" android:layout_height="match_parent" android:paddingLeft="16dp" - android:gravity="left|center_vertical" - android:textColor="#009688" - android:textSize="24sp" - android:textAllCaps="true" + android:gravity="start|center_vertical" + android:textColor="@color/apps_view_section_text_color" + android:textSize="@dimen/apps_view_section_text_size" android:focusable="false" /> </LinearLayout>
\ No newline at end of file diff --git a/res/layout/apps_list_view.xml b/res/layout/apps_list_view.xml index b1b0f310b..dfb2fc42f 100644 --- a/res/layout/apps_list_view.xml +++ b/res/layout/apps_list_view.xml @@ -13,18 +13,41 @@ See the License for the specific language governing permissions and limitations under the License. --> -<ListView +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/apps_list" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_gravity="center" - android:paddingTop="12dp" - android:paddingBottom="12dp" - android:clipToPadding="false" - android:scrollbars="vertical" + android:orientation="vertical" android:elevation="15dp" android:background="@drawable/apps_list_bg" - android:visibility="gone" - android:focusable="true" - android:descendantFocusability="afterDescendants" />
\ No newline at end of file + android:visibility="gone"> + <EditText + android:id="@+id/app_search_box" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp" + android:hint="@string/apps_view_search_bar_hint" + android:maxLines="1" + android:singleLine="true" + android:scrollHorizontally="true" + android:gravity="fill_horizontal" + android:textSize="16sp" + android:textColor="#4c4c4c" + android:textColorHint="#9c9c9c" + android:imeOptions="flagNoExtractUi" + android:background="@drawable/apps_list_search_bg" + android:elevation="4dp" /> + <com.android.launcher3.AppsContainerRecyclerView + android:id="@+id/apps_list_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:paddingTop="12dp" + android:paddingBottom="12dp" + android:clipToPadding="false" + android:scrollbars="vertical" + android:scrollbarThumbVertical="@drawable/apps_list_scrollbar_thumb" + android:focusable="true" + android:descendantFocusability="afterDescendants" /> +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/apps_view.xml b/res/layout/apps_view.xml index 19ad3d2c9..c1bae63f6 100644 --- a/res/layout/apps_view.xml +++ b/res/layout/apps_view.xml @@ -19,7 +19,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:padding="8dp" - android:background="#22000000" + android:background="@drawable/apps_customize_bg" android:descendantFocusability="afterDescendants"> <include layout="@layout/apps_list_reveal_view" /> diff --git a/res/values-sw600dp/dimens.xml b/res/values-sw600dp/dimens.xml index f7ad0c4cd..d9075872a 100644 --- a/res/values-sw600dp/dimens.xml +++ b/res/values-sw600dp/dimens.xml @@ -18,6 +18,7 @@ <dimen name="app_icon_size">64dp</dimen> <!-- Apps view --> + <dimen name="apps_container_width">480dp</dimen> <dimen name="apps_view_row_height">76dp</dimen> <!-- AppsCustomize --> diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 4e7c59280..845b18230 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -24,6 +24,7 @@ <attr name="iconSizeOverride" format="dimension" /> <attr name="iconPaddingOverride" format="dimension" /> <attr name="textSizeOverride" format="dimension" /> + <attr name="deferShadowGeneration" format="boolean" /> </declare-styleable> <!-- Page Indicator specific attributes. --> diff --git a/res/values/colors.xml b/res/values/colors.xml index 2daf9fe12..590a8872b 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -36,4 +36,8 @@ <color name="outline_color">#FFFFFFFF</color> <color name="widget_text_panel">#FF374248</color> +<!-- Apps view --> + <color name="apps_view_scrollbar_thumb_color">#009688</color> + <color name="apps_view_section_text_color">#009688</color> + </resources> diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 013bd925b..9b4c56094 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -47,7 +47,13 @@ <dimen name="toolbar_button_horizontal_padding">12dip</dimen> <!-- Apps view --> + <dimen name="apps_container_width">0dp</dimen> + <dimen name="apps_grid_view_start_margin">52dp</dimen> <dimen name="apps_view_row_height">64dp</dimen> + <dimen name="apps_view_section_text_size">24sp</dimen> + <dimen name="apps_view_fast_scroll_gutter_size">48dp</dimen> + <dimen name="apps_view_fast_scroll_popup_size">64dp</dimen> + <dimen name="apps_view_fast_scroll_text_size">48dp</dimen> <!-- AllApps/Customize/AppsCustomize --> <!-- The height of the tab bar - if this changes, we should update the diff --git a/res/values/strings.xml b/res/values/strings.xml index 74b88148c..0d113dbf7 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -71,6 +71,10 @@ drop if there are multiple choices. [CHAR_LIMIT=35] --> <string name="external_drop_widget_pick_title">Choose widget to create</string> + <!-- Apps view --> + <!-- Search bar text in the apps view. [CHAR_LIMIT=50] --> + <string name="apps_view_search_bar_hint">Search Apps</string> + <!-- Folders --> <skip /> <!-- Label of Folder name field in Rename folder dialog box --> diff --git a/src/com/android/launcher3/AlphabeticalAppsList.java b/src/com/android/launcher3/AlphabeticalAppsList.java new file mode 100644 index 000000000..2847afc89 --- /dev/null +++ b/src/com/android/launcher3/AlphabeticalAppsList.java @@ -0,0 +1,242 @@ +package com.android.launcher3; + +import android.content.ComponentName; +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import com.android.launcher3.compat.AlphabeticIndexCompat; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + + +/** + * The alphabetically sorted list of applications. + */ +public class AlphabeticalAppsList { + + /** + * Info about a section in the alphabetic list + */ + public class SectionInfo { + public String sectionName; + public int numAppsInSection; + } + + /** + * A filter interface to limit the set of applications in the apps list. + */ + public interface Filter { + public boolean retainApp(AppInfo info); + } + + // Hack to force RecyclerView to break sections + public static final AppInfo SECTION_BREAK_INFO = null; + + private List<AppInfo> mApps = new ArrayList<>(); + private List<AppInfo> mFilteredApps = new ArrayList<>(); + private List<AppInfo> mSectionedFilteredApps = new ArrayList<>(); + private List<SectionInfo> mSections = new ArrayList<>(); + private RecyclerView.Adapter mAdapter; + private Filter mFilter; + private AlphabeticIndexCompat mIndexer; + + public AlphabeticalAppsList(Context context) { + mIndexer = new AlphabeticIndexCompat(context); + } + + /** + * Sets the adapter to notify when this dataset changes. + */ + public void setAdapter(RecyclerView.Adapter adapter) { + mAdapter = adapter; + } + + /** + * Returns sections of all the current filtered applications. + */ + public List<SectionInfo> getSections() { + return mSections; + } + + /** + * Returns the current filtered list of applications broken down into their sections. + */ + public List<AppInfo> getApps() { + return mSectionedFilteredApps; + } + + /** + * Returns the current filtered list of applications. + */ + public List<AppInfo> getAppsWithoutSectionBreaks() { + return mFilteredApps; + } + + /** + * Returns the section name for the application. + */ + public String getSectionNameForApp(AppInfo info) { + String title = info.title.toString(); + String sectionName = mIndexer.getBucketLabel(mIndexer.getBucketIndex(title)); + return sectionName; + } + + /** + * Returns the indexer for this locale. + */ + public AlphabeticIndexCompat getIndexer() { + return mIndexer; + } + + /** + * Sets the current filter for this list of apps. + */ + public void setFilter(Filter f) { + mFilter = f; + onAppsUpdated(); + mAdapter.notifyDataSetChanged(); + } + + /** + * Sets the current set of apps. + */ + public void setApps(List<AppInfo> apps) { + Collections.sort(apps, LauncherModel.getAppNameComparator()); + mApps.clear(); + mApps.addAll(apps); + onAppsUpdated(); + mAdapter.notifyDataSetChanged(); + } + + /** + * Adds new apps to the list. + */ + public void addApps(List<AppInfo> apps) { + // We add it in place, in alphabetical order + for (AppInfo info : apps) { + addApp(info); + } + } + + /** + * Updates existing apps in the list + */ + public void updateApps(List<AppInfo> apps) { + for (AppInfo info : apps) { + int index = mApps.indexOf(info); + if (index != -1) { + mApps.set(index, info); + onAppsUpdated(); + mAdapter.notifyItemChanged(index); + } else { + addApp(info); + } + } + } + + /** + * Removes some apps from the list. + */ + public void removeApps(List<AppInfo> apps) { + for (AppInfo info : apps) { + int removeIndex = findAppByComponent(mApps, info); + if (removeIndex != -1) { + int sectionedIndex = mSectionedFilteredApps.indexOf(info); + int numAppsInSection = numAppsInSection(info); + mApps.remove(removeIndex); + onAppsUpdated(); + if (numAppsInSection == 1) { + // Remove the section and the icon + mAdapter.notifyItemRemoved(sectionedIndex - 1); + mAdapter.notifyItemRemoved(sectionedIndex - 1); + } else { + mAdapter.notifyItemRemoved(sectionedIndex); + } + } + } + } + + /** + * Finds the index of an app given a target AppInfo. + */ + private int findAppByComponent(List<AppInfo> apps, AppInfo targetInfo) { + ComponentName targetComponent = targetInfo.intent.getComponent(); + int length = apps.size(); + for (int i = 0; i < length; ++i) { + AppInfo info = apps.get(i); + if (info.user.equals(info.user) + && info.intent.getComponent().equals(targetComponent)) { + return i; + } + } + return -1; + } + + /** + * Implementation to actually add an app to the alphabetic list + */ + private void addApp(AppInfo info) { + Comparator<AppInfo> appNameComparator = LauncherModel.getAppNameComparator(); + int index = Collections.binarySearch(mApps, info, appNameComparator); + if (index < 0) { + mApps.add(-(index + 1), info); + onAppsUpdated(); + + int sectionedIndex = mSectionedFilteredApps.indexOf(info); + int numAppsInSection = numAppsInSection(info); + if (numAppsInSection == 1) { + // New section added along with icon + mAdapter.notifyItemInserted(sectionedIndex - 1); + mAdapter.notifyItemInserted(sectionedIndex - 1); + } else { + mAdapter.notifyItemInserted(sectionedIndex); + } + } + } + + /** + * Returns the number of apps in the section that the given info is in. + */ + private int numAppsInSection(AppInfo info) { + int appIndex = mFilteredApps.indexOf(info); + int appCount = 0; + for (SectionInfo section : mSections) { + if (appCount + section.numAppsInSection > appIndex) { + return section.numAppsInSection; + } + appCount += section.numAppsInSection; + } + return 1; + } + + /** + * Updates internals when the set of apps are updated. + */ + private void onAppsUpdated() { + // Recreate the filtered apps + mFilteredApps.clear(); + for (AppInfo info : mApps) { + if (mFilter == null || mFilter.retainApp(info)) { + mFilteredApps.add(info); + } + } + + // Section the apps (for convenience for the grid layout) + mSections.clear(); + mSectionedFilteredApps.clear(); + SectionInfo lastSectionInfo = null; + for (AppInfo info : mFilteredApps) { + String sectionName = getSectionNameForApp(info); + if (lastSectionInfo == null || !lastSectionInfo.sectionName.equals(sectionName)) { + lastSectionInfo = new SectionInfo(); + lastSectionInfo.sectionName = sectionName; + mSectionedFilteredApps.add(SECTION_BREAK_INFO); + mSections.add(lastSectionInfo); + } + lastSectionInfo.numAppsInSection++; + mSectionedFilteredApps.add(info); + } + } +} diff --git a/src/com/android/launcher3/AppsContainerRecyclerView.java b/src/com/android/launcher3/AppsContainerRecyclerView.java new file mode 100644 index 000000000..2280e99ef --- /dev/null +++ b/src/com/android/launcher3/AppsContainerRecyclerView.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2015 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; + +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import java.util.List; + +/** + * A RecyclerView with custom fastscroll support. This is the main container for the all apps + * icons. + */ +public class AppsContainerRecyclerView extends RecyclerView + implements RecyclerView.OnItemTouchListener { + + private AlphabeticalAppsList mApps; + private int mNumAppsPerRow; + + private Drawable mFastScrollerBg; + private boolean mDraggingFastScroller; + private String mFastScrollSectionName; + private Paint mFastScrollTextPaint; + private Rect mFastScrollTextBounds = new Rect(); + private float mFastScrollAlpha; + private int mDownX; + private int mDownY; + private int mLastX; + private int mLastY; + private int mGutterSize; + + public AppsContainerRecyclerView(Context context) { + this(context, null); + } + + public AppsContainerRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AppsContainerRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AppsContainerRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr); + + Resources res = context.getResources(); + int fastScrollerSize = res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_popup_size); + mFastScrollerBg = res.getDrawable(R.drawable.apps_list_fastscroll_bg); + mFastScrollerBg.setBounds(0, 0, fastScrollerSize, fastScrollerSize); + mFastScrollTextPaint = new Paint(); + mFastScrollTextPaint.setColor(Color.WHITE); + mFastScrollTextPaint.setAntiAlias(true); + mFastScrollTextPaint.setTextSize(res.getDimensionPixelSize( + R.dimen.apps_view_fast_scroll_text_size)); + mGutterSize = res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_gutter_size); + setFastScrollerAlpha(getFastScrollerAlpha()); + } + + /** + * Sets the list of apps in this view, used to determine the fastscroll position. + */ + public void setApps(AlphabeticalAppsList apps) { + mApps = apps; + } + + /** + * Sets the number of apps per row in this recycler view. + */ + public void setNumAppsPerRow(int rowSize) { + mNumAppsPerRow = rowSize; + } + + /** + * Sets the fast scroller alpha. + */ + public void setFastScrollerAlpha(float alpha) { + mFastScrollAlpha = alpha; + invalidateFastScroller(); + } + + /** + * Gets the fast scroller alpha. + */ + public float getFastScrollerAlpha() { + return mFastScrollAlpha; + } + + @Override + protected void onFinishInflate() { + addOnItemTouchListener(this); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if (mFastScrollAlpha > 0f) { + boolean isRtl = (getResources().getConfiguration().getLayoutDirection() == + LAYOUT_DIRECTION_RTL); + Rect bgBounds = mFastScrollerBg.getBounds(); + int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); + int x; + if (isRtl) { + x = getPaddingLeft() + getScrollBarSize(); + } else { + x = getWidth() - getPaddingRight() - getScrollBarSize() - bgBounds.width(); + } + int y = mLastY - bgBounds.height() / 2; + y = Math.max(getPaddingTop(), Math.min(y, getHeight() - getPaddingBottom() - + bgBounds.height())); + canvas.translate(x, y); + mFastScrollerBg.setAlpha((int) (mFastScrollAlpha * 255)); + mFastScrollerBg.draw(canvas); + mFastScrollTextPaint.setAlpha((int) (mFastScrollAlpha * 255)); + mFastScrollTextPaint.getTextBounds(mFastScrollSectionName, 0, + mFastScrollSectionName.length(), mFastScrollTextBounds); + canvas.drawText(mFastScrollSectionName, + (bgBounds.width() - mFastScrollTextBounds.width()) / 2, + bgBounds.height() - (bgBounds.height() - mFastScrollTextBounds.height()) / 2, + mFastScrollTextPaint); + canvas.restoreToCount(restoreCount); + } + } + + /** + * We intercept the touch handling only to support fast scrolling when initiated from the + * gutter. Otherwise, we fall back to the default RecyclerView touch handling. + */ + @Override + public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) { + return handleTouchEvent(ev); + } + + @Override + public void onTouchEvent(RecyclerView rv, MotionEvent ev) { + handleTouchEvent(ev); + } + + /** + * Handles the touch event and determines whether to show the fast scroller (or updates it if + * it is already showing). + */ + private boolean handleTouchEvent(MotionEvent ev) { + ViewConfiguration config = ViewConfiguration.get(getContext()); + + int action = ev.getAction(); + int x = (int) ev.getX(); + int y = (int) ev.getY(); + switch (action) { + case MotionEvent.ACTION_DOWN: + // Keep track of the down positions + mDownX = mLastX = x; + mDownY = mLastY = y; + stopScroll(); + break; + case MotionEvent.ACTION_MOVE: + // Check if we are scrolling + boolean isRtl = (getResources().getConfiguration().getLayoutDirection() == + LAYOUT_DIRECTION_RTL); + boolean isInGutter; + if (isRtl) { + isInGutter = mDownX < mGutterSize; + } else { + isInGutter = mDownX >= (getWidth() - mGutterSize); + } + if (!mDraggingFastScroller && isInGutter && + Math.abs(y - mDownY) > config.getScaledTouchSlop()) { + getParent().requestDisallowInterceptTouchEvent(true); + mDraggingFastScroller = true; + animateFastScrollerVisibility(true); + } + if (mDraggingFastScroller) { + mLastX = x; + mLastY = y; + + // Scroll to the right position, and update the section name + int top = getPaddingTop() + (mFastScrollerBg.getBounds().height() / 2); + int bottom = getHeight() - getPaddingBottom() - + (mFastScrollerBg.getBounds().height() / 2); + float boundedY = (float) Math.max(top, Math.min(bottom, y)); + mFastScrollSectionName = scrollToPositionAtProgress((boundedY - top) / + (bottom - top)); + invalidateFastScroller(); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mDraggingFastScroller = false; + animateFastScrollerVisibility(false); + break; + } + return mDraggingFastScroller; + + } + + /** + * Animates the visibility of the fast scroller popup. + */ + private void animateFastScrollerVisibility(boolean visible) { + ObjectAnimator anim = ObjectAnimator.ofFloat(this, "fastScrollerAlpha", visible ? 1f : 0f); + anim.setDuration(visible ? 200 : 150); + anim.start(); + } + + /** + * Invalidates the fast scroller popup. + */ + private void invalidateFastScroller() { + invalidate(getWidth() - getPaddingRight() - getScrollBarSize() - + mFastScrollerBg.getIntrinsicWidth(), 0, getWidth(), getHeight()); + } + + /** + * Maps the progress (from 0..1) to the position that should be visible + */ + private String scrollToPositionAtProgress(float progress) { + List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections(); + // Get the total number of rows + int rowCount = 0; + for (AlphabeticalAppsList.SectionInfo info : sections) { + int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow); + rowCount += numRowsInSection; + } + + // Find the index of the first app in that row and scroll to that position + int rowAtProgress = (int) (progress * rowCount); + int appIndex = 0; + rowCount = 0; + for (AlphabeticalAppsList.SectionInfo info : sections) { + int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow); + if (rowCount + numRowsInSection > rowAtProgress) { + appIndex += (rowAtProgress - rowCount) * mNumAppsPerRow; + break; + } + rowCount += numRowsInSection; + appIndex += info.numAppsInSection; + } + appIndex = Math.max(0, Math.min(mApps.getAppsWithoutSectionBreaks().size() - 1, appIndex)); + AppInfo appInfo = mApps.getAppsWithoutSectionBreaks().get(appIndex); + int sectionedAppIndex = mApps.getApps().indexOf(appInfo); + scrollToPosition(sectionedAppIndex); + + // Returns the section name of the row + return mApps.getSectionNameForApp(appInfo); + } +} diff --git a/src/com/android/launcher3/AppsContainerView.java b/src/com/android/launcher3/AppsContainerView.java index cabacec3c..cc31e20fa 100644 --- a/src/com/android/launcher3/AppsContainerView.java +++ b/src/com/android/launcher3/AppsContainerView.java @@ -1,409 +1,64 @@ +/* + * Copyright (C) 2015 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; -import android.content.ComponentName; import android.content.Context; import android.graphics.Point; import android.graphics.Rect; +import android.support.v7.widget.RecyclerView; +import android.text.Editable; +import android.text.TextWatcher; import android.util.AttributeSet; -import android.view.Gravity; -import android.view.LayoutInflater; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.ListView; -import android.widget.SectionIndexer; import android.widget.TextView; +import com.android.launcher3.compat.AlphabeticIndexCompat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; - - -/** - * Represents a row in the apps list view. - */ -class AppsRow { - int sectionId; - String sectionDescription; - List<AppInfo> apps; - - public AppsRow(int sId, String sc, List<AppInfo> ai) { - sectionId = sId; - sectionDescription = sc; - apps = ai; - } - - public AppsRow(int sId, List<AppInfo> ai) { - sectionId = sId; - apps = ai; - } -} - -/** - * An interface to an algorithm that generates app rows. - */ -interface AppRowAlgorithm { - public List<AppsRow> computeAppRows(List<AppInfo> sortedApps, int appsPerRow); - public int getIconViewLayoutId(); - public int getRowViewLayoutId(); - public void bindRowViewIconToInfo(BubbleTextView icon, AppInfo info); -} - -/** - * Computes the rows in the apps list view. - */ -class SectionedAppsAlgorithm implements AppRowAlgorithm { - - @Override - public List<AppsRow> computeAppRows(List<AppInfo> sortedApps, int appsPerRow) { - List<AppsRow> rows = new ArrayList<>(); - LinkedHashMap<String, List<AppInfo>> sections = computeSectionedApps(sortedApps); - int sectionId = 0; - for (Map.Entry<String, List<AppInfo>> sectionEntry : sections.entrySet()) { - String section = sectionEntry.getKey(); - List<AppInfo> apps = sectionEntry.getValue(); - int numRows = (int) Math.ceil((float) apps.size() / appsPerRow); - for (int i = 0; i < numRows; i++) { - List<AppInfo> appsInRow = new ArrayList<>(); - int offset = i * appsPerRow; - for (int j = 0; j < appsPerRow; j++) { - if (offset + j < apps.size()) { - appsInRow.add(apps.get(offset + j)); - } - } - if (i == 0) { - rows.add(new AppsRow(sectionId, section, appsInRow)); - } else { - rows.add(new AppsRow(sectionId, appsInRow)); - } - } - sectionId++; - } - return rows; - } - - @Override - public int getIconViewLayoutId() { - return R.layout.apps_grid_row_icon_view; - } - - @Override - public int getRowViewLayoutId() { - return R.layout.apps_grid_row_view; - } - - private LinkedHashMap<String, List<AppInfo>> computeSectionedApps(List<AppInfo> sortedApps) { - LinkedHashMap<String, List<AppInfo>> sections = new LinkedHashMap<>(); - for (AppInfo info : sortedApps) { - String section = getSection(info); - List<AppInfo> sectionApps = sections.get(section); - if (sectionApps == null) { - sectionApps = new ArrayList<>(); - sections.put(section, sectionApps); - } - sectionApps.add(info); - } - return sections; - } - - @Override - public void bindRowViewIconToInfo(BubbleTextView icon, AppInfo info) { - icon.applyFromApplicationInfo(info); - } - - private String getSection(AppInfo app) { - return app.title.toString().substring(0, 1).toLowerCase(); - } -} - -/** - * Computes the rows in the apps grid view. - */ -class ListedAppsAlgorithm implements AppRowAlgorithm { - - @Override - public List<AppsRow> computeAppRows(List<AppInfo> sortedApps, int appsPerRow) { - List<AppsRow> rows = new ArrayList<>(); - int sectionId = -1; - String prevSection = ""; - for (AppInfo info : sortedApps) { - List<AppInfo> appsInRow = new ArrayList<>(); - appsInRow.add(info); - String section = getSection(info); - if (!prevSection.equals(section)) { - prevSection = section; - sectionId++; - rows.add(new AppsRow(sectionId, section, appsInRow)); - } else { - rows.add(new AppsRow(sectionId, appsInRow)); - } - } - return rows; - } - - @Override - public int getIconViewLayoutId() { - return R.layout.apps_list_row_icon_view; - } - - @Override - public int getRowViewLayoutId() { - return R.layout.apps_list_row_view; - } - - @Override - public void bindRowViewIconToInfo(BubbleTextView icon, AppInfo info) { - icon.applyFromApplicationInfo(info); - } - - private String getSection(AppInfo app) { - return app.title.toString().substring(0, 1).toLowerCase(); - } -} - -/** - * The adapter of all the apps - */ -class AppsListAdapter extends BaseAdapter implements SectionIndexer { - - private LayoutInflater mLayoutInflater; - private List<AppsRow> mAppRows = new ArrayList<>(); - private View.OnTouchListener mTouchListener; - private View.OnClickListener mIconClickListener; - private View.OnLongClickListener mIconLongClickListener; - private AppRowAlgorithm mRowAlgorithm; - private int mAppsPerRow; - - public AppsListAdapter(Context context, View.OnTouchListener touchListener, - View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener) { - mLayoutInflater = LayoutInflater.from(context); - mTouchListener = touchListener; - mIconClickListener = iconClickListener; - mIconLongClickListener = iconLongClickListener; - } - - void setApps(List<AppsRow> apps, int appsPerRow, AppRowAlgorithm algo) { - mAppsPerRow = appsPerRow; - mRowAlgorithm = algo; - mAppRows.clear(); - mAppRows.addAll(apps); - notifyDataSetChanged(); - } - - @Override - public int getCount() { - return mAppRows.size(); - } - - @Override - public Object getItem(int position) { - return mAppRows.get(position); - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - AppsRow info = mAppRows.get(position); - ViewGroup row = (ViewGroup) convertView; - if (row == null) { - // Inflate the row and all the icon children necessary - row = (ViewGroup) mLayoutInflater.inflate(mRowAlgorithm.getRowViewLayoutId(), - parent, false); - for (int i = 0; i < mAppsPerRow; i++) { - BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( - mRowAlgorithm.getIconViewLayoutId(), row, false); - LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(0, - ViewGroup.LayoutParams.WRAP_CONTENT, 1); - lp.gravity = Gravity.CENTER_VERTICAL; - icon.setLayoutParams(lp); - icon.setOnTouchListener(mTouchListener); - icon.setOnClickListener(mIconClickListener); - icon.setOnLongClickListener(mIconLongClickListener); - icon.setFocusable(true); - row.addView(icon); - } - } - // Bind the section header - TextView tv = (TextView) row.findViewById(R.id.section); - if (info.sectionDescription != null) { - tv.setText(info.sectionDescription); - tv.setVisibility(View.VISIBLE); - } else { - tv.setVisibility(View.INVISIBLE); - } - // Bind the icons - for (int i = 0; i < mAppsPerRow; i++) { - BubbleTextView icon = (BubbleTextView) row.getChildAt(i + 1); - if (i < info.apps.size()) { - mRowAlgorithm.bindRowViewIconToInfo(icon, info.apps.get(i)); - icon.setVisibility(View.VISIBLE); - } else { - icon.setVisibility(View.INVISIBLE); - } - } - return row; - } - - @Override - public Object[] getSections() { - ArrayList<Object> sections = new ArrayList<>(); - int prevSectionId = -1; - for (AppsRow row : mAppRows) { - if (row.sectionId != prevSectionId) { - sections.add(row.sectionDescription.toUpperCase()); - prevSectionId = row.sectionId; - } - } - return sections.toArray(); - } - - @Override - public int getPositionForSection(int sectionIndex) { - for (int i = 0; i < mAppRows.size(); i++) { - AppsRow row = mAppRows.get(i); - if (row.sectionId == sectionIndex) { - return i; - } - } - return 0; - } - - @Override - public int getSectionForPosition(int position) { - return mAppRows.get(position).sectionId; - } -} -/** - * The alphabetically sorted list of applications. - */ -class AlphabeticalAppList { - - /** - * Callbacks for when this list is modified. - */ - public interface Callbacks { - public void onAppsUpdated(); - } - - private List<AppInfo> mApps; - private Callbacks mCb; - - public AlphabeticalAppList(Callbacks cb) { - mCb = cb; - } - - /** - * Returns the list of applications. - */ - public List<AppInfo> getApps() { - return mApps; - } - - /** - * Sets the current set of apps. - */ - public void setApps(List<AppInfo> apps) { - Collections.sort(apps, LauncherModel.getAppNameComparator()); - mApps = apps; - mCb.onAppsUpdated(); - } - - /** - * Adds new apps to the list. - */ - public void addApps(List<AppInfo> apps) { - // We add it in place, in alphabetical order - Comparator<AppInfo> appNameComparator = LauncherModel.getAppNameComparator(); - for (AppInfo info : apps) { - // This call will return the exact index of where the item is if >= 0, or the index - // where it should be inserted if < 0. - int index = Collections.binarySearch(mApps, info, appNameComparator); - if (index < 0) { - mApps.add(-(index + 1), info); - } - } - mCb.onAppsUpdated(); - } - - /** - * Updates existing apps in the list - */ - public void updateApps(List<AppInfo> apps) { - Comparator<AppInfo> appNameComparator = LauncherModel.getAppNameComparator(); - for (AppInfo info : apps) { - int index = mApps.indexOf(info); - if (index != -1) { - mApps.set(index, info); - } else { - index = Collections.binarySearch(mApps, info, appNameComparator); - if (index < 0) { - mApps.add(-(index + 1), info); - } - } - } - mCb.onAppsUpdated(); - } - - /** - * Removes some apps from the list. - */ - public void removeApps(List<AppInfo> apps) { - for (AppInfo info : apps) { - int removeIndex = findAppByComponent(mApps, info); - if (removeIndex != -1) { - mApps.remove(removeIndex); - } - } - mCb.onAppsUpdated(); - } - - /** - * Finds the index of an app given a target AppInfo. - */ - private int findAppByComponent(List<AppInfo> apps, AppInfo targetInfo) { - ComponentName targetComponent = targetInfo.intent.getComponent(); - int length = apps.size(); - for (int i = 0; i < length; ++i) { - AppInfo info = apps.get(i); - if (info.user.equals(info.user) - && info.intent.getComponent().equals(targetComponent)) { - return i; - } - } - return -1; - } - -} /** * The all apps list view container. */ public class AppsContainerView extends FrameLayout implements DragSource, View.OnTouchListener, - View.OnLongClickListener, Insettable, AlphabeticalAppList.Callbacks { + View.OnLongClickListener, Insettable, TextWatcher, TextView.OnEditorActionListener, + LauncherTransitionable { - static final int GRID_LAYOUT = 0; - static final int LIST_LAYOUT = 1; - static final int USE_LAYOUT = LIST_LAYOUT; + private static final boolean ALLOW_SINGLE_APP_LAUNCH = true; + + private static final int GRID_LAYOUT = 0; + private static final int LIST_LAYOUT = 1; + private static final int USE_LAYOUT = GRID_LAYOUT; private Launcher mLauncher; - private AppRowAlgorithm mAppRowsAlgorithm; - private AppsListAdapter mAdapter; - private AlphabeticalAppList mApps; - private ListView mList; - private int mAppsRowSize; + private AlphabeticalAppsList mApps; + private RecyclerView.Adapter mAdapter; + private RecyclerView.LayoutManager mLayoutManager; + private RecyclerView.ItemDecoration mItemDecoration; + private AppsContainerRecyclerView mAppsListView; + private EditText mSearchBar; + private int mNumAppsPerRow; private Point mLastTouchDownPos = new Point(); private Rect mPadding = new Rect(); + private int mContentMarginStart; public AppsContainerView(Context context) { this(context, null); @@ -423,15 +78,22 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); mLauncher = (Launcher) context; + mApps = new AlphabeticalAppsList(context); if (USE_LAYOUT == GRID_LAYOUT) { - mAppRowsAlgorithm = new SectionedAppsAlgorithm(); - mAppsRowSize = grid.allAppsRowsSize; + mNumAppsPerRow = grid.appsViewNumCols; + AppsGridAdapter adapter = new AppsGridAdapter(context, mApps, mNumAppsPerRow, this, + mLauncher, this); + mLayoutManager = adapter.getLayoutManager(context); + mItemDecoration = adapter.getItemDecoration(); + mAdapter = adapter; + mContentMarginStart = adapter.getContentMarginStart(); } else if (USE_LAYOUT == LIST_LAYOUT) { - mAppRowsAlgorithm = new ListedAppsAlgorithm(); - mAppsRowSize = 1; + mNumAppsPerRow = 1; + AppsListAdapter adapter = new AppsListAdapter(context, mApps, this, mLauncher, this); + mLayoutManager = adapter.getLayoutManager(context); + mAdapter = adapter; } - mAdapter = new AppsListAdapter(context, this, mLauncher, this); - mApps = new AlphabeticalAppList(this); + mApps.setAdapter(mAdapter); } /** @@ -466,7 +128,7 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O * Scrolls this list view to the top. */ public void scrollToTop() { - mList.scrollTo(0, 0); + mAppsListView.scrollToPosition(0); } /** @@ -480,23 +142,37 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O * Returns the reveal view used for the launcher transitions. */ public View getRevealView() { - return findViewById(R.id.all_apps_transition_overlay); - } - - @Override - public void onAppsUpdated() { - List<AppsRow> rows = mAppRowsAlgorithm.computeAppRows(mApps.getApps(), mAppsRowSize); - mAdapter.setApps(rows, mAppsRowSize, mAppRowsAlgorithm); + return findViewById(R.id.apps_view_transition_overlay); } @Override protected void onFinishInflate() { - mList = (ListView) findViewById(R.id.apps_list); - mList.setFastScrollEnabled(true); - mList.setFastScrollAlwaysVisible(true); - mList.setItemsCanFocus(true); - mList.setAdapter(mAdapter); - mPadding.set(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom()); + boolean isRtl = (getResources().getConfiguration().getLayoutDirection() == + LAYOUT_DIRECTION_RTL); + if (USE_LAYOUT == GRID_LAYOUT) { + ((AppsGridAdapter) mAdapter).setRtl(isRtl); + } + mSearchBar = (EditText) findViewById(R.id.app_search_box); + mSearchBar.addTextChangedListener(this); + mSearchBar.setOnEditorActionListener(this); + mAppsListView = (AppsContainerRecyclerView) findViewById(R.id.apps_list_view); + mAppsListView.setApps(mApps); + mAppsListView.setNumAppsPerRow(mNumAppsPerRow); + mAppsListView.setLayoutManager(mLayoutManager); + mAppsListView.setAdapter(mAdapter); + mAppsListView.setHasFixedSize(true); + if (isRtl) { + mAppsListView.setPadding(mAppsListView.getPaddingLeft(), mAppsListView.getPaddingTop(), + mAppsListView.getPaddingRight() + mContentMarginStart, mAppsListView.getPaddingBottom()); + } else { + mAppsListView.setPadding(mAppsListView.getPaddingLeft() + mContentMarginStart, mAppsListView.getPaddingTop(), + mAppsListView.getPaddingRight(), mAppsListView.getPaddingBottom()); + } + if (mItemDecoration != null) { + mAppsListView.addItemDecoration(mItemDecoration); + } + mPadding.set(getPaddingLeft(), getPaddingTop(), getPaddingRight(), + getPaddingBottom()); } @Override @@ -574,7 +250,8 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O } @Override - public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete, boolean success) { + public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete, + boolean success) { if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) { // Exit spring loaded mode if we have not successfully dropped or have not handled the @@ -606,4 +283,83 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O d.deferDragViewCleanupPostAnimation = false; } } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Do nothing + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Do nothing + } + + @Override + public void afterTextChanged(final Editable s) { + if (s.toString().isEmpty()) { + mApps.setFilter(null); + } else { + final AlphabeticIndexCompat indexer = mApps.getIndexer(); + final String filterText = s.toString().toLowerCase().replaceAll("\\s+", ""); + mApps.setFilter(new AlphabeticalAppsList.Filter() { + @Override + public boolean retainApp(AppInfo info) { + String title = info.title.toString(); + String sectionName = mApps.getSectionNameForApp(info); + return sectionName.toLowerCase().contains(filterText) || + title.toLowerCase().replaceAll("\\s+", "").contains(filterText); + } + }); + } + } + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (ALLOW_SINGLE_APP_LAUNCH && actionId == EditorInfo.IME_ACTION_DONE) { + List<AppInfo> appsWithoutSections = mApps.getAppsWithoutSectionBreaks(); + List<AppInfo> apps = mApps.getApps(); + if (appsWithoutSections.size() == 1) { + mSearchBar.clearFocus(); + mAppsListView.getChildAt(apps.indexOf(appsWithoutSections.get(0))).performClick(); + InputMethodManager imm = (InputMethodManager) + getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(getWindowToken(), 0); + } + return true; + } + return false; + } + + @Override + public View getContent() { + return null; + } + + @Override + public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { + if (!toWorkspace) { + // Disable the focus so that the search bar doesn't get focus + mSearchBar.setFocusableInTouchMode(false); + } + } + + @Override + public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { + // Do nothing + } + + @Override + public void onLauncherTransitionStep(Launcher l, float t) { + // Do nothing + } + + @Override + public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { + if (toWorkspace) { + // Clear the search bar + mSearchBar.setText(""); + } else { + mSearchBar.setFocusableInTouchMode(true); + } + } } diff --git a/src/com/android/launcher3/AppsGridAdapter.java b/src/com/android/launcher3/AppsGridAdapter.java new file mode 100644 index 000000000..6727e4f09 --- /dev/null +++ b/src/com/android/launcher3/AppsGridAdapter.java @@ -0,0 +1,206 @@ +package com.android.launcher3; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.launcher3.compat.AlphabeticIndexCompat; + + +/** + * The grid view adapter of all the apps. + */ +class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> { + + public static final String TAG = "AppsGridAdapter"; + + private static final int SECTION_BREAK_VIEW_TYPE = 0; + private static final int ICON_VIEW_TYPE = 1; + + /** + * ViewHolder for each icon. + */ + public static class ViewHolder extends RecyclerView.ViewHolder { + public View mContent; + public boolean mIsSectionRow; + + public ViewHolder(View v, boolean isSectionRow) { + super(v); + mContent = v; + mIsSectionRow = isSectionRow; + } + } + + /** + * Helper class to size the grid items. + */ + public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup { + @Override + public int getSpanSize(int position) { + AppInfo info = mApps.getApps().get(position); + if (info == AlphabeticalAppsList.SECTION_BREAK_INFO) { + return mAppsPerRow; + } else { + return 1; + } + } + } + + /** + * Helper class to draw the section headers + */ + public class GridItemDecoration extends RecyclerView.ItemDecoration { + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + AlphabeticIndexCompat indexer = mApps.getIndexer(); + for (int i = 0; i < parent.getChildCount(); i++) { + View child = parent.getChildAt(i); + ViewHolder holder = (ViewHolder) parent.getChildViewHolder(child); + if (holder != null) { + GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) + child.getLayoutParams(); + if (!holder.mIsSectionRow && !lp.isItemRemoved()) { + if (mApps.getApps().get(holder.getPosition() - 1) == + AlphabeticalAppsList.SECTION_BREAK_INFO) { + // Draw at the parent + AppInfo info = mApps.getApps().get(holder.getPosition()); + String section = mApps.getSectionNameForApp(info); + mSectionTextPaint.getTextBounds(section, 0, section.length(), + mTmpBounds); + if (mIsRtl) { + c.drawText(section, parent.getWidth() - mStartMargin + + (mStartMargin - mTmpBounds.width()) / 2, + child.getTop() + (2 * child.getPaddingTop()) + + mTmpBounds.height(), mSectionTextPaint); + } else { + c.drawText(section, (mStartMargin - mTmpBounds.width()) / 2, + child.getTop() + (2 * child.getPaddingTop()) + + mTmpBounds.height(), mSectionTextPaint); + } + } + } + } + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + // Do nothing + } + } + + private LayoutInflater mLayoutInflater; + private AlphabeticalAppsList mApps; + private GridSpanSizer mGridSizer; + private GridItemDecoration mItemDecoration; + private View.OnTouchListener mTouchListener; + private View.OnClickListener mIconClickListener; + private View.OnLongClickListener mIconLongClickListener; + private int mAppsPerRow; + private boolean mIsRtl; + + // Section drawing + private int mStartMargin; + private Paint mSectionTextPaint; + private Rect mTmpBounds = new Rect(); + + + public AppsGridAdapter(Context context, AlphabeticalAppsList apps, int appsPerRow, + View.OnTouchListener touchListener, View.OnClickListener iconClickListener, + View.OnLongClickListener iconLongClickListener) { + Resources res = context.getResources(); + mApps = apps; + mAppsPerRow = appsPerRow; + mGridSizer = new GridSpanSizer(); + mItemDecoration = new GridItemDecoration(); + mLayoutInflater = LayoutInflater.from(context); + mTouchListener = touchListener; + mIconClickListener = iconClickListener; + mIconLongClickListener = iconLongClickListener; + mStartMargin = res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin); + mSectionTextPaint = new Paint(); + mSectionTextPaint.setTextSize(res.getDimensionPixelSize( + R.dimen.apps_view_section_text_size)); + mSectionTextPaint.setColor(res.getColor(R.color.apps_view_section_text_color)); + mSectionTextPaint.setAntiAlias(true); + } + + /** + * Sets whether we are in RTL mode. + */ + public void setRtl(boolean rtl) { + mIsRtl = rtl; + } + + /** + * Returns the grid layout manager. + */ + public GridLayoutManager getLayoutManager(Context context) { + GridLayoutManager layoutMgr = new GridLayoutManager(context, mAppsPerRow, + GridLayoutManager.VERTICAL, false); + layoutMgr.setSpanSizeLookup(mGridSizer); + return layoutMgr; + } + + /** + * Returns the item decoration for the recycler view. + */ + public RecyclerView.ItemDecoration getItemDecoration() { + return mItemDecoration; + } + + /** + * Returns the left padding for the recycler view. + */ + public int getContentMarginStart() { + return mStartMargin; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + switch (viewType) { + case SECTION_BREAK_VIEW_TYPE: + return new ViewHolder(new View(parent.getContext()), true); + case ICON_VIEW_TYPE: + BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( + R.layout.apps_grid_row_icon_view, parent, false); + icon.setOnTouchListener(mTouchListener); + icon.setOnClickListener(mIconClickListener); + icon.setOnLongClickListener(mIconLongClickListener); + icon.setFocusable(true); + return new ViewHolder(icon, false); + default: + throw new RuntimeException("Unexpected view type"); + } + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + AppInfo info = mApps.getApps().get(position); + if (info != AlphabeticalAppsList.SECTION_BREAK_INFO) { + BubbleTextView icon = (BubbleTextView) holder.mContent; + icon.applyFromApplicationInfo(info); + } + } + + @Override + public int getItemCount() { + return mApps.getApps().size(); + } + + @Override + public int getItemViewType(int position) { + if (mApps.getApps().get(position) == AlphabeticalAppsList.SECTION_BREAK_INFO) { + return SECTION_BREAK_VIEW_TYPE; + } + return ICON_VIEW_TYPE; + } +} diff --git a/src/com/android/launcher3/AppsListAdapter.java b/src/com/android/launcher3/AppsListAdapter.java new file mode 100644 index 000000000..8ac381e79 --- /dev/null +++ b/src/com/android/launcher3/AppsListAdapter.java @@ -0,0 +1,119 @@ +package com.android.launcher3; + +import android.content.Context; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; +import com.android.launcher3.compat.AlphabeticIndexCompat; + +/** + * The linear list view adapter for all the apps. + */ +class AppsListAdapter extends RecyclerView.Adapter<AppsListAdapter.ViewHolder> { + + /** + * ViewHolder for each row. + */ + public static class ViewHolder extends RecyclerView.ViewHolder { + public View mContent; + + public ViewHolder(View v) { + super(v); + mContent = v; + } + } + + private static final int SECTION_BREAK_VIEW_TYPE = 0; + private static final int ICON_VIEW_TYPE = 1; + + private LayoutInflater mLayoutInflater; + private AlphabeticalAppsList mApps; + private View.OnTouchListener mTouchListener; + private View.OnClickListener mIconClickListener; + private View.OnLongClickListener mIconLongClickListener; + + public AppsListAdapter(Context context, AlphabeticalAppsList apps, + View.OnTouchListener touchListener, View.OnClickListener iconClickListener, + View.OnLongClickListener iconLongClickListener) { + mApps = apps; + mLayoutInflater = LayoutInflater.from(context); + mTouchListener = touchListener; + mIconClickListener = iconClickListener; + mIconLongClickListener = iconLongClickListener; + } + + public RecyclerView.LayoutManager getLayoutManager(Context context) { + return new LinearLayoutManager(context); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + switch (viewType) { + case SECTION_BREAK_VIEW_TYPE: + return new ViewHolder(new View(parent.getContext())); + case ICON_VIEW_TYPE: + // Inflate the row and all the icon children necessary + ViewGroup row = (ViewGroup) mLayoutInflater.inflate(R.layout.apps_list_row_view, + parent, false); + BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( + R.layout.apps_list_row_icon_view, row, false); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(0, + ViewGroup.LayoutParams.WRAP_CONTENT, 1); + lp.gravity = Gravity.CENTER_VERTICAL; + icon.setLayoutParams(lp); + icon.setOnTouchListener(mTouchListener); + icon.setOnClickListener(mIconClickListener); + icon.setOnLongClickListener(mIconLongClickListener); + icon.setFocusable(true); + row.addView(icon); + return new ViewHolder(row); + default: + throw new RuntimeException("Unexpected view type"); + } + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + AppInfo info = mApps.getApps().get(position); + if (info != AlphabeticalAppsList.SECTION_BREAK_INFO) { + ViewGroup content = (ViewGroup) holder.mContent; + String sectionDescription = mApps.getSectionNameForApp(info); + + // Bind the section header + boolean showSectionHeader = true; + if (position > 0) { + AppInfo prevInfo = mApps.getApps().get(position - 1); + showSectionHeader = (prevInfo == AlphabeticalAppsList.SECTION_BREAK_INFO); + } + TextView tv = (TextView) content.findViewById(R.id.section); + if (showSectionHeader) { + tv.setText(sectionDescription); + tv.setVisibility(View.VISIBLE); + } else { + tv.setVisibility(View.INVISIBLE); + } + + // Bind the icon + BubbleTextView icon = (BubbleTextView) content.getChildAt(1); + icon.applyFromApplicationInfo(info); + } + } + + @Override + public int getItemCount() { + return mApps.getApps().size(); + } + + @Override + public int getItemViewType(int position) { + if (mApps.getApps().get(position) == AlphabeticalAppsList.SECTION_BREAK_INFO) { + return SECTION_BREAK_VIEW_TYPE; + } + return ICON_VIEW_TYPE; + } +} diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 5ea84aeb2..fabae5702 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -63,6 +63,7 @@ public class BubbleTextView extends TextView { private float mSlop; + private final boolean mDeferShadowGenerationOnTouch; private final boolean mCustomShadowsEnabled; private final boolean mLayoutHorizontal; private final int mIconSize; @@ -96,6 +97,8 @@ public class BubbleTextView extends TextView { grid.iconDrawablePaddingPx); mTextSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_textSizeOverride, grid.allAppsIconTextSizePx); + mDeferShadowGenerationOnTouch = + a.getBoolean(R.styleable.BubbleTextView_deferShadowGeneration, false); a.recycle(); if (mCustomShadowsEnabled) { @@ -218,7 +221,7 @@ public class BubbleTextView extends TextView { // So that the pressed outline is visible immediately on setStayPressed(), // we pre-create it on ACTION_DOWN (it takes a small but perceptible amount of time // to create it) - if (mPressedBackground == null) { + if (!mDeferShadowGenerationOnTouch && mPressedBackground == null) { mPressedBackground = mOutlineHelper.createMediumDropShadow(this); } @@ -247,6 +250,10 @@ public class BubbleTextView extends TextView { mStayPressed = stayPressed; if (!stayPressed) { mPressedBackground = null; + } else { + if (mPressedBackground == null) { + mPressedBackground = mOutlineHelper.createMediumDropShadow(this); + } } // Only show the shadow effect when persistent pressed state is set. diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index b5bb55ca7..ddd300257 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -122,8 +122,7 @@ public class DeviceProfile { int hotseatAllAppsRank; int allAppsNumRows; int allAppsNumCols; - // TODO(winsonc): to be used with the grid layout - int allAppsRowsSize; + int appsViewNumCols; int searchBarSpaceWidthPx; int searchBarSpaceHeightPx; int pageIndicatorHeightPx; @@ -365,7 +364,7 @@ public class DeviceProfile { } } - private void updateIconSize(float scale, int drawablePadding, Resources resources, + private void updateIconSize(float scale, int drawablePadding, Resources res, DisplayMetrics dm) { iconSizePx = (int) (DynamicGrid.pxFromDp(iconSize, dm) * scale); iconTextSizePx = (int) (DynamicGrid.pxFromSp(iconTextSize, dm) * scale); @@ -374,9 +373,9 @@ public class DeviceProfile { // Search Bar searchBarSpaceWidthPx = Math.min(widthPx, - resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width)); + res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width)); searchBarSpaceHeightPx = getSearchBarTopOffset() - + resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height); + + res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height); // Calculate the actual text height Paint textPaint = new Paint(); @@ -384,7 +383,7 @@ public class DeviceProfile { FontMetrics fm = textPaint.getFontMetrics(); cellWidthPx = iconSizePx; cellHeightPx = iconSizePx + iconDrawablePaddingPx + (int) Math.ceil(fm.bottom - fm.top); - final float scaleDps = resources.getDimensionPixelSize(R.dimen.dragViewScale); + final float scaleDps = res.getDimensionPixelSize(R.dimen.dragViewScale); dragViewScale = (iconSizePx + scaleDps) / iconSizePx; // Hotseat @@ -402,11 +401,11 @@ public class DeviceProfile { allAppsCellWidthPx = allAppsIconSizePx; allAppsCellHeightPx = allAppsIconSizePx + drawablePadding + iconTextSizePx; int maxLongEdgeCellCount = - resources.getInteger(R.integer.config_dynamic_grid_max_long_edge_cell_count); + res.getInteger(R.integer.config_dynamic_grid_max_long_edge_cell_count); int maxShortEdgeCellCount = - resources.getInteger(R.integer.config_dynamic_grid_max_short_edge_cell_count); + res.getInteger(R.integer.config_dynamic_grid_max_short_edge_cell_count); int minEdgeCellCount = - resources.getInteger(R.integer.config_dynamic_grid_min_edge_cell_count); + res.getInteger(R.integer.config_dynamic_grid_min_edge_cell_count); int maxRows = (isLandscape ? maxShortEdgeCellCount : maxLongEdgeCellCount); int maxCols = (isLandscape ? maxLongEdgeCellCount : maxShortEdgeCellCount); @@ -417,10 +416,17 @@ public class DeviceProfile { allAppsNumRows = (availableHeightPx - pageIndicatorHeightPx) / (allAppsCellHeightPx + allAppsCellPaddingPx); allAppsNumRows = Math.max(minEdgeCellCount, Math.min(maxRows, allAppsNumRows)); - allAppsNumCols = (availableWidthPx) / - (allAppsCellWidthPx + allAppsCellPaddingPx); + allAppsNumCols = (availableWidthPx) / (allAppsCellWidthPx + allAppsCellPaddingPx); allAppsNumCols = Math.max(minEdgeCellCount, Math.min(maxCols, allAppsNumCols)); } + + int appsContainerViewPx = res.getDimensionPixelSize(R.dimen.apps_container_width); + int appsViewLeftMarginPx = + res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin); + int availableAppsWidthPx = (appsContainerViewPx > 0) ? appsContainerViewPx : + availableWidthPx; + appsViewNumCols = (availableAppsWidthPx - appsViewLeftMarginPx) / + (allAppsCellWidthPx + allAppsCellPaddingPx); } void updateFromConfiguration(Context context, Resources resources, int wPx, int hPx, diff --git a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java new file mode 100644 index 000000000..602a84566 --- /dev/null +++ b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java @@ -0,0 +1,131 @@ +package com.android.launcher3.compat; + +import android.content.Context; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Locale; + +/** + * Fallback class to support Alphabetic indexing if not supported by the framework. + * TODO(winsonc): disable for non-english locales + */ +class BaseAlphabeticIndex { + + private static final String BUCKETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-"; + private static final int UNKNOWN_BUCKET_INDEX = BUCKETS.length() - 1; + + public BaseAlphabeticIndex() {} + + /** + * Sets the max number of the label buckets in this index. + */ + public void setMaxLabelCount(int count) { + // Not currently supported + } + + /** + * Returns the index of the bucket in which the given string should appear. + */ + public int getBucketIndex(String s) { + if (s.isEmpty()) { + return UNKNOWN_BUCKET_INDEX; + } + int index = BUCKETS.indexOf(s.substring(0, 1).toUpperCase()); + if (index != -1) { + return index; + } + return UNKNOWN_BUCKET_INDEX; + } + + /** + * Returns the label for the bucket at the given index (as returned by getBucketIndex). + */ + public String getBucketLabel(int index) { + return BUCKETS.substring(index, index + 1); + } +} + +/** + * Reflected libcore.icu.AlphabeticIndex implementation, falls back to the base alphabetic index. + */ +public class AlphabeticIndexCompat extends BaseAlphabeticIndex { + + private Object mAlphabeticIndex; + private Method mAddLabelsMethod; + private Method mSetMaxLabelCountMethod; + private Method mGetBucketIndexMethod; + private Method mGetBucketLabelMethod; + private boolean mHasValidAlphabeticIndex; + + public AlphabeticIndexCompat(Context context) { + super(); + try { + Locale curLocale = context.getResources().getConfiguration().locale; + Class clazz = Class.forName("libcore.icu.AlphabeticIndex"); + Constructor ctor = clazz.getConstructor(Locale.class); + mAddLabelsMethod = clazz.getDeclaredMethod("addLabels", Locale.class); + mSetMaxLabelCountMethod = clazz.getDeclaredMethod("setMaxLabelCount", int.class); + mGetBucketIndexMethod = clazz.getDeclaredMethod("getBucketIndex", String.class); + mGetBucketLabelMethod = clazz.getDeclaredMethod("getBucketLabel", int.class); + mAlphabeticIndex = ctor.newInstance(curLocale); + try { + // Ensure we always have some base English locale buckets + if (!curLocale.getLanguage().equals(new Locale("en").getLanguage())) { + mAddLabelsMethod.invoke(mAlphabeticIndex, Locale.ENGLISH); + } + } catch (Exception e) { + e.printStackTrace(); + } + mHasValidAlphabeticIndex = true; + } catch (Exception e) { + mHasValidAlphabeticIndex = false; + } + } + + /** + * Sets the max number of the label buckets in this index. + * (ICU 51 default is 99) + */ + public void setMaxLabelCount(int count) { + if (mHasValidAlphabeticIndex) { + try { + mSetMaxLabelCountMethod.invoke(mAlphabeticIndex, count); + } catch (Exception e) { + e.printStackTrace(); + } + } else { + super.setMaxLabelCount(count); + } + } + + /** + * Returns the index of the bucket in which {@param s} should appear. + * Function is synchronized because underlying routine walks an iterator + * whose state is maintained inside the index object. + */ + public int getBucketIndex(String s) { + if (mHasValidAlphabeticIndex) { + try { + return (Integer) mGetBucketIndexMethod.invoke(mAlphabeticIndex, s); + } catch (Exception e) { + e.printStackTrace(); + } + } + return super.getBucketIndex(s); + } + + /** + * Returns the label for the bucket at the given index (as returned by getBucketIndex). + */ + public String getBucketLabel(int index) { + if (mHasValidAlphabeticIndex) { + try { + return (String) mGetBucketLabelMethod.invoke(mAlphabeticIndex, index); + } catch (Exception e) { + e.printStackTrace(); + } + } + return super.getBucketLabel(index); + } +} |