• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.allapps;
17 
18 import android.content.Context;
19 import android.content.Intent;
20 import android.content.res.Resources;
21 import android.support.animation.DynamicAnimation;
22 import android.support.animation.SpringAnimation;
23 import android.support.v4.view.accessibility.AccessibilityEventCompat;
24 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
25 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
26 import android.support.v7.widget.GridLayoutManager;
27 import android.support.v7.widget.RecyclerView;
28 import android.view.Gravity;
29 import android.view.LayoutInflater;
30 import android.view.View;
31 import android.view.View.OnFocusChangeListener;
32 import android.view.ViewConfiguration;
33 import android.view.ViewGroup;
34 import android.view.accessibility.AccessibilityEvent;
35 import android.widget.TextView;
36 
37 import com.android.launcher3.AppInfo;
38 import com.android.launcher3.BubbleTextView;
39 import com.android.launcher3.Launcher;
40 import com.android.launcher3.R;
41 import com.android.launcher3.Utilities;
42 import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem;
43 import com.android.launcher3.anim.SpringAnimationHandler;
44 import com.android.launcher3.config.FeatureFlags;
45 import com.android.launcher3.discovery.AppDiscoveryAppInfo;
46 import com.android.launcher3.discovery.AppDiscoveryItemView;
47 import com.android.launcher3.util.PackageManagerHelper;
48 
49 import java.util.List;
50 
51 /**
52  * The grid view adapter of all the apps.
53  */
54 public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> {
55 
56     public static final String TAG = "AppsGridAdapter";
57 
58     // A normal icon
59     public static final int VIEW_TYPE_ICON = 1 << 1;
60     // A prediction icon
61     public static final int VIEW_TYPE_PREDICTION_ICON = 1 << 2;
62     // The message shown when there are no filtered results
63     public static final int VIEW_TYPE_EMPTY_SEARCH = 1 << 3;
64     // The message to continue to a market search when there are no filtered results
65     public static final int VIEW_TYPE_SEARCH_MARKET = 1 << 4;
66 
67     // We use various dividers for various purposes.  They share enough attributes to reuse layouts,
68     // but differ in enough attributes to require different view types
69 
70     // A divider that separates the apps list and the search market button
71     public static final int VIEW_TYPE_SEARCH_MARKET_DIVIDER = 1 << 5;
72     // The divider that separates prediction icons from the app list
73     public static final int VIEW_TYPE_PREDICTION_DIVIDER = 1 << 6;
74     public static final int VIEW_TYPE_APPS_LOADING_DIVIDER = 1 << 7;
75     public static final int VIEW_TYPE_DISCOVERY_ITEM = 1 << 8;
76 
77     // Common view type masks
78     public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_SEARCH_MARKET_DIVIDER
79             | VIEW_TYPE_PREDICTION_DIVIDER;
80     public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON
81             | VIEW_TYPE_PREDICTION_ICON;
82     public static final int VIEW_TYPE_MASK_CONTENT = VIEW_TYPE_MASK_ICON
83             | VIEW_TYPE_DISCOVERY_ITEM;
84     public static final int VIEW_TYPE_MASK_HAS_SPRINGS = VIEW_TYPE_MASK_ICON
85             | VIEW_TYPE_PREDICTION_DIVIDER;
86 
87 
88     public interface BindViewCallback {
onBindView(ViewHolder holder)89         void onBindView(ViewHolder holder);
90     }
91 
92     /**
93      * ViewHolder for each icon.
94      */
95     public static class ViewHolder extends RecyclerView.ViewHolder {
96 
ViewHolder(View v)97         public ViewHolder(View v) {
98             super(v);
99         }
100     }
101 
102     /**
103      * A subclass of GridLayoutManager that overrides accessibility values during app search.
104      */
105     public class AppsGridLayoutManager extends GridLayoutManager {
106 
AppsGridLayoutManager(Context context)107         public AppsGridLayoutManager(Context context) {
108             super(context, 1, GridLayoutManager.VERTICAL, false);
109         }
110 
111         @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)112         public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
113             super.onInitializeAccessibilityEvent(event);
114 
115             // Ensure that we only report the number apps for accessibility not including other
116             // adapter views
117             final AccessibilityRecordCompat record = AccessibilityEventCompat
118                     .asRecord(event);
119             record.setItemCount(mApps.getNumFilteredApps());
120             record.setFromIndex(Math.max(0,
121                     record.getFromIndex() - getRowsNotForAccessibility(record.getFromIndex())));
122             record.setToIndex(Math.max(0,
123                     record.getToIndex() - getRowsNotForAccessibility(record.getToIndex())));
124         }
125 
126         @Override
getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)127         public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
128                 RecyclerView.State state) {
129             return super.getRowCountForAccessibility(recycler, state) -
130                     getRowsNotForAccessibility(mApps.getAdapterItems().size() - 1);
131         }
132 
133         @Override
onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, RecyclerView.State state, View host, AccessibilityNodeInfoCompat info)134         public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
135                 RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
136             super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info);
137 
138             ViewGroup.LayoutParams lp = host.getLayoutParams();
139             AccessibilityNodeInfoCompat.CollectionItemInfoCompat cic = info.getCollectionItemInfo();
140             if (!(lp instanceof LayoutParams) || (cic == null)) {
141                 return;
142             }
143             LayoutParams glp = (LayoutParams) lp;
144             info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
145                     cic.getRowIndex() - getRowsNotForAccessibility(glp.getViewAdapterPosition()),
146                     cic.getRowSpan(),
147                     cic.getColumnIndex(),
148                     cic.getColumnSpan(),
149                     cic.isHeading(),
150                     cic.isSelected()));
151         }
152 
153         /**
154          * Returns the number of rows before {@param adapterPosition}, including this position
155          * which should not be counted towards the collection info.
156          */
getRowsNotForAccessibility(int adapterPosition)157         private int getRowsNotForAccessibility(int adapterPosition) {
158             List<AdapterItem> items = mApps.getAdapterItems();
159             adapterPosition = Math.max(adapterPosition, mApps.getAdapterItems().size() - 1);
160             int extraRows = 0;
161             for (int i = 0; i <= adapterPosition; i++) {
162                 if (!isViewType(items.get(i).viewType, VIEW_TYPE_MASK_CONTENT)) {
163                     extraRows++;
164                 }
165             }
166             return extraRows;
167         }
168     }
169 
170     /**
171      * Helper class to size the grid items.
172      */
173     public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup {
174 
GridSpanSizer()175         public GridSpanSizer() {
176             super();
177             setSpanIndexCacheEnabled(true);
178         }
179 
180         @Override
getSpanSize(int position)181         public int getSpanSize(int position) {
182             if (isIconViewType(mApps.getAdapterItems().get(position).viewType)) {
183                 return 1;
184             } else {
185                     // Section breaks span the full width
186                     return mAppsPerRow;
187             }
188         }
189     }
190 
191     private final Launcher mLauncher;
192     private final LayoutInflater mLayoutInflater;
193     private final AlphabeticalAppsList mApps;
194     private final GridLayoutManager mGridLayoutMgr;
195     private final GridSpanSizer mGridSizer;
196     private final View.OnClickListener mIconClickListener;
197     private final View.OnLongClickListener mIconLongClickListener;
198 
199     private int mAppsPerRow;
200 
201     private BindViewCallback mBindViewCallback;
202     private OnFocusChangeListener mIconFocusListener;
203 
204     // The text to show when there are no search results and no market search handler.
205     private String mEmptySearchMessage;
206     // The intent to send off to the market app, updated each time the search query changes.
207     private Intent mMarketSearchIntent;
208 
209     private SpringAnimationHandler<ViewHolder> mSpringAnimationHandler;
210 
AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener)211     public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener
212             iconClickListener, View.OnLongClickListener iconLongClickListener) {
213         Resources res = launcher.getResources();
214         mLauncher = launcher;
215         mApps = apps;
216         mEmptySearchMessage = res.getString(R.string.all_apps_loading_message);
217         mGridSizer = new GridSpanSizer();
218         mGridLayoutMgr = new AppsGridLayoutManager(launcher);
219         mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
220         mLayoutInflater = LayoutInflater.from(launcher);
221         mIconClickListener = iconClickListener;
222         mIconLongClickListener = iconLongClickListener;
223         if (FeatureFlags.LAUNCHER3_PHYSICS) {
224             mSpringAnimationHandler = new SpringAnimationHandler<>(
225                     SpringAnimationHandler.Y_DIRECTION, new AllAppsSpringAnimationFactory());
226         }
227     }
228 
getSpringAnimationHandler()229     public SpringAnimationHandler getSpringAnimationHandler() {
230         return mSpringAnimationHandler;
231     }
232 
isDividerViewType(int viewType)233     public static boolean isDividerViewType(int viewType) {
234         return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER);
235     }
236 
isIconViewType(int viewType)237     public static boolean isIconViewType(int viewType) {
238         return isViewType(viewType, VIEW_TYPE_MASK_ICON);
239     }
240 
isViewType(int viewType, int viewTypeMask)241     public static boolean isViewType(int viewType, int viewTypeMask) {
242         return (viewType & viewTypeMask) != 0;
243     }
244 
245     /**
246      * Sets the number of apps per row.
247      */
setNumAppsPerRow(int appsPerRow)248     public void setNumAppsPerRow(int appsPerRow) {
249         mAppsPerRow = appsPerRow;
250         mGridLayoutMgr.setSpanCount(appsPerRow);
251     }
252 
getNumAppsPerRow()253     public int getNumAppsPerRow() {
254         return mAppsPerRow;
255     }
256 
setIconFocusListener(OnFocusChangeListener focusListener)257     public void setIconFocusListener(OnFocusChangeListener focusListener) {
258         mIconFocusListener = focusListener;
259     }
260 
261     /**
262      * Sets the last search query that was made, used to show when there are no results and to also
263      * seed the intent for searching the market.
264      */
setLastSearchQuery(String query)265     public void setLastSearchQuery(String query) {
266         Resources res = mLauncher.getResources();
267         mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query);
268         mMarketSearchIntent = PackageManagerHelper.getMarketSearchIntent(mLauncher, query);
269     }
270 
271     /**
272      * Sets the callback for when views are bound.
273      */
setBindViewCallback(BindViewCallback cb)274     public void setBindViewCallback(BindViewCallback cb) {
275         mBindViewCallback = cb;
276     }
277 
278     /**
279      * Returns the grid layout manager.
280      */
getLayoutManager()281     public GridLayoutManager getLayoutManager() {
282         return mGridLayoutMgr;
283     }
284 
285     @Override
onCreateViewHolder(ViewGroup parent, int viewType)286     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
287         switch (viewType) {
288             case VIEW_TYPE_ICON:
289             case VIEW_TYPE_PREDICTION_ICON:
290                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
291                         R.layout.all_apps_icon, parent, false);
292                 icon.setOnClickListener(mIconClickListener);
293                 icon.setOnLongClickListener(mIconLongClickListener);
294                 icon.setLongPressTimeout(ViewConfiguration.getLongPressTimeout());
295                 icon.setOnFocusChangeListener(mIconFocusListener);
296 
297                 // Ensure the all apps icon height matches the workspace icons in portrait mode.
298                 icon.getLayoutParams().height = mLauncher.getDeviceProfile().allAppsCellHeightPx;
299                 return new ViewHolder(icon);
300             case VIEW_TYPE_DISCOVERY_ITEM:
301                 AppDiscoveryItemView appDiscoveryItemView = (AppDiscoveryItemView) mLayoutInflater
302                         .inflate(R.layout.all_apps_discovery_item, parent, false);
303                 appDiscoveryItemView.init(mIconClickListener, mLauncher.getAccessibilityDelegate(),
304                         mIconLongClickListener);
305                 return new ViewHolder(appDiscoveryItemView);
306             case VIEW_TYPE_EMPTY_SEARCH:
307                 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search,
308                         parent, false));
309             case VIEW_TYPE_SEARCH_MARKET:
310                 View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market,
311                         parent, false);
312                 searchMarketView.setOnClickListener(new View.OnClickListener() {
313                     @Override
314                     public void onClick(View v) {
315                         mLauncher.startActivitySafely(v, mMarketSearchIntent, null);
316                     }
317                 });
318                 return new ViewHolder(searchMarketView);
319             case VIEW_TYPE_APPS_LOADING_DIVIDER:
320                 View loadingDividerView = mLayoutInflater.inflate(
321                         R.layout.all_apps_discovery_loading_divider, parent, false);
322                 return new ViewHolder(loadingDividerView);
323             case VIEW_TYPE_PREDICTION_DIVIDER:
324             case VIEW_TYPE_SEARCH_MARKET_DIVIDER:
325                 return new ViewHolder(mLayoutInflater.inflate(
326                         R.layout.all_apps_divider, parent, false));
327             default:
328                 throw new RuntimeException("Unexpected view type");
329         }
330     }
331 
332     @Override
onBindViewHolder(ViewHolder holder, int position)333     public void onBindViewHolder(ViewHolder holder, int position) {
334         switch (holder.getItemViewType()) {
335             case VIEW_TYPE_ICON:
336             case VIEW_TYPE_PREDICTION_ICON:
337                 AppInfo info = mApps.getAdapterItems().get(position).appInfo;
338                 BubbleTextView icon = (BubbleTextView) holder.itemView;
339                 icon.applyFromApplicationInfo(info);
340                 break;
341             case VIEW_TYPE_DISCOVERY_ITEM:
342                 AppDiscoveryAppInfo appDiscoveryAppInfo = (AppDiscoveryAppInfo)
343                         mApps.getAdapterItems().get(position).appInfo;
344                 AppDiscoveryItemView view = (AppDiscoveryItemView) holder.itemView;
345                 view.apply(appDiscoveryAppInfo);
346                 break;
347             case VIEW_TYPE_EMPTY_SEARCH:
348                 TextView emptyViewText = (TextView) holder.itemView;
349                 emptyViewText.setText(mEmptySearchMessage);
350                 emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER :
351                         Gravity.START | Gravity.CENTER_VERTICAL);
352                 break;
353             case VIEW_TYPE_SEARCH_MARKET:
354                 TextView searchView = (TextView) holder.itemView;
355                 if (mMarketSearchIntent != null) {
356                     searchView.setVisibility(View.VISIBLE);
357                 } else {
358                     searchView.setVisibility(View.GONE);
359                 }
360                 break;
361             case VIEW_TYPE_APPS_LOADING_DIVIDER:
362                 int visLoading = mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE;
363                 int visLoaded = !mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE;
364                 holder.itemView.findViewById(R.id.loadingProgressBar).setVisibility(visLoading);
365                 holder.itemView.findViewById(R.id.loadedDivider).setVisibility(visLoaded);
366                 break;
367             case VIEW_TYPE_SEARCH_MARKET_DIVIDER:
368                 // nothing to do
369                 break;
370         }
371         if (mBindViewCallback != null) {
372             mBindViewCallback.onBindView(holder);
373         }
374     }
375 
376     @Override
onViewAttachedToWindow(ViewHolder holder)377     public void onViewAttachedToWindow(ViewHolder holder) {
378         int type = holder.getItemViewType();
379         if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) {
380             mSpringAnimationHandler.add(holder.itemView, holder);
381         }
382     }
383 
384     @Override
onViewDetachedFromWindow(ViewHolder holder)385     public void onViewDetachedFromWindow(ViewHolder holder) {
386         int type = holder.getItemViewType();
387         if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) {
388             mSpringAnimationHandler.remove(holder.itemView);
389         }
390     }
391 
392     @Override
onFailedToRecycleView(ViewHolder holder)393     public boolean onFailedToRecycleView(ViewHolder holder) {
394         // Always recycle and we will reset the view when it is bound
395         return true;
396     }
397 
398     @Override
getItemCount()399     public int getItemCount() {
400         return mApps.getAdapterItems().size();
401     }
402 
403     @Override
getItemViewType(int position)404     public int getItemViewType(int position) {
405         AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
406         return item.viewType;
407     }
408 
409     /**
410      * Helper class to set the SpringAnimation values for an item in the adapter.
411      */
412     private class AllAppsSpringAnimationFactory
413             implements SpringAnimationHandler.AnimationFactory<ViewHolder> {
414         private static final float DEFAULT_MAX_VALUE_PX = 100;
415         private static final float DEFAULT_MIN_VALUE_PX = -DEFAULT_MAX_VALUE_PX;
416 
417         // Damping ratio range is [0, 1]
418         private static final float SPRING_DAMPING_RATIO = 0.55f;
419 
420         // Stiffness is a non-negative number.
421         private static final float MIN_SPRING_STIFFNESS = 580f;
422         private static final float MAX_SPRING_STIFFNESS = 900f;
423 
424         // The amount by which each adjacent rows' stiffness will differ.
425         private static final float ROW_STIFFNESS_COEFFICIENT = 50f;
426 
427         @Override
initialize(ViewHolder vh)428         public SpringAnimation initialize(ViewHolder vh) {
429             return SpringAnimationHandler.forView(vh.itemView, DynamicAnimation.TRANSLATION_Y, 0);
430         }
431 
432         /**
433          * @param spring A new or recycled SpringAnimation.
434          * @param vh The ViewHolder that {@param spring} is related to.
435          */
436         @Override
update(SpringAnimation spring, ViewHolder vh)437         public void update(SpringAnimation spring, ViewHolder vh) {
438             int numPredictedApps = Math.min(mAppsPerRow, mApps.getPredictedApps().size());
439             int appPosition = getAppPosition(vh.getAdapterPosition(), numPredictedApps,
440                     mAppsPerRow);
441 
442             int col = appPosition % mAppsPerRow;
443             int row = appPosition / mAppsPerRow;
444 
445             int numTotalRows = mApps.getNumAppRows() - 1; // zero-based count
446             if (row > (numTotalRows / 2)) {
447                 // Mirror the rows so that the top row acts the same as the bottom row.
448                 row = Math.abs(numTotalRows - row);
449             }
450 
451             calculateSpringValues(spring, row, col);
452         }
453 
454         @Override
setDefaultValues(SpringAnimation spring)455         public void setDefaultValues(SpringAnimation spring) {
456             calculateSpringValues(spring, 0, mAppsPerRow / 2);
457         }
458 
459         /**
460          * We manipulate the stiffness, min, and max values based on the items distance to the
461          * first row and the items distance to the center column to create the ^-shaped motion
462          * effect.
463          */
calculateSpringValues(SpringAnimation spring, int row, int col)464         private void calculateSpringValues(SpringAnimation spring, int row, int col) {
465             float rowFactor = (1 + row) * 0.5f;
466             float colFactor = getColumnFactor(col, mAppsPerRow);
467 
468             float minValue = DEFAULT_MIN_VALUE_PX * (rowFactor + colFactor);
469             float maxValue = DEFAULT_MAX_VALUE_PX * (rowFactor + colFactor);
470 
471             float stiffness = Utilities.boundToRange(
472                     MAX_SPRING_STIFFNESS - (row * ROW_STIFFNESS_COEFFICIENT),
473                     MIN_SPRING_STIFFNESS,
474                     MAX_SPRING_STIFFNESS);
475 
476             spring.setMinValue(minValue)
477                     .setMaxValue(maxValue)
478                     .getSpring()
479                     .setStiffness(stiffness)
480                     .setDampingRatio(SPRING_DAMPING_RATIO);
481         }
482 
483         /**
484          * @return The app position is the position of the app in the Adapter if we ignored all
485          * other view types.
486          *
487          * The first app is at position 0, and the first app each following row is at a
488          * position that is a multiple of {@param appsPerRow}.
489          *
490          * ie. If there are 5 apps per row, and there are two rows of apps:
491          *     0 1 2 3 4
492          *     5 6 7 8 9
493          */
getAppPosition(int position, int numPredictedApps, int appsPerRow)494         private int getAppPosition(int position, int numPredictedApps, int appsPerRow) {
495             if (position < numPredictedApps) {
496                 // Predicted apps are first in the adapter.
497                 return position;
498             }
499 
500             // There is at most 1 divider view between the predicted apps and the alphabetical apps.
501             int numDividerViews = numPredictedApps == 0 ? 0 : 1;
502 
503             // This offset takes into consideration an incomplete row of predicted apps.
504             int numPredictedAppsOffset = appsPerRow - numPredictedApps;
505             return position + numPredictedAppsOffset - numDividerViews;
506         }
507 
508         /**
509          * Increase the column factor as the distance increases between the column and the center
510          * column(s).
511          */
getColumnFactor(int col, int numCols)512         private float getColumnFactor(int col, int numCols) {
513             float centerColumn = numCols / 2;
514             int distanceToCenter = (int) Math.abs(col - centerColumn);
515 
516             boolean evenNumberOfColumns = numCols % 2 == 0;
517             if (evenNumberOfColumns && col < centerColumn) {
518                 distanceToCenter -= 1;
519             }
520 
521             float factor = 0;
522             while (distanceToCenter > 0) {
523                 if (distanceToCenter == 1) {
524                     factor += 0.2f;
525                 } else {
526                     factor += 0.1f;
527                 }
528                 --distanceToCenter;
529             }
530 
531             return factor;
532         }
533     }
534 }
535