| /* |
| * 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.content.Intent; |
| import android.content.res.Resources; |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.graphics.PointF; |
| import android.graphics.Rect; |
| import android.support.v4.view.accessibility.AccessibilityEventCompat; |
| import android.support.v4.view.accessibility.AccessibilityRecordCompat; |
| 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.View.OnFocusChangeListener; |
| 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.shortcuts.DeepShortcutManager; |
| import com.android.launcher3.shortcuts.ShortcutsContainerListener; |
| |
| import java.util.HashMap; |
| import java.util.List; |
| |
| |
| /** |
| * The grid view adapter of all the apps. |
| */ |
| public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> { |
| |
| public static final String TAG = "AppsGridAdapter"; |
| private static final boolean DEBUG = false; |
| |
| // A section break in the grid |
| public static final int VIEW_TYPE_SECTION_BREAK = 1 << 0; |
| // A normal icon |
| public static final int VIEW_TYPE_ICON = 1 << 1; |
| // A prediction icon |
| public static final int VIEW_TYPE_PREDICTION_ICON = 1 << 2; |
| // The message shown when there are no filtered results |
| public static final int VIEW_TYPE_EMPTY_SEARCH = 1 << 3; |
| // The message to continue to a market search when there are no filtered results |
| public static final int VIEW_TYPE_SEARCH_MARKET = 1 << 4; |
| |
| // We use various dividers for various purposes. They share enough attributes to reuse layouts, |
| // but differ in enough attributes to require different view types |
| |
| // A divider that separates the apps list and the search market button |
| public static final int VIEW_TYPE_SEARCH_MARKET_DIVIDER = 1 << 5; |
| // The divider under the search field |
| public static final int VIEW_TYPE_SEARCH_DIVIDER = 1 << 6; |
| // The divider that separates prediction icons from the app list |
| public static final int VIEW_TYPE_PREDICTION_DIVIDER = 1 << 7; |
| |
| // Common view type masks |
| public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_SEARCH_DIVIDER |
| | VIEW_TYPE_SEARCH_MARKET_DIVIDER |
| | VIEW_TYPE_PREDICTION_DIVIDER |
| | VIEW_TYPE_SECTION_BREAK; |
| public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON |
| | VIEW_TYPE_PREDICTION_ICON; |
| |
| |
| public interface BindViewCallback { |
| public void onBindView(ViewHolder holder); |
| } |
| |
| /** |
| * ViewHolder for each icon. |
| */ |
| public static class ViewHolder extends RecyclerView.ViewHolder { |
| public View mContent; |
| |
| public ViewHolder(View v) { |
| super(v); |
| mContent = v; |
| } |
| } |
| |
| /** |
| * 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 { |
| |
| public GridSpanSizer() { |
| super(); |
| setSpanIndexCacheEnabled(true); |
| } |
| |
| @Override |
| public int getSpanSize(int position) { |
| if (isIconViewType(mApps.getAdapterItems().get(position).viewType)) { |
| return 1; |
| } else { |
| // Section breaks span the full width |
| return mAppsPerRow; |
| } |
| } |
| } |
| |
| /** |
| * Helper class to draw the section headers |
| */ |
| public class GridItemDecoration extends RecyclerView.ItemDecoration { |
| |
| private static final boolean DEBUG_SECTION_MARGIN = false; |
| private static final boolean FADE_OUT_SECTIONS = false; |
| |
| private HashMap<String, PointF> mCachedSectionBounds = new HashMap<>(); |
| private Rect mTmpBounds = new Rect(); |
| |
| @Override |
| public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { |
| if (mApps.hasFilter() || mAppsPerRow == 0) { |
| return; |
| } |
| |
| if (DEBUG_SECTION_MARGIN) { |
| Paint p = new Paint(); |
| p.setColor(0x33ff0000); |
| c.drawRect(mBackgroundPadding.left, 0, mBackgroundPadding.left + mSectionNamesMargin, |
| parent.getMeasuredHeight(), p); |
| } |
| |
| List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); |
| boolean showSectionNames = mSectionNamesMargin > 0; |
| int childCount = parent.getChildCount(); |
| int lastSectionTop = 0; |
| int lastSectionHeight = 0; |
| for (int i = 0; i < childCount; i++) { |
| View child = parent.getChildAt(i); |
| ViewHolder holder = (ViewHolder) parent.getChildViewHolder(child); |
| if (!isValidHolderAndChild(holder, child, items)) { |
| continue; |
| } |
| |
| if (showSectionNames && shouldDrawItemSection(holder, i, items)) { |
| // At this point, we only draw sections for each section break; |
| int viewTopOffset = (2 * child.getPaddingTop()); |
| int pos = holder.getPosition(); |
| AlphabeticalAppsList.AdapterItem item = items.get(pos); |
| AlphabeticalAppsList.SectionInfo sectionInfo = item.sectionInfo; |
| |
| // Draw all the sections for this index |
| String lastSectionName = item.sectionName; |
| for (int j = item.sectionAppIndex; j < sectionInfo.numApps; j++, pos++) { |
| AlphabeticalAppsList.AdapterItem nextItem = items.get(pos); |
| String sectionName = nextItem.sectionName; |
| if (nextItem.sectionInfo != sectionInfo) { |
| break; |
| } |
| if (j > item.sectionAppIndex && sectionName.equals(lastSectionName)) { |
| continue; |
| } |
| |
| // Find the section name bounds |
| PointF sectionBounds = getAndCacheSectionBounds(sectionName); |
| |
| // Calculate where to draw the section |
| int sectionBaseline = (int) (viewTopOffset + sectionBounds.y); |
| int x = mIsRtl ? |
| parent.getWidth() - mBackgroundPadding.left - mSectionNamesMargin : |
| mBackgroundPadding.left; |
| x += (int) ((mSectionNamesMargin - sectionBounds.x) / 2f); |
| int y = child.getTop() + sectionBaseline; |
| |
| // Determine whether this is the last row with apps in that section, if |
| // so, then fix the section to the row allowing it to scroll past the |
| // baseline, otherwise, bound it to the baseline so it's in the viewport |
| int appIndexInSection = items.get(pos).sectionAppIndex; |
| int nextRowPos = Math.min(items.size() - 1, |
| pos + mAppsPerRow - (appIndexInSection % mAppsPerRow)); |
| AlphabeticalAppsList.AdapterItem nextRowItem = items.get(nextRowPos); |
| boolean fixedToRow = !sectionName.equals(nextRowItem.sectionName); |
| if (!fixedToRow) { |
| y = Math.max(sectionBaseline, y); |
| } |
| |
| // In addition, if it overlaps with the last section that was drawn, then |
| // offset it so that it does not overlap |
| if (lastSectionHeight > 0 && y <= (lastSectionTop + lastSectionHeight)) { |
| y += lastSectionTop - y + lastSectionHeight; |
| } |
| |
| // Draw the section header |
| if (FADE_OUT_SECTIONS) { |
| int alpha = 255; |
| if (fixedToRow) { |
| alpha = Math.min(255, |
| (int) (255 * (Math.max(0, y) / (float) sectionBaseline))); |
| } |
| mSectionTextPaint.setAlpha(alpha); |
| } |
| c.drawText(sectionName, x, y, mSectionTextPaint); |
| |
| lastSectionTop = y; |
| lastSectionHeight = (int) (sectionBounds.y + mSectionHeaderOffset); |
| lastSectionName = sectionName; |
| } |
| i += (sectionInfo.numApps - item.sectionAppIndex); |
| } |
| } |
| } |
| |
| @Override |
| public void getItemOffsets(Rect outRect, View view, RecyclerView parent, |
| RecyclerView.State state) { |
| // Do nothing |
| } |
| |
| /** |
| * Given a section name, return the bounds of the given section name. |
| */ |
| private PointF getAndCacheSectionBounds(String sectionName) { |
| PointF bounds = mCachedSectionBounds.get(sectionName); |
| if (bounds == null) { |
| mSectionTextPaint.getTextBounds(sectionName, 0, sectionName.length(), mTmpBounds); |
| bounds = new PointF(mSectionTextPaint.measureText(sectionName), mTmpBounds.height()); |
| mCachedSectionBounds.put(sectionName, bounds); |
| } |
| return bounds; |
| } |
| |
| /** |
| * Returns whether we consider this a valid view holder for us to draw a divider or section for. |
| */ |
| private boolean isValidHolderAndChild(ViewHolder holder, View child, |
| List<AlphabeticalAppsList.AdapterItem> items) { |
| // Ensure item is not already removed |
| GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) |
| child.getLayoutParams(); |
| if (lp.isItemRemoved()) { |
| return false; |
| } |
| // Ensure we have a valid holder |
| if (holder == null) { |
| return false; |
| } |
| // Ensure we have a holder position |
| int pos = holder.getPosition(); |
| if (pos < 0 || pos >= items.size()) { |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Returns whether to draw the section for the given child. |
| */ |
| private boolean shouldDrawItemSection(ViewHolder holder, int childIndex, |
| List<AlphabeticalAppsList.AdapterItem> items) { |
| int pos = holder.getPosition(); |
| AlphabeticalAppsList.AdapterItem item = items.get(pos); |
| |
| // Ensure it's an icon |
| if (item.viewType != AllAppsGridAdapter.VIEW_TYPE_ICON) { |
| return false; |
| } |
| // Draw the section header for the first item in each section |
| return (childIndex == 0) || |
| (items.get(pos - 1).viewType == AllAppsGridAdapter.VIEW_TYPE_SECTION_BREAK); |
| } |
| } |
| |
| private final Launcher mLauncher; |
| private final LayoutInflater mLayoutInflater; |
| private final AlphabeticalAppsList mApps; |
| private final GridLayoutManager mGridLayoutMgr; |
| private final GridSpanSizer mGridSizer; |
| private final GridItemDecoration mItemDecoration; |
| private final View.OnClickListener mIconClickListener; |
| private final View.OnLongClickListener mIconLongClickListener; |
| |
| private final Rect mBackgroundPadding = new Rect(); |
| private final boolean mIsRtl; |
| |
| // Section drawing |
| private final int mSectionNamesMargin; |
| private final int mSectionHeaderOffset; |
| private final Paint mSectionTextPaint; |
| |
| private int mAppsPerRow; |
| |
| private BindViewCallback mBindViewCallback; |
| private AllAppsSearchBarController mSearchController; |
| private OnFocusChangeListener mIconFocusListener; |
| |
| // The text to show when there are no search results and no market search handler. |
| private String mEmptySearchMessage; |
| // The intent to send off to the market app, updated each time the search query changes. |
| private Intent mMarketSearchIntent; |
| |
| public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener |
| iconClickListener, View.OnLongClickListener iconLongClickListener) { |
| Resources res = launcher.getResources(); |
| mLauncher = launcher; |
| mApps = apps; |
| mEmptySearchMessage = res.getString(R.string.all_apps_loading_message); |
| mGridSizer = new GridSpanSizer(); |
| mGridLayoutMgr = new AppsGridLayoutManager(launcher); |
| mGridLayoutMgr.setSpanSizeLookup(mGridSizer); |
| mItemDecoration = new GridItemDecoration(); |
| mLayoutInflater = LayoutInflater.from(launcher); |
| mIconClickListener = iconClickListener; |
| mIconLongClickListener = iconLongClickListener; |
| mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin); |
| mSectionHeaderOffset = res.getDimensionPixelSize(R.dimen.all_apps_grid_section_y_offset); |
| mIsRtl = Utilities.isRtl(res); |
| |
| mSectionTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| mSectionTextPaint.setTextSize(res.getDimensionPixelSize( |
| R.dimen.all_apps_grid_section_text_size)); |
| mSectionTextPaint.setColor(Utilities.getColorAccent(launcher)); |
| } |
| |
| public static boolean isDividerViewType(int viewType) { |
| return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER); |
| } |
| |
| public static boolean isIconViewType(int viewType) { |
| return isViewType(viewType, VIEW_TYPE_MASK_ICON); |
| } |
| |
| public static boolean isViewType(int viewType, int viewTypeMask) { |
| return (viewType & viewTypeMask) != 0; |
| } |
| |
| /** |
| * Sets the number of apps per row. |
| */ |
| public void setNumAppsPerRow(int appsPerRow) { |
| mAppsPerRow = appsPerRow; |
| mGridLayoutMgr.setSpanCount(appsPerRow); |
| } |
| |
| public void setSearchController(AllAppsSearchBarController searchController) { |
| mSearchController = searchController; |
| } |
| |
| public void setIconFocusListener(OnFocusChangeListener focusListener) { |
| mIconFocusListener = focusListener; |
| } |
| |
| /** |
| * 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 setLastSearchQuery(String query) { |
| Resources res = mLauncher.getResources(); |
| mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query); |
| mMarketSearchIntent = mSearchController.createMarketSearchIntent(query); |
| } |
| |
| /** |
| * Sets the callback for when views are bound. |
| */ |
| public void setBindViewCallback(BindViewCallback cb) { |
| mBindViewCallback = cb; |
| } |
| |
| /** |
| * Notifies the adapter of the background padding so that it can draw things correctly in the |
| * item decorator. |
| */ |
| public void updateBackgroundPadding(Rect padding) { |
| mBackgroundPadding.set(padding); |
| } |
| |
| /** |
| * Returns the grid layout manager. |
| */ |
| public GridLayoutManager getLayoutManager() { |
| return mGridLayoutMgr; |
| } |
| |
| /** |
| * Returns the item decoration for the recycler view. |
| */ |
| public RecyclerView.ItemDecoration getItemDecoration() { |
| // We don't draw any headers when we are uncomfortably dense |
| return mItemDecoration; |
| } |
| |
| @Override |
| public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |
| switch (viewType) { |
| case VIEW_TYPE_SECTION_BREAK: |
| return new ViewHolder(new View(parent.getContext())); |
| case VIEW_TYPE_ICON: |
| /* falls through */ |
| case VIEW_TYPE_PREDICTION_ICON: { |
| BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( |
| R.layout.all_apps_icon, parent, false); |
| icon.setOnClickListener(mIconClickListener); |
| icon.setOnLongClickListener(mIconLongClickListener); |
| icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext()) |
| .getLongPressTimeout()); |
| icon.setOnFocusChangeListener(mIconFocusListener); |
| return new ViewHolder(icon); |
| } |
| case VIEW_TYPE_EMPTY_SEARCH: |
| return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, |
| parent, false)); |
| case VIEW_TYPE_SEARCH_MARKET: |
| View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market, |
| parent, false); |
| searchMarketView.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| mLauncher.startActivitySafely(v, mMarketSearchIntent, null); |
| } |
| }); |
| return new ViewHolder(searchMarketView); |
| case VIEW_TYPE_SEARCH_DIVIDER: |
| return new ViewHolder(mLayoutInflater.inflate( |
| R.layout.all_apps_search_divider, parent, false)); |
| case VIEW_TYPE_PREDICTION_DIVIDER: |
| /* falls through */ |
| case VIEW_TYPE_SEARCH_MARKET_DIVIDER: |
| return new ViewHolder(mLayoutInflater.inflate( |
| R.layout.all_apps_divider, parent, false)); |
| default: |
| throw new RuntimeException("Unexpected view type"); |
| } |
| } |
| |
| @Override |
| public void onBindViewHolder(ViewHolder holder, int position) { |
| switch (holder.getItemViewType()) { |
| case VIEW_TYPE_ICON: { |
| AppInfo info = mApps.getAdapterItems().get(position).appInfo; |
| BubbleTextView icon = (BubbleTextView) holder.mContent; |
| icon.applyFromApplicationInfo(info); |
| if (DeepShortcutManager.supportsShortcuts(info)) { |
| // TODO: only add this listener if the item has shortcuts associated with it. |
| icon.setOnTouchListener(new ShortcutsContainerListener(icon)); |
| } |
| icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate()); |
| break; |
| } |
| case VIEW_TYPE_PREDICTION_ICON: { |
| AppInfo info = mApps.getAdapterItems().get(position).appInfo; |
| BubbleTextView icon = (BubbleTextView) holder.mContent; |
| icon.applyFromApplicationInfo(info); |
| if (DeepShortcutManager.supportsShortcuts(info)) { |
| // TODO: only add this listener if the item has shortcuts associated with it. |
| icon.setOnTouchListener(new ShortcutsContainerListener(icon)); |
| } |
| icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate()); |
| break; |
| } |
| case VIEW_TYPE_EMPTY_SEARCH: |
| TextView emptyViewText = (TextView) holder.mContent; |
| emptyViewText.setText(mEmptySearchMessage); |
| emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER : |
| Gravity.START | Gravity.CENTER_VERTICAL); |
| break; |
| case VIEW_TYPE_SEARCH_MARKET: |
| TextView searchView = (TextView) holder.mContent; |
| if (mMarketSearchIntent != null) { |
| searchView.setVisibility(View.VISIBLE); |
| } else { |
| searchView.setVisibility(View.GONE); |
| } |
| break; |
| } |
| if (mBindViewCallback != null) { |
| mBindViewCallback.onBindView(holder); |
| } |
| } |
| |
| @Override |
| public boolean onFailedToRecycleView(ViewHolder holder) { |
| // Always recycle and we will reset the view when it is bound |
| return true; |
| } |
| |
| @Override |
| public int getItemCount() { |
| return mApps.getAdapterItems().size(); |
| } |
| |
| @Override |
| public int getItemViewType(int position) { |
| AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); |
| return item.viewType; |
| } |
| } |