Files
Lawnchair/src/com/android/launcher3/AppsGridAdapter.java
T
Adam Cohen 2e6da1539b Refactoring DeviceProfile
-> Pulling out the parts of device profile which can (and need to be)
   initialized and accessed without access to an Activity context,
   ie. the invariant bits.
-> The invariant bits are stored in InvariantDeviceProfile which is
   initialized statically from LauncherAppState.
-> The DeviceProfile contains the Activity context-dependent bits,
   and we will create one of these for each Activity instance, and
   this instance is accessed through the Launcher activity.
-> It's possible that we can continue to refactor this such that
   all appropriate dimensions can be computed without an Activity
   context (by only specifying orientation). This would be an
   extension of this CL and allow us to know exactly how launcher
   will look in both orientations from any context.

Sets the stage for some improvements around b/19514688

Change-Id: Ia7daccf14d8ca2b9cb340b8780b684769e9f1892
2015-05-21 16:16:40 -07:00

466 lines
18 KiB
Java

/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.android.launcher3.util.Thunk;
import java.util.HashMap;
import java.util.List;
/**
* The grid view adapter of all the apps.
*/
class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
public static final String TAG = "AppsGridAdapter";
private static final boolean DEBUG = false;
// A section break in the grid
public static final int SECTION_BREAK_VIEW_TYPE = 0;
// A normal icon
public static final int ICON_VIEW_TYPE = 1;
// The message shown when there are no filtered results
public static final int EMPTY_VIEW_TYPE = 2;
// The spacer used for the prediction bar
public static final int PREDICTION_BAR_SPACER_TYPE = 3;
/**
* Callback for when the prediction bar spacer is bound.
*/
public interface PredictionBarSpacerCallbacks {
void onBindPredictionBar();
}
/**
* ViewHolder for each icon.
*/
public static class ViewHolder extends RecyclerView.ViewHolder {
public View mContent;
public ViewHolder(View v) {
super(v);
mContent = v;
}
}
/**
* 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 (mApps.hasNoFilteredResults()) {
// Empty view spans full width
return mAppsPerRow;
}
if (mApps.getAdapterItems().get(position).viewType != AppsGridAdapter.ICON_VIEW_TYPE) {
// Both the section breaks and predictive bar span the full width
return mAppsPerRow;
} else {
return 1;
}
}
}
/**
* Helper class to draw the section headers
*/
public class GridItemDecoration extends RecyclerView.ItemDecoration {
private static final boolean FADE_OUT_SECTIONS = false;
private HashMap<String, PointF> mCachedSectionBounds = new HashMap<>();
private Rect mTmpBounds = new Rect();
private Launcher mLauncher;
public GridItemDecoration(Context context) {
mLauncher = (Launcher) context;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (mApps.hasFilter()) {
return;
}
DeviceProfile grid = mLauncher.getDeviceProfile();
List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
boolean hasDrawnPredictedAppsDivider = false;
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 (shouldDrawItemDivider(holder, items) && !hasDrawnPredictedAppsDivider) {
// Draw the divider under the predicted apps
int top = child.getTop() + child.getHeight();
c.drawLine(mBackgroundPadding.left, top,
parent.getWidth() - mBackgroundPadding.right, top,
mPredictedAppsDividerPaint);
hasDrawnPredictedAppsDivider = true;
} else if (grid.isPhone() && 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() - mPaddingStart - mStartMargin :
mPaddingStart;
x += (int) ((mStartMargin - 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 divider for a given child.
*/
private boolean shouldDrawItemDivider(ViewHolder holder,
List<AlphabeticalAppsList.AdapterItem> items) {
int pos = holder.getPosition();
return items.get(pos).viewType == AppsGridAdapter.PREDICTION_BAR_SPACER_TYPE;
}
/**
* 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 != AppsGridAdapter.ICON_VIEW_TYPE) {
return false;
}
// Draw the section header for the first item in each section
return (childIndex == 0) ||
(items.get(pos - 1).viewType == AppsGridAdapter.SECTION_BREAK_VIEW_TYPE);
}
}
private Handler mHandler;
private LayoutInflater mLayoutInflater;
@Thunk AlphabeticalAppsList mApps;
private GridLayoutManager mGridLayoutMgr;
private GridSpanSizer mGridSizer;
private GridItemDecoration mItemDecoration;
private PredictionBarSpacerCallbacks mPredictionBarCb;
private View.OnTouchListener mTouchListener;
private View.OnClickListener mIconClickListener;
private View.OnLongClickListener mIconLongClickListener;
@Thunk final Rect mBackgroundPadding = new Rect();
@Thunk int mPredictionBarHeight;
@Thunk int mAppsPerRow;
@Thunk boolean mIsRtl;
private String mEmptySearchText;
// Section drawing
@Thunk int mPaddingStart;
@Thunk int mStartMargin;
@Thunk int mSectionHeaderOffset;
@Thunk Paint mSectionTextPaint;
@Thunk Paint mPredictedAppsDividerPaint;
public AppsGridAdapter(Context context, AlphabeticalAppsList apps, int appsPerRow,
PredictionBarSpacerCallbacks pbCb, View.OnTouchListener touchListener,
View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener) {
Resources res = context.getResources();
mHandler = new Handler();
mApps = apps;
mAppsPerRow = appsPerRow;
mPredictionBarCb = pbCb;
mGridSizer = new GridSpanSizer();
mGridLayoutMgr = new GridLayoutManager(context, appsPerRow, GridLayoutManager.VERTICAL,
false);
mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
mItemDecoration = new GridItemDecoration(context);
mLayoutInflater = LayoutInflater.from(context);
mTouchListener = touchListener;
mIconClickListener = iconClickListener;
mIconLongClickListener = iconLongClickListener;
mStartMargin = res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin);
mSectionHeaderOffset = res.getDimensionPixelSize(R.dimen.apps_grid_section_y_offset);
mPaddingStart = res.getDimensionPixelSize(R.dimen.apps_container_inset);
mSectionTextPaint = new Paint();
mSectionTextPaint.setTextSize(res.getDimensionPixelSize(
R.dimen.apps_view_section_text_size));
mSectionTextPaint.setColor(res.getColor(R.color.apps_view_section_text_color));
mSectionTextPaint.setAntiAlias(true);
mPredictedAppsDividerPaint = new Paint();
mPredictedAppsDividerPaint.setStrokeWidth(Utilities.pxFromDp(1f, res.getDisplayMetrics()));
mPredictedAppsDividerPaint.setColor(0x1E000000);
mPredictedAppsDividerPaint.setAntiAlias(true);
}
/**
* Sets the number of apps per row.
*/
public void setNumAppsPerRow(int appsPerRow) {
mAppsPerRow = appsPerRow;
mGridLayoutMgr.setSpanCount(appsPerRow);
}
/**
* Sets the prediction row height.
*/
public void setPredictionRowHeight(int height) {
mPredictionBarHeight = height;
}
/**
* Sets whether we are in RTL mode.
*/
public void setRtl(boolean rtl) {
mIsRtl = rtl;
}
/**
* Sets the text to show when there are no apps.
*/
public void setEmptySearchText(String query) {
mEmptySearchText = query;
}
/**
* Notifies the adapter of the background padding so that it can draw things correctly in the
* item decorator.
*/
public void updateBackgroundPadding(Drawable background) {
background.getPadding(mBackgroundPadding);
}
/**
* 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;
}
/**
* Returns the left padding for the recycler view.
*/
public int getContentMarginStart() {
return mStartMargin;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case EMPTY_VIEW_TYPE:
return new ViewHolder(mLayoutInflater.inflate(R.layout.apps_empty_view, parent,
false));
case SECTION_BREAK_VIEW_TYPE:
return new ViewHolder(new View(parent.getContext()));
case PREDICTION_BAR_SPACER_TYPE:
// Create a view of a specific height to match the floating prediction bar
View v = new View(parent.getContext());
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, mPredictionBarHeight);
v.setLayoutParams(lp);
return new ViewHolder(v);
case ICON_VIEW_TYPE:
BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
R.layout.apps_grid_icon_view, parent, false);
icon.setOnTouchListener(mTouchListener);
icon.setOnClickListener(mIconClickListener);
icon.setOnLongClickListener(mIconLongClickListener);
icon.setFocusable(true);
return new ViewHolder(icon);
default:
throw new RuntimeException("Unexpected view type");
}
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
switch (holder.getItemViewType()) {
case ICON_VIEW_TYPE:
AppInfo info = mApps.getAdapterItems().get(position).appInfo;
BubbleTextView icon = (BubbleTextView) holder.mContent;
icon.applyFromApplicationInfo(info);
break;
case PREDICTION_BAR_SPACER_TYPE:
mHandler.post(new Runnable() {
@Override
public void run() {
if (mPredictionBarCb != null) {
mPredictionBarCb.onBindPredictionBar();
}
}
});
break;
case EMPTY_VIEW_TYPE:
TextView emptyViewText = (TextView) holder.mContent.findViewById(R.id.empty_text);
emptyViewText.setText(mEmptySearchText);
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_VIEW_TYPE;
}
AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
return item.viewType;
}
}