diff options
author | Sunny Goyal <sunnygoyal@google.com> | 2015-09-10 16:34:09 -0700 |
---|---|---|
committer | Sunny Goyal <sunnygoyal@google.com> | 2015-09-10 17:22:17 -0700 |
commit | ea9ad5cead9ad894fb670476bb5381198cdcf2de (patch) | |
tree | fcf23a25aadc660bd2dbb8b5178da454e8a00f08 /src/com/android/launcher3/allapps | |
parent | 5845d3dbea53d513466c98b301eb49e96fe5d6a3 (diff) | |
parent | 4abaf133546b0c950edc82594985e9da50d9c1dd (diff) | |
download | android_packages_apps_Trebuchet-ea9ad5cead9ad894fb670476bb5381198cdcf2de.tar.gz android_packages_apps_Trebuchet-ea9ad5cead9ad894fb670476bb5381198cdcf2de.tar.bz2 android_packages_apps_Trebuchet-ea9ad5cead9ad894fb670476bb5381198cdcf2de.zip |
Merge remote-tracking branch 'origin/ub-launcher3-burnaby' into mnc-dev
Conflicts:
Android.mk
Change-Id: I05429e418a25b94e7669e002d39bc70806396b8e
Diffstat (limited to 'src/com/android/launcher3/allapps')
7 files changed, 581 insertions, 158 deletions
diff --git a/src/com/android/launcher3/allapps/AllAppsBackgroundDrawable.java b/src/com/android/launcher3/allapps/AllAppsBackgroundDrawable.java new file mode 100644 index 000000000..117aca921 --- /dev/null +++ b/src/com/android/launcher3/allapps/AllAppsBackgroundDrawable.java @@ -0,0 +1,194 @@ +/* + * 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.allapps; + +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +import android.view.Gravity; +import com.android.launcher3.R; + +/** + * A helper class to positon and orient a drawable to be drawn. + */ +class TransformedImageDrawable { + private Drawable mImage; + private float mXPercent; + private float mYPercent; + private int mGravity; + + /** + * @param gravity If one of the Gravity center values, the x and y offset will take the width + * and height of the image into account to center the image to the offset. + */ + public TransformedImageDrawable(Resources res, int resourceId, float xPct, float yPct, + int gravity) { + mImage = res.getDrawable(resourceId); + mXPercent = xPct; + mYPercent = yPct; + mGravity = gravity; + } + + public void setAlpha(int alpha) { + mImage.setAlpha(alpha); + } + + public int getAlpha() { + return mImage.getAlpha(); + } + + public void updateBounds(Rect bounds) { + int width = mImage.getIntrinsicWidth(); + int height = mImage.getIntrinsicHeight(); + int left = bounds.left + (int) (mXPercent * bounds.width()); + int top = bounds.top + (int) (mYPercent * bounds.height()); + if ((mGravity & Gravity.CENTER_HORIZONTAL) == Gravity.CENTER_HORIZONTAL) { + left -= (width / 2); + } + if ((mGravity & Gravity.CENTER_VERTICAL) == Gravity.CENTER_VERTICAL) { + top -= (height / 2); + } + mImage.setBounds(left, top, left + width, top + height); + } + + public void draw(Canvas canvas) { + int c = canvas.save(Canvas.MATRIX_SAVE_FLAG); + mImage.draw(canvas); + canvas.restoreToCount(c); + } +} + +/** + * This is a custom composite drawable that has a fixed virtual size and dynamically lays out its + * children images relatively within its bounds. This way, we can reduce the memory usage of a + * single, large sparsely populated image. + */ +public class AllAppsBackgroundDrawable extends Drawable { + + private final TransformedImageDrawable mHand; + private final TransformedImageDrawable[] mIcons; + private final int mWidth; + private final int mHeight; + + private ObjectAnimator mBackgroundAnim; + + public AllAppsBackgroundDrawable(Context context) { + Resources res = context.getResources(); + mHand = new TransformedImageDrawable(res, R.drawable.ic_all_apps_bg_hand, + 0.575f, 0.1f, Gravity.CENTER_HORIZONTAL); + mIcons = new TransformedImageDrawable[4]; + mIcons[0] = new TransformedImageDrawable(res, R.drawable.ic_all_apps_bg_icon_1, + 0.375f, 0, Gravity.CENTER_HORIZONTAL); + mIcons[1] = new TransformedImageDrawable(res, R.drawable.ic_all_apps_bg_icon_2, + 0.3125f, 0.25f, Gravity.CENTER_HORIZONTAL); + mIcons[2] = new TransformedImageDrawable(res, R.drawable.ic_all_apps_bg_icon_3, + 0.475f, 0.4f, Gravity.CENTER_HORIZONTAL); + mIcons[3] = new TransformedImageDrawable(res, R.drawable.ic_all_apps_bg_icon_4, + 0.7f, 0.125f, Gravity.CENTER_HORIZONTAL); + mWidth = res.getDimensionPixelSize(R.dimen.all_apps_background_canvas_width); + mHeight = res.getDimensionPixelSize(R.dimen.all_apps_background_canvas_height); + } + + /** + * Animates the background alpha. + */ + public void animateBgAlpha(float finalAlpha, int duration) { + int finalAlphaI = (int) (finalAlpha * 255f); + if (getAlpha() != finalAlphaI) { + mBackgroundAnim = cancelAnimator(mBackgroundAnim); + mBackgroundAnim = ObjectAnimator.ofInt(this, "alpha", finalAlphaI); + mBackgroundAnim.setDuration(duration); + mBackgroundAnim.start(); + } + } + + /** + * Sets the background alpha immediately. + */ + public void setBgAlpha(float finalAlpha) { + int finalAlphaI = (int) (finalAlpha * 255f); + if (getAlpha() != finalAlphaI) { + mBackgroundAnim = cancelAnimator(mBackgroundAnim); + setAlpha(finalAlphaI); + } + } + + @Override + public int getIntrinsicWidth() { + return mWidth; + } + + @Override + public int getIntrinsicHeight() { + return mHeight; + } + + @Override + public void draw(Canvas canvas) { + mHand.draw(canvas); + for (int i = 0; i < mIcons.length; i++) { + mIcons[i].draw(canvas); + } + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + mHand.updateBounds(bounds); + for (int i = 0; i < mIcons.length; i++) { + mIcons[i].updateBounds(bounds); + } + invalidateSelf(); + } + + @Override + public void setAlpha(int alpha) { + mHand.setAlpha(alpha); + for (int i = 0; i < mIcons.length; i++) { + mIcons[i].setAlpha(alpha); + } + invalidateSelf(); + } + + @Override + public int getAlpha() { + return mHand.getAlpha(); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + // Do nothing + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + private ObjectAnimator cancelAnimator(ObjectAnimator animator) { + if (animator != null) { + animator.removeAllListeners(); + animator.cancel(); + } + return null; + } +} diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java index 67d572819..88c6acada 100644 --- a/src/com/android/launcher3/allapps/AllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java @@ -16,34 +16,26 @@ package com.android.launcher3.allapps; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.content.Context; +import android.content.Intent; import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.InsetDrawable; -import android.os.Build; -import android.os.Bundle; import android.support.v7.widget.RecyclerView; import android.text.Selection; import android.text.SpannableStringBuilder; import android.text.method.TextKeyListener; import android.util.AttributeSet; import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.widget.FrameLayout; import android.widget.LinearLayout; - import com.android.launcher3.AppInfo; import com.android.launcher3.BaseContainerView; -import com.android.launcher3.BubbleTextView; import com.android.launcher3.CellLayout; -import com.android.launcher3.CheckLongPressHelper; import com.android.launcher3.DeleteDropTarget; import com.android.launcher3.DeviceProfile; import com.android.launcher3.DragSource; @@ -53,7 +45,6 @@ import com.android.launcher3.ItemInfo; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherTransitionable; import com.android.launcher3.R; -import com.android.launcher3.Stats; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; import com.android.launcher3.util.ComponentKey; @@ -155,6 +146,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc @Thunk AllAppsSearchBarController mSearchBarController; private ViewGroup mSearchBarContainerView; private View mSearchBarView; + private SpannableStringBuilder mSearchQueryBuilder = null; private int mSectionNamesMargin; private int mNumAppsPerRow; @@ -165,7 +157,13 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc // This coordinate is relative to its parent private final Point mIconLastTouchPos = new Point(); - private SpannableStringBuilder mSearchQueryBuilder = null; + private View.OnClickListener mSearchClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent searchIntent = (Intent) v.getTag(); + mLauncher.startActivitySafely(v, searchIntent, null); + } + }; public AllAppsContainerView(Context context) { this(context, null); @@ -182,8 +180,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc mLauncher = (Launcher) context; mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin); mApps = new AlphabeticalAppsList(context); - mAdapter = new AllAppsGridAdapter(context, mApps, this, mLauncher, this); - mAdapter.setEmptySearchText(res.getString(R.string.all_apps_loading_message)); + mAdapter = new AllAppsGridAdapter(mLauncher, mApps, this, mLauncher, this); mApps.setAdapter(mAdapter); mLayoutManager = mAdapter.getLayoutManager(); mItemDecoration = mAdapter.getItemDecoration(); @@ -528,7 +525,6 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen); ItemInfo itemInfo = (ItemInfo) d.dragInfo; if (layout != null) { - layout.calculateSpans(itemInfo); showOutOfSpaceMessage = !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY); } @@ -559,8 +555,9 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc @Override public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { if (toWorkspace) { - // Reset the search bar after transitioning home + // Reset the search bar and base recycler view after transitioning home mSearchBarController.reset(); + mAppsRecyclerView.reset(); } } @@ -616,19 +613,16 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc @Override public void onSearchResult(String query, ArrayList<ComponentKey> apps) { if (apps != null) { - if (apps.isEmpty()) { - String formatStr = getResources().getString(R.string.all_apps_no_search_results); - mAdapter.setEmptySearchText(String.format(formatStr, query)); - } else { - mAppsRecyclerView.scrollToTop(); - } mApps.setOrderedFilter(apps); + mAdapter.setLastSearchQuery(query); + mAppsRecyclerView.onSearchResultsChanged(); } } @Override public void clearSearchResult() { mApps.setOrderedFilter(null); + mAppsRecyclerView.onSearchResultsChanged(); // Clear the search query mSearchQueryBuilder.clear(); diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java index 057883cab..1f95133d4 100644 --- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java +++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java @@ -16,21 +16,30 @@ package com.android.launcher3.allapps; import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PointF; import android.graphics.Rect; -import android.os.Handler; +import android.graphics.drawable.Drawable; +import android.support.v4.view.accessibility.AccessibilityRecordCompat; +import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.net.Uri; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; +import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; import android.widget.TextView; import com.android.launcher3.AppInfo; import com.android.launcher3.BubbleTextView; +import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.util.Thunk; @@ -42,7 +51,7 @@ import java.util.List; /** * The grid view adapter of all the apps. */ -class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> { +public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> { public static final String TAG = "AppsGridAdapter"; private static final boolean DEBUG = false; @@ -55,6 +64,10 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol public static final int PREDICTION_ICON_VIEW_TYPE = 2; // The message shown when there are no filtered results public static final int EMPTY_SEARCH_VIEW_TYPE = 3; + // A divider that separates the apps list and the search market button + public static final int SEARCH_MARKET_DIVIDER_VIEW_TYPE = 4; + // The message to continue to a market search when there are no filtered results + public static final int SEARCH_MARKET_VIEW_TYPE = 5; /** * ViewHolder for each icon. @@ -69,6 +82,38 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol } /** + * A subclass of GridLayoutManager that overrides accessibility values during app search. + */ + public class AppsGridLayoutManager extends GridLayoutManager { + + public AppsGridLayoutManager(Context context) { + super(context, 1, GridLayoutManager.VERTICAL, false); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + + // Ensure that we only report the number apps for accessibility not including other + // adapter views + final AccessibilityRecordCompat record = AccessibilityEventCompat + .asRecord(event); + record.setItemCount(mApps.getNumFilteredApps()); + } + + @Override + public int getRowCountForAccessibility(RecyclerView.Recycler recycler, + RecyclerView.State state) { + if (mApps.hasNoFilteredResults()) { + // Disregard the no-search-results text as a list item for accessibility + return 0; + } else { + return super.getRowCountForAccessibility(recycler, state); + } + } + } + + /** * Helper class to size the grid items. */ public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup { @@ -80,11 +125,6 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol @Override public int getSpanSize(int position) { - if (mApps.hasNoFilteredResults()) { - // Empty view spans full width - return mAppsPerRow; - } - switch (mApps.getAdapterItems().get(position).viewType) { case AllAppsGridAdapter.ICON_VIEW_TYPE: case AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE: @@ -279,6 +319,7 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol } } + private Launcher mLauncher; private LayoutInflater mLayoutInflater; @Thunk AlphabeticalAppsList mApps; private GridLayoutManager mGridLayoutMgr; @@ -291,7 +332,19 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol @Thunk int mPredictionBarDividerOffset; @Thunk int mAppsPerRow; @Thunk boolean mIsRtl; - private String mEmptySearchText; + + // The text to show when there are no search results and no market search handler. + private String mEmptySearchMessage; + // The name of the market app which handles searches, to be used in the format str + // below when updating the search-market view. Only needs to be loaded once. + private String mMarketAppName; + // The text to show when there is a market app which can handle a specific query, updated + // each time the search query changes. + private String mMarketSearchMessage; + // The intent to send off to the market app, updated each time the search query changes. + private Intent mMarketSearchIntent; + // The last query that the user entered into the search field + private String mLastSearchQuery; // Section drawing @Thunk int mSectionNamesMargin; @@ -299,16 +352,18 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol @Thunk Paint mSectionTextPaint; @Thunk Paint mPredictedAppsDividerPaint; - public AllAppsGridAdapter(Context context, AlphabeticalAppsList apps, + public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnTouchListener touchListener, View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener) { - Resources res = context.getResources(); + Resources res = launcher.getResources(); + mLauncher = launcher; mApps = apps; + mEmptySearchMessage = res.getString(R.string.all_apps_loading_message); mGridSizer = new GridSpanSizer(); - mGridLayoutMgr = new GridLayoutManager(context, 1, GridLayoutManager.VERTICAL, false); + mGridLayoutMgr = new AppsGridLayoutManager(launcher); mGridLayoutMgr.setSpanSizeLookup(mGridSizer); mItemDecoration = new GridItemDecoration(); - mLayoutInflater = LayoutInflater.from(context); + mLayoutInflater = LayoutInflater.from(launcher); mTouchListener = touchListener; mIconClickListener = iconClickListener; mIconLongClickListener = iconLongClickListener; @@ -328,6 +383,14 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol mPredictionBarDividerOffset = (-res.getDimensionPixelSize(R.dimen.all_apps_prediction_icon_bottom_padding) + res.getDimensionPixelSize(R.dimen.all_apps_icon_top_bottom_padding)) / 2; + + // Resolve the market app handling additional searches + PackageManager pm = launcher.getPackageManager(); + ResolveInfo marketInfo = pm.resolveActivity(createMarketSearchIntent(""), + PackageManager.MATCH_DEFAULT_ONLY); + if (marketInfo != null) { + mMarketAppName = marketInfo.loadLabel(pm).toString(); + } } /** @@ -346,10 +409,19 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol } /** - * Sets the text to show when there are no apps. + * Sets the last search query that was made, used to show when there are no results and to also + * seed the intent for searching the market. */ - public void setEmptySearchText(String query) { - mEmptySearchText = query; + public void setLastSearchQuery(String query) { + Resources res = mLauncher.getResources(); + String formatStr = res.getString(R.string.all_apps_no_search_results); + mLastSearchQuery = query; + mEmptySearchMessage = String.format(formatStr, query); + if (mMarketAppName != null) { + mMarketSearchMessage = String.format(res.getString(R.string.all_apps_search_market_message), + mMarketAppName); + mMarketSearchIntent = createMarketSearchIntent(query); + } } /** @@ -378,9 +450,6 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { switch (viewType) { - case EMPTY_SEARCH_VIEW_TYPE: - return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, parent, - false)); case SECTION_BREAK_VIEW_TYPE: return new ViewHolder(new View(parent.getContext())); case ICON_VIEW_TYPE: { @@ -405,6 +474,22 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol icon.setFocusable(true); return new ViewHolder(icon); } + case EMPTY_SEARCH_VIEW_TYPE: + return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, + parent, false)); + case SEARCH_MARKET_DIVIDER_VIEW_TYPE: + return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_search_market_divider, + parent, false)); + case SEARCH_MARKET_VIEW_TYPE: + View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market, + parent, false); + searchMarketView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mLauncher.startSearchFromAllApps(v, mMarketSearchIntent, mLastSearchQuery); + } + }); + return new ViewHolder(searchMarketView); default: throw new RuntimeException("Unexpected view type"); } @@ -426,28 +511,47 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol break; } case EMPTY_SEARCH_VIEW_TYPE: - TextView emptyViewText = (TextView) holder.mContent.findViewById(R.id.empty_text); - emptyViewText.setText(mEmptySearchText); + TextView emptyViewText = (TextView) holder.mContent; + emptyViewText.setText(mEmptySearchMessage); + emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER : + Gravity.START | Gravity.CENTER_VERTICAL); + break; + case SEARCH_MARKET_VIEW_TYPE: + TextView searchView = (TextView) holder.mContent; + if (mMarketSearchIntent != null) { + searchView.setVisibility(View.VISIBLE); + searchView.setContentDescription(mMarketSearchMessage); + searchView.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER : + Gravity.START | Gravity.CENTER_VERTICAL); + searchView.setText(mMarketSearchMessage); + } else { + searchView.setVisibility(View.GONE); + } break; } } @Override public int getItemCount() { - if (mApps.hasNoFilteredResults()) { - // For the empty view - return 1; - } return mApps.getAdapterItems().size(); } @Override public int getItemViewType(int position) { - if (mApps.hasNoFilteredResults()) { - return EMPTY_SEARCH_VIEW_TYPE; - } - AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); return item.viewType; } + + /** + * Creates a new market search intent. + */ + private Intent createMarketSearchIntent(String query) { + Uri marketSearchUri = Uri.parse("market://search") + .buildUpon() + .appendQueryParameter("q", query) + .build(); + Intent marketSearchIntent = new Intent(Intent.ACTION_VIEW); + marketSearchIntent.setData(marketSearchUri); + return marketSearchIntent; + } } diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java index 730c8d15a..2f66e2cad 100644 --- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java @@ -15,8 +15,11 @@ */ package com.android.launcher3.allapps; +import android.animation.ObjectAnimator; import android.content.Context; +import android.content.res.Resources; import android.graphics.Canvas; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -26,6 +29,7 @@ import android.view.View; import com.android.launcher3.BaseRecyclerView; import com.android.launcher3.BaseRecyclerViewFastScrollBar; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; import com.android.launcher3.Stats; import com.android.launcher3.Utilities; import com.android.launcher3.util.Thunk; @@ -57,6 +61,9 @@ public class AllAppsRecyclerView extends BaseRecyclerView private ScrollPositionState mScrollPosState = new ScrollPositionState(); + private AllAppsBackgroundDrawable mEmptySearchBackground; + private int mEmptySearchBackgroundTopOffset; + public AllAppsRecyclerView(Context context) { this(context, null); } @@ -72,6 +79,11 @@ public class AllAppsRecyclerView extends BaseRecyclerView public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr); + + Resources res = getResources(); + mScrollbar.setDetachThumbOnFastScroll(); + mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize( + R.dimen.all_apps_empty_search_bg_top_offset); } /** @@ -90,6 +102,8 @@ public class AllAppsRecyclerView extends BaseRecyclerView RecyclerView.RecycledViewPool pool = getRecycledViewPool(); int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx); pool.setMaxRecycledViews(AllAppsGridAdapter.EMPTY_SEARCH_VIEW_TYPE, 1); + pool.setMaxRecycledViews(AllAppsGridAdapter.SEARCH_MARKET_DIVIDER_VIEW_TYPE, 1); + pool.setMaxRecycledViews(AllAppsGridAdapter.SEARCH_MARKET_VIEW_TYPE, 1); pool.setMaxRecycledViews(AllAppsGridAdapter.ICON_VIEW_TYPE, approxRows * mNumAppsPerRow); pool.setMaxRecycledViews(AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE, mNumAppsPerRow); pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows); @@ -99,6 +113,10 @@ public class AllAppsRecyclerView extends BaseRecyclerView * Scrolls this recycler view to the top. */ public void scrollToTop() { + // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling + if (mScrollbar.isThumbDetached()) { + mScrollbar.reattachThumbToScroll(); + } scrollToPosition(0); } @@ -115,6 +133,30 @@ public class AllAppsRecyclerView extends BaseRecyclerView } @Override + public void onDraw(Canvas c) { + // Draw the background + if (mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) { + c.clipRect(mBackgroundPadding.left, mBackgroundPadding.top, + getWidth() - mBackgroundPadding.right, + getHeight() - mBackgroundPadding.bottom); + + mEmptySearchBackground.draw(c); + } + + super.onDraw(c); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return who == mEmptySearchBackground || super.verifyDrawable(who); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + updateEmptySearchBackgroundBounds(); + } + + @Override protected void onFinishInflate() { super.onFinishInflate(); @@ -134,6 +176,25 @@ public class AllAppsRecyclerView extends BaseRecyclerView } } + public void onSearchResultsChanged() { + // Always scroll the view to the top so the user can see the changed results + scrollToTop(); + + if (mApps.hasNoFilteredResults()) { + if (mEmptySearchBackground == null) { + mEmptySearchBackground = new AllAppsBackgroundDrawable(getContext()); + mEmptySearchBackground.setAlpha(0); + mEmptySearchBackground.setCallback(this); + updateEmptySearchBackgroundBounds(); + } + mEmptySearchBackground.animateBgAlpha(1f, 150); + } else if (mEmptySearchBackground != null) { + // For the time being, we just immediately hide the background to ensure that it does + // not overlap with the results + mEmptySearchBackground.setBgAlpha(0f); + } + } + /** * Maps the touch (from 0..1) to the adapter position that should be visible. */ @@ -166,8 +227,8 @@ public class AllAppsRecyclerView extends BaseRecyclerView } // Map the touch position back to the scroll of the recycler view - getCurScrollState(mScrollPosState, mApps.getAdapterItems()); - int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight, 0); + getCurScrollState(mScrollPosState); + int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight); LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager(); if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) { layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); @@ -214,24 +275,83 @@ public class AllAppsRecyclerView extends BaseRecyclerView * Updates the bounds for the scrollbar. */ @Override - public void onUpdateScrollbar() { + public void onUpdateScrollbar(int dy) { List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); // Skip early if there are no items or we haven't been measured if (items.isEmpty() || mNumAppsPerRow == 0) { - mScrollbar.setScrollbarThumbOffset(-1, -1); + mScrollbar.setThumbOffset(-1, -1); return; } // Find the index and height of the first visible row (all rows have the same height) int rowCount = mApps.getNumAppRows(); - getCurScrollState(mScrollPosState, items); + getCurScrollState(mScrollPosState); if (mScrollPosState.rowIndex < 0) { - mScrollbar.setScrollbarThumbOffset(-1, -1); + mScrollbar.setThumbOffset(-1, -1); return; } - synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, 0); + // Only show the scrollbar if there is height to be scrolled + int availableScrollBarHeight = getAvailableScrollBarHeight(); + int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows(), mScrollPosState.rowHeight); + if (availableScrollHeight <= 0) { + mScrollbar.setThumbOffset(-1, -1); + return; + } + + // Calculate the current scroll position, the scrollY of the recycler view accounts for the + // view padding, while the scrollBarY is drawn right up to the background padding (ignoring + // padding) + int scrollY = getPaddingTop() + + (mScrollPosState.rowIndex * mScrollPosState.rowHeight) - mScrollPosState.rowTopOffset; + int scrollBarY = mBackgroundPadding.top + + (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); + + if (mScrollbar.isThumbDetached()) { + int scrollBarX; + if (Utilities.isRtl(getResources())) { + scrollBarX = mBackgroundPadding.left; + } else { + scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth(); + } + + if (mScrollbar.isDraggingThumb()) { + // If the thumb is detached, then just update the thumb to the current + // touch position + mScrollbar.setThumbOffset(scrollBarX, (int) mScrollbar.getLastTouchY()); + } else { + int thumbScrollY = mScrollbar.getThumbOffset().y; + int diffScrollY = scrollBarY - thumbScrollY; + if (diffScrollY * dy > 0f) { + // User is scrolling in the same direction the thumb needs to catch up to the + // current scroll position. We do this by mapping the difference in movement + // from the original scroll bar position to the difference in movement necessary + // in the detached thumb position to ensure that both speed towards the same + // position at either end of the list. + if (dy < 0) { + int offset = (int) ((dy * thumbScrollY) / (float) scrollBarY); + thumbScrollY += Math.max(offset, diffScrollY); + } else { + int offset = (int) ((dy * (availableScrollBarHeight - thumbScrollY)) / + (float) (availableScrollBarHeight - scrollBarY)); + thumbScrollY += Math.min(offset, diffScrollY); + } + thumbScrollY = Math.max(0, Math.min(availableScrollBarHeight, thumbScrollY)); + mScrollbar.setThumbOffset(scrollBarX, thumbScrollY); + if (scrollBarY == thumbScrollY) { + mScrollbar.reattachThumbToScroll(); + } + } else { + // User is scrolling in an opposite direction to the direction that the thumb + // needs to catch up to the scroll position. Do nothing except for updating + // the scroll bar x to match the thumb width. + mScrollbar.setThumbOffset(scrollBarX, thumbScrollY); + } + } + } else { + synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount); + } } /** @@ -283,13 +403,13 @@ public class AllAppsRecyclerView extends BaseRecyclerView /** * Returns the current scroll state of the apps rows. */ - private void getCurScrollState(ScrollPositionState stateOut, - List<AlphabeticalAppsList.AdapterItem> items) { + protected void getCurScrollState(ScrollPositionState stateOut) { stateOut.rowIndex = -1; stateOut.rowTopOffset = -1; stateOut.rowHeight = -1; // Return early if there are no items or we haven't been measured + List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); if (items.isEmpty() || mNumAppsPerRow == 0) { return; } @@ -324,4 +444,20 @@ public class AllAppsRecyclerView extends BaseRecyclerView return 0; } } + + /** + * Updates the bounds of the empty search background. + */ + private void updateEmptySearchBackgroundBounds() { + if (mEmptySearchBackground == null) { + return; + } + + // Center the empty search background on this new view bounds + int x = (getMeasuredWidth() - mEmptySearchBackground.getIntrinsicWidth()) / 2; + int y = mEmptySearchBackgroundTopOffset; + mEmptySearchBackground.setBounds(x, y, + x + mEmptySearchBackground.getIntrinsicWidth(), + y + mEmptySearchBackground.getIntrinsicHeight()); + } } diff --git a/src/com/android/launcher3/allapps/AllAppsSearchEditView.java b/src/com/android/launcher3/allapps/AllAppsSearchEditView.java deleted file mode 100644 index b7dcd66ed..000000000 --- a/src/com/android/launcher3/allapps/AllAppsSearchEditView.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.allapps; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.widget.EditText; - - -/** - * The edit text for the search container - */ -public class AllAppsSearchEditView extends EditText { - - /** - * Implemented by listeners of the back key. - */ - public interface OnBackKeyListener { - public void onBackKey(); - } - - private OnBackKeyListener mBackKeyListener; - - public AllAppsSearchEditView(Context context) { - this(context, null); - } - - public AllAppsSearchEditView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public AllAppsSearchEditView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public void setOnBackKeyListener(OnBackKeyListener listener) { - mBackKeyListener = listener; - } - - @Override - public boolean onKeyPreIme(int keyCode, KeyEvent event) { - // If this is a back key, propagate the key back to the listener - if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { - if (mBackKeyListener != null) { - mBackKeyListener.onBackKey(); - } - return false; - } - return super.onKeyPreIme(keyCode, event); - } -} diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java index 47241ce5d..dac0df12a 100644 --- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java +++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java @@ -43,6 +43,11 @@ public class AlphabeticalAppsList { private static final boolean DEBUG = false; private static final boolean DEBUG_PREDICTIONS = false; + private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION = 0; + private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS = 1; + + private final int mFastScrollDistributionMode = FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS; + /** * Info about a section in the alphabetic list */ @@ -81,8 +86,6 @@ public class AlphabeticalAppsList { public int position; // The type of this item public int viewType; - // The row that this item shows up on - public int rowIndex; /** Section & App properties */ // The section for this item @@ -94,6 +97,8 @@ public class AlphabeticalAppsList { public String sectionName = null; // The index of this app in the section public int sectionAppIndex = -1; + // The row that this item shows up on + public int rowIndex; // The index of this app in the row public int rowAppIndex; // The associated AppInfo for the app @@ -111,14 +116,14 @@ public class AlphabeticalAppsList { } public static AdapterItem asPredictedApp(int pos, SectionInfo section, String sectionName, - int sectionAppIndex, AppInfo appInfo, int appIndex) { + int sectionAppIndex, AppInfo appInfo, int appIndex) { AdapterItem item = asApp(pos, section, sectionName, sectionAppIndex, appInfo, appIndex); item.viewType = AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE; return item; } public static AdapterItem asApp(int pos, SectionInfo section, String sectionName, - int sectionAppIndex, AppInfo appInfo, int appIndex) { + int sectionAppIndex, AppInfo appInfo, int appIndex) { AdapterItem item = new AdapterItem(); item.viewType = AllAppsGridAdapter.ICON_VIEW_TYPE; item.position = pos; @@ -129,6 +134,27 @@ public class AlphabeticalAppsList { item.appIndex = appIndex; return item; } + + public static AdapterItem asEmptySearch(int pos) { + AdapterItem item = new AdapterItem(); + item.viewType = AllAppsGridAdapter.EMPTY_SEARCH_VIEW_TYPE; + item.position = pos; + return item; + } + + public static AdapterItem asDivider(int pos) { + AdapterItem item = new AdapterItem(); + item.viewType = AllAppsGridAdapter.SEARCH_MARKET_DIVIDER_VIEW_TYPE; + item.position = pos; + return item; + } + + public static AdapterItem asMarketSearch(int pos) { + AdapterItem item = new AdapterItem(); + item.viewType = AllAppsGridAdapter.SEARCH_MARKET_VIEW_TYPE; + item.position = pos; + return item; + } } /** @@ -222,17 +248,17 @@ public class AlphabeticalAppsList { } /** - * Returns the number of applications in this list. + * Returns the number of rows of applications (not including predictions) */ - public int getSize() { - return mFilteredApps.size(); + public int getNumAppRows() { + return mNumAppRowsInAdapter; } /** - * Returns the number of rows of applications (not including predictions) + * Returns the number of applications in this list. */ - public int getNumAppRows() { - return mNumAppRowsInAdapter; + public int getNumFilteredApps() { + return mFilteredApps.size(); } /** @@ -457,6 +483,16 @@ public class AlphabeticalAppsList { mFilteredApps.add(info); } + // Append the search market item if we are currently searching + if (hasFilter()) { + if (hasNoFilteredResults()) { + mAdapterItems.add(AdapterItem.asEmptySearch(position++)); + } else { + mAdapterItems.add(AdapterItem.asDivider(position++)); + } + mAdapterItems.add(AdapterItem.asMarketSearch(position++)); + } + // Merge multiple sections together as requested by the merge strategy for this device mergeSections(); @@ -484,18 +520,36 @@ public class AlphabeticalAppsList { } mNumAppRowsInAdapter = rowIndex + 1; - // Pre-calculate all the fast scroller fractions based on the number of rows - float rowFraction = 1f / mNumAppRowsInAdapter; - for (FastScrollSectionInfo info : mFastScrollerSections) { - AdapterItem item = info.fastScrollToItem; - if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE && - item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { - info.touchFraction = 0f; - continue; - } - - float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow); - info.touchFraction = item.rowIndex * rowFraction + subRowFraction; + // Pre-calculate all the fast scroller fractions + switch (mFastScrollDistributionMode) { + case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION: + float rowFraction = 1f / mNumAppRowsInAdapter; + for (FastScrollSectionInfo info : mFastScrollerSections) { + AdapterItem item = info.fastScrollToItem; + if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE && + item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { + info.touchFraction = 0f; + continue; + } + + float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow); + info.touchFraction = item.rowIndex * rowFraction + subRowFraction; + } + break; + case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS: + float perSectionTouchFraction = 1f / mFastScrollerSections.size(); + float cumulativeTouchFraction = 0f; + for (FastScrollSectionInfo info : mFastScrollerSections) { + AdapterItem item = info.fastScrollToItem; + if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE && + item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { + info.touchFraction = 0f; + continue; + } + info.touchFraction = cumulativeTouchFraction; + cumulativeTouchFraction += perSectionTouchFraction; + } + break; } } diff --git a/src/com/android/launcher3/allapps/DefaultAppSearchController.java b/src/com/android/launcher3/allapps/DefaultAppSearchController.java index 83b920589..3169f842a 100644 --- a/src/com/android/launcher3/allapps/DefaultAppSearchController.java +++ b/src/com/android/launcher3/allapps/DefaultAppSearchController.java @@ -25,6 +25,7 @@ import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.TextView; +import com.android.launcher3.ExtendedEditText; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.util.Thunk; @@ -54,7 +55,8 @@ final class DefaultAppSearchController extends AllAppsSearchBarController @Thunk View mSearchBarContainerView; private View mSearchButtonView; private View mDismissSearchButtonView; - @Thunk AllAppsSearchEditView mSearchBarEditView; + @Thunk + ExtendedEditText mSearchBarEditView; @Thunk AllAppsRecyclerView mAppsRecyclerView; @Thunk Runnable mFocusRecyclerViewRunnable = new Runnable() { @Override @@ -82,21 +84,23 @@ final class DefaultAppSearchController extends AllAppsSearchBarController mSearchBarContainerView = mSearchView.findViewById(R.id.search_container); mDismissSearchButtonView = mSearchBarContainerView.findViewById(R.id.dismiss_search_button); mDismissSearchButtonView.setOnClickListener(this); - mSearchBarEditView = (AllAppsSearchEditView) + mSearchBarEditView = (ExtendedEditText) mSearchBarContainerView.findViewById(R.id.search_box_input); mSearchBarEditView.addTextChangedListener(this); mSearchBarEditView.setOnEditorActionListener(this); mSearchBarEditView.setOnBackKeyListener( - new AllAppsSearchEditView.OnBackKeyListener() { + new ExtendedEditText.OnBackKeyListener() { @Override - public void onBackKey() { + public boolean onBackKey() { // Only hide the search field if there is no query, or if there // are no filtered results String query = Utilities.trim( mSearchBarEditView.getEditableText().toString()); if (query.isEmpty() || mApps.hasNoFilteredResults()) { hideSearchField(true, mFocusRecyclerViewRunnable); + return true; } + return false; } }); return mSearchView; @@ -166,22 +170,24 @@ final class DefaultAppSearchController extends AllAppsSearchBarController return false; } // Skip if it's not the right action - if (actionId != EditorInfo.IME_ACTION_DONE) { + if (actionId != EditorInfo.IME_ACTION_SEARCH) { return false; } - // Skip if there isn't exactly one item - if (mApps.getSize() != 1) { + // Skip if there are more than one icon + if (mApps.getNumFilteredApps() > 1) { return false; } - // If there is exactly one icon, then quick-launch it + // Otherwise, find the first icon, or fallback to the search-market-view and launch it List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); for (int i = 0; i < items.size(); i++) { AlphabeticalAppsList.AdapterItem item = items.get(i); - if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE) { - mAppsRecyclerView.getChildAt(i).performClick(); - mInputMethodManager.hideSoftInputFromWindow( - mContainerView.getWindowToken(), 0); - return true; + switch (item.viewType) { + case AllAppsGridAdapter.ICON_VIEW_TYPE: + case AllAppsGridAdapter.SEARCH_MARKET_VIEW_TYPE: + mAppsRecyclerView.getChildAt(i).performClick(); + mInputMethodManager.hideSoftInputFromWindow( + mContainerView.getWindowToken(), 0); + return true; } } return false; |