Home | History | Annotate | Download | only in allapps
      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.res.Resources;
     20 import android.graphics.Canvas;
     21 import android.graphics.Paint;
     22 import android.graphics.PointF;
     23 import android.graphics.Rect;
     24 import android.os.Handler;
     25 import android.support.v7.widget.GridLayoutManager;
     26 import android.support.v7.widget.RecyclerView;
     27 import android.view.LayoutInflater;
     28 import android.view.View;
     29 import android.view.ViewConfiguration;
     30 import android.view.ViewGroup;
     31 import android.widget.TextView;
     32 import com.android.launcher3.AppInfo;
     33 import com.android.launcher3.BubbleTextView;
     34 import com.android.launcher3.R;
     35 import com.android.launcher3.Utilities;
     36 import com.android.launcher3.util.Thunk;
     37 
     38 import java.util.HashMap;
     39 import java.util.List;
     40 
     41 
     42 /**
     43  * The grid view adapter of all the apps.
     44  */
     45 class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> {
     46 
     47     public static final String TAG = "AppsGridAdapter";
     48     private static final boolean DEBUG = false;
     49 
     50     // A section break in the grid
     51     public static final int SECTION_BREAK_VIEW_TYPE = 0;
     52     // A normal icon
     53     public static final int ICON_VIEW_TYPE = 1;
     54     // A prediction icon
     55     public static final int PREDICTION_ICON_VIEW_TYPE = 2;
     56     // The message shown when there are no filtered results
     57     public static final int EMPTY_SEARCH_VIEW_TYPE = 3;
     58 
     59     /**
     60      * ViewHolder for each icon.
     61      */
     62     public static class ViewHolder extends RecyclerView.ViewHolder {
     63         public View mContent;
     64 
     65         public ViewHolder(View v) {
     66             super(v);
     67             mContent = v;
     68         }
     69     }
     70 
     71     /**
     72      * Helper class to size the grid items.
     73      */
     74     public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup {
     75 
     76         public GridSpanSizer() {
     77             super();
     78             setSpanIndexCacheEnabled(true);
     79         }
     80 
     81         @Override
     82         public int getSpanSize(int position) {
     83             if (mApps.hasNoFilteredResults()) {
     84                 // Empty view spans full width
     85                 return mAppsPerRow;
     86             }
     87 
     88             switch (mApps.getAdapterItems().get(position).viewType) {
     89                 case AllAppsGridAdapter.ICON_VIEW_TYPE:
     90                 case AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE:
     91                     return 1;
     92                 default:
     93                     // Section breaks span the full width
     94                     return mAppsPerRow;
     95             }
     96         }
     97     }
     98 
     99     /**
    100      * Helper class to draw the section headers
    101      */
    102     public class GridItemDecoration extends RecyclerView.ItemDecoration {
    103 
    104         private static final boolean DEBUG_SECTION_MARGIN = false;
    105         private static final boolean FADE_OUT_SECTIONS = false;
    106 
    107         private HashMap<String, PointF> mCachedSectionBounds = new HashMap<>();
    108         private Rect mTmpBounds = new Rect();
    109 
    110         @Override
    111         public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    112             if (mApps.hasFilter() || mAppsPerRow == 0) {
    113                 return;
    114             }
    115 
    116             if (DEBUG_SECTION_MARGIN) {
    117                 Paint p = new Paint();
    118                 p.setColor(0x33ff0000);
    119                 c.drawRect(mBackgroundPadding.left, 0, mBackgroundPadding.left + mSectionNamesMargin,
    120                         parent.getMeasuredHeight(), p);
    121             }
    122 
    123             List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
    124             boolean hasDrawnPredictedAppsDivider = false;
    125             boolean showSectionNames = mSectionNamesMargin > 0;
    126             int childCount = parent.getChildCount();
    127             int lastSectionTop = 0;
    128             int lastSectionHeight = 0;
    129             for (int i = 0; i < childCount; i++) {
    130                 View child = parent.getChildAt(i);
    131                 ViewHolder holder = (ViewHolder) parent.getChildViewHolder(child);
    132                 if (!isValidHolderAndChild(holder, child, items)) {
    133                     continue;
    134                 }
    135 
    136                 if (shouldDrawItemDivider(holder, items) && !hasDrawnPredictedAppsDivider) {
    137                     // Draw the divider under the predicted apps
    138                     int top = child.getTop() + child.getHeight() + mPredictionBarDividerOffset;
    139                     c.drawLine(mBackgroundPadding.left, top,
    140                             parent.getWidth() - mBackgroundPadding.right, top,
    141                             mPredictedAppsDividerPaint);
    142                     hasDrawnPredictedAppsDivider = true;
    143 
    144                 } else if (showSectionNames && shouldDrawItemSection(holder, i, items)) {
    145                     // At this point, we only draw sections for each section break;
    146                     int viewTopOffset = (2 * child.getPaddingTop());
    147                     int pos = holder.getPosition();
    148                     AlphabeticalAppsList.AdapterItem item = items.get(pos);
    149                     AlphabeticalAppsList.SectionInfo sectionInfo = item.sectionInfo;
    150 
    151                     // Draw all the sections for this index
    152                     String lastSectionName = item.sectionName;
    153                     for (int j = item.sectionAppIndex; j < sectionInfo.numApps; j++, pos++) {
    154                         AlphabeticalAppsList.AdapterItem nextItem = items.get(pos);
    155                         String sectionName = nextItem.sectionName;
    156                         if (nextItem.sectionInfo != sectionInfo) {
    157                             break;
    158                         }
    159                         if (j > item.sectionAppIndex && sectionName.equals(lastSectionName)) {
    160                             continue;
    161                         }
    162 
    163 
    164                         // Find the section name bounds
    165                         PointF sectionBounds = getAndCacheSectionBounds(sectionName);
    166 
    167                         // Calculate where to draw the section
    168                         int sectionBaseline = (int) (viewTopOffset + sectionBounds.y);
    169                         int x = mIsRtl ?
    170                                 parent.getWidth() - mBackgroundPadding.left - mSectionNamesMargin :
    171                                         mBackgroundPadding.left;
    172                         x += (int) ((mSectionNamesMargin - sectionBounds.x) / 2f);
    173                         int y = child.getTop() + sectionBaseline;
    174 
    175                         // Determine whether this is the last row with apps in that section, if
    176                         // so, then fix the section to the row allowing it to scroll past the
    177                         // baseline, otherwise, bound it to the baseline so it's in the viewport
    178                         int appIndexInSection = items.get(pos).sectionAppIndex;
    179                         int nextRowPos = Math.min(items.size() - 1,
    180                                 pos + mAppsPerRow - (appIndexInSection % mAppsPerRow));
    181                         AlphabeticalAppsList.AdapterItem nextRowItem = items.get(nextRowPos);
    182                         boolean fixedToRow = !sectionName.equals(nextRowItem.sectionName);
    183                         if (!fixedToRow) {
    184                             y = Math.max(sectionBaseline, y);
    185                         }
    186 
    187                         // In addition, if it overlaps with the last section that was drawn, then
    188                         // offset it so that it does not overlap
    189                         if (lastSectionHeight > 0 && y <= (lastSectionTop + lastSectionHeight)) {
    190                             y += lastSectionTop - y + lastSectionHeight;
    191                         }
    192 
    193                         // Draw the section header
    194                         if (FADE_OUT_SECTIONS) {
    195                             int alpha = 255;
    196                             if (fixedToRow) {
    197                                 alpha = Math.min(255,
    198                                         (int) (255 * (Math.max(0, y) / (float) sectionBaseline)));
    199                             }
    200                             mSectionTextPaint.setAlpha(alpha);
    201                         }
    202                         c.drawText(sectionName, x, y, mSectionTextPaint);
    203 
    204                         lastSectionTop = y;
    205                         lastSectionHeight = (int) (sectionBounds.y + mSectionHeaderOffset);
    206                         lastSectionName = sectionName;
    207                     }
    208                     i += (sectionInfo.numApps - item.sectionAppIndex);
    209                 }
    210             }
    211         }
    212 
    213         @Override
    214         public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
    215                 RecyclerView.State state) {
    216             // Do nothing
    217         }
    218 
    219         /**
    220          * Given a section name, return the bounds of the given section name.
    221          */
    222         private PointF getAndCacheSectionBounds(String sectionName) {
    223             PointF bounds = mCachedSectionBounds.get(sectionName);
    224             if (bounds == null) {
    225                 mSectionTextPaint.getTextBounds(sectionName, 0, sectionName.length(), mTmpBounds);
    226                 bounds = new PointF(mSectionTextPaint.measureText(sectionName), mTmpBounds.height());
    227                 mCachedSectionBounds.put(sectionName, bounds);
    228             }
    229             return bounds;
    230         }
    231 
    232         /**
    233          * Returns whether we consider this a valid view holder for us to draw a divider or section for.
    234          */
    235         private boolean isValidHolderAndChild(ViewHolder holder, View child,
    236                 List<AlphabeticalAppsList.AdapterItem> items) {
    237             // Ensure item is not already removed
    238             GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams)
    239                     child.getLayoutParams();
    240             if (lp.isItemRemoved()) {
    241                 return false;
    242             }
    243             // Ensure we have a valid holder
    244             if (holder == null) {
    245                 return false;
    246             }
    247             // Ensure we have a holder position
    248             int pos = holder.getPosition();
    249             if (pos < 0 || pos >= items.size()) {
    250                 return false;
    251             }
    252             return true;
    253         }
    254 
    255         /**
    256          * Returns whether to draw the divider for a given child.
    257          */
    258         private boolean shouldDrawItemDivider(ViewHolder holder,
    259                 List<AlphabeticalAppsList.AdapterItem> items) {
    260             int pos = holder.getPosition();
    261             return items.get(pos).viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE;
    262         }
    263 
    264         /**
    265          * Returns whether to draw the section for the given child.
    266          */
    267         private boolean shouldDrawItemSection(ViewHolder holder, int childIndex,
    268                 List<AlphabeticalAppsList.AdapterItem> items) {
    269             int pos = holder.getPosition();
    270             AlphabeticalAppsList.AdapterItem item = items.get(pos);
    271 
    272             // Ensure it's an icon
    273             if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) {
    274                 return false;
    275             }
    276             // Draw the section header for the first item in each section
    277             return (childIndex == 0) ||
    278                     (items.get(pos - 1).viewType == AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE);
    279         }
    280     }
    281 
    282     private LayoutInflater mLayoutInflater;
    283     @Thunk AlphabeticalAppsList mApps;
    284     private GridLayoutManager mGridLayoutMgr;
    285     private GridSpanSizer mGridSizer;
    286     private GridItemDecoration mItemDecoration;
    287     private View.OnTouchListener mTouchListener;
    288     private View.OnClickListener mIconClickListener;
    289     private View.OnLongClickListener mIconLongClickListener;
    290     @Thunk final Rect mBackgroundPadding = new Rect();
    291     @Thunk int mPredictionBarDividerOffset;
    292     @Thunk int mAppsPerRow;
    293     @Thunk boolean mIsRtl;
    294     private String mEmptySearchText;
    295 
    296     // Section drawing
    297     @Thunk int mSectionNamesMargin;
    298     @Thunk int mSectionHeaderOffset;
    299     @Thunk Paint mSectionTextPaint;
    300     @Thunk Paint mPredictedAppsDividerPaint;
    301 
    302     public AllAppsGridAdapter(Context context, AlphabeticalAppsList apps,
    303             View.OnTouchListener touchListener, View.OnClickListener iconClickListener,
    304             View.OnLongClickListener iconLongClickListener) {
    305         Resources res = context.getResources();
    306         mApps = apps;
    307         mGridSizer = new GridSpanSizer();
    308         mGridLayoutMgr = new GridLayoutManager(context, 1, GridLayoutManager.VERTICAL, false);
    309         mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
    310         mItemDecoration = new GridItemDecoration();
    311         mLayoutInflater = LayoutInflater.from(context);
    312         mTouchListener = touchListener;
    313         mIconClickListener = iconClickListener;
    314         mIconLongClickListener = iconLongClickListener;
    315         mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
    316         mSectionHeaderOffset = res.getDimensionPixelSize(R.dimen.all_apps_grid_section_y_offset);
    317 
    318         mSectionTextPaint = new Paint();
    319         mSectionTextPaint.setTextSize(res.getDimensionPixelSize(
    320                 R.dimen.all_apps_grid_section_text_size));
    321         mSectionTextPaint.setColor(res.getColor(R.color.all_apps_grid_section_text_color));
    322         mSectionTextPaint.setAntiAlias(true);
    323 
    324         mPredictedAppsDividerPaint = new Paint();
    325         mPredictedAppsDividerPaint.setStrokeWidth(Utilities.pxFromDp(1f, res.getDisplayMetrics()));
    326         mPredictedAppsDividerPaint.setColor(0x1E000000);
    327         mPredictedAppsDividerPaint.setAntiAlias(true);
    328         mPredictionBarDividerOffset =
    329                 (-res.getDimensionPixelSize(R.dimen.all_apps_prediction_icon_bottom_padding) +
    330                         res.getDimensionPixelSize(R.dimen.all_apps_icon_top_bottom_padding)) / 2;
    331     }
    332 
    333     /**
    334      * Sets the number of apps per row.
    335      */
    336     public void setNumAppsPerRow(int appsPerRow) {
    337         mAppsPerRow = appsPerRow;
    338         mGridLayoutMgr.setSpanCount(appsPerRow);
    339     }
    340 
    341     /**
    342      * Sets whether we are in RTL mode.
    343      */
    344     public void setRtl(boolean rtl) {
    345         mIsRtl = rtl;
    346     }
    347 
    348     /**
    349      * Sets the text to show when there are no apps.
    350      */
    351     public void setEmptySearchText(String query) {
    352         mEmptySearchText = query;
    353     }
    354 
    355     /**
    356      * Notifies the adapter of the background padding so that it can draw things correctly in the
    357      * item decorator.
    358      */
    359     public void updateBackgroundPadding(Rect padding) {
    360         mBackgroundPadding.set(padding);
    361     }
    362 
    363     /**
    364      * Returns the grid layout manager.
    365      */
    366     public GridLayoutManager getLayoutManager() {
    367         return mGridLayoutMgr;
    368     }
    369 
    370     /**
    371      * Returns the item decoration for the recycler view.
    372      */
    373     public RecyclerView.ItemDecoration getItemDecoration() {
    374         // We don't draw any headers when we are uncomfortably dense
    375         return mItemDecoration;
    376     }
    377 
    378     @Override
    379     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    380         switch (viewType) {
    381             case EMPTY_SEARCH_VIEW_TYPE:
    382                 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, parent,
    383                         false));
    384             case SECTION_BREAK_VIEW_TYPE:
    385                 return new ViewHolder(new View(parent.getContext()));
    386             case ICON_VIEW_TYPE: {
    387                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
    388                         R.layout.all_apps_icon, parent, false);
    389                 icon.setOnTouchListener(mTouchListener);
    390                 icon.setOnClickListener(mIconClickListener);
    391                 icon.setOnLongClickListener(mIconLongClickListener);
    392                 icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext())
    393                         .getLongPressTimeout());
    394                 icon.setFocusable(true);
    395                 return new ViewHolder(icon);
    396             }
    397             case PREDICTION_ICON_VIEW_TYPE: {
    398                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
    399                         R.layout.all_apps_prediction_bar_icon, parent, false);
    400                 icon.setOnTouchListener(mTouchListener);
    401                 icon.setOnClickListener(mIconClickListener);
    402                 icon.setOnLongClickListener(mIconLongClickListener);
    403                 icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext())
    404                         .getLongPressTimeout());
    405                 icon.setFocusable(true);
    406                 return new ViewHolder(icon);
    407             }
    408             default:
    409                 throw new RuntimeException("Unexpected view type");
    410         }
    411     }
    412 
    413     @Override
    414     public void onBindViewHolder(ViewHolder holder, int position) {
    415         switch (holder.getItemViewType()) {
    416             case ICON_VIEW_TYPE: {
    417                 AppInfo info = mApps.getAdapterItems().get(position).appInfo;
    418                 BubbleTextView icon = (BubbleTextView) holder.mContent;
    419                 icon.applyFromApplicationInfo(info);
    420                 break;
    421             }
    422             case PREDICTION_ICON_VIEW_TYPE: {
    423                 AppInfo info = mApps.getAdapterItems().get(position).appInfo;
    424                 BubbleTextView icon = (BubbleTextView) holder.mContent;
    425                 icon.applyFromApplicationInfo(info);
    426                 break;
    427             }
    428             case EMPTY_SEARCH_VIEW_TYPE:
    429                 TextView emptyViewText = (TextView) holder.mContent.findViewById(R.id.empty_text);
    430                 emptyViewText.setText(mEmptySearchText);
    431                 break;
    432         }
    433     }
    434 
    435     @Override
    436     public int getItemCount() {
    437         if (mApps.hasNoFilteredResults()) {
    438             // For the empty view
    439             return 1;
    440         }
    441         return mApps.getAdapterItems().size();
    442     }
    443 
    444     @Override
    445     public int getItemViewType(int position) {
    446         if (mApps.hasNoFilteredResults()) {
    447             return EMPTY_SEARCH_VIEW_TYPE;
    448         }
    449 
    450         AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
    451         return item.viewType;
    452     }
    453 }
    454