Merge "Updating sticky headers." into ub-launcher3-burnaby

This commit is contained in:
Winson Chung
2015-05-09 00:38:08 +00:00
committed by Android (Google) Code Review
5 changed files with 246 additions and 85 deletions
+2 -1
View File
@@ -43,7 +43,7 @@
android:paddingBottom="12dp"
android:contentDescription="@string/all_apps_button_label"
android:src="@drawable/ic_arrow_back_grey" />
<EditText
<com.android.launcher3.AppsContainerSearchEditTextView
android:id="@+id/app_search_box"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -59,6 +59,7 @@
android:textColor="#4c4c4c"
android:textColorHint="#9c9c9c"
android:imeOptions="actionDone|flagNoExtractUi"
android:focusableInTouchMode="true"
android:background="@android:color/transparent" />
</LinearLayout>
<ImageView
@@ -104,28 +104,38 @@ public class AlphabeticalAppsList {
public int position;
// Whether or not the item at this adapter position is a section or not
public boolean isSectionHeader;
// The name of this section, or the section that this app is contained in
// The name of this section, or the section section name of the app. Note that if this
// app was merged into another section, then this may be a different name than the
// sectionInfo's sectionName
public String sectionName;
// The section to which this app belongs
public SectionInfo sectionInfo;
// The index of this app in the section
public int sectionAppIndex;
// The associated AppInfo, or null if this adapter item is a section
public AppInfo appInfo;
// The index of this app (not including sections), or -1 if this adapter item is a section
public int appIndex;
public static AdapterItem asSection(int pos, String name) {
public static AdapterItem asSection(int pos, SectionInfo section) {
AdapterItem item = new AdapterItem();
item.position = pos;
item.isSectionHeader = true;
item.sectionName = name;
item.sectionInfo = section;
item.sectionName = section.sectionName;
item.appInfo = null;
item.appIndex = -1;
return item;
}
public static AdapterItem asApp(int pos, String sectionName, AppInfo appInfo, int appIndex) {
public static AdapterItem asApp(int pos, SectionInfo section, String sectionName,
int sectionAppIndex, AppInfo appInfo, int appIndex) {
AdapterItem item = new AdapterItem();
item.position = pos;
item.isSectionHeader = false;
item.sectionInfo = section;
item.sectionName = sectionName;
item.sectionAppIndex = sectionAppIndex;
item.appInfo = appInfo;
item.appIndex = appIndex;
return item;
@@ -140,7 +150,7 @@ public class AlphabeticalAppsList {
}
// The maximum number of rows allowed in a merged section before we stop merging
private static final int MAX_ROWS_IN_MERGED_SECTION = Integer.MAX_VALUE;
private static final int MAX_ROWS_IN_MERGED_SECTION = 3;
private List<AppInfo> mApps = new ArrayList<>();
private List<AppInfo> mFilteredApps = new ArrayList<>();
@@ -314,6 +324,7 @@ public class AlphabeticalAppsList {
SectionInfo lastSectionInfo = null;
int position = 0;
int appIndex = 0;
int sectionAppIndex = 0;
for (AppInfo info : mApps) {
String sectionName = mIndexer.computeSectionName(info.title.toString().trim());
@@ -325,11 +336,12 @@ public class AlphabeticalAppsList {
// Create a new section if necessary
if (lastSectionInfo == null || !lastSectionInfo.sectionName.equals(sectionName)) {
lastSectionInfo = new SectionInfo(sectionName);
sectionAppIndex = 0;
mSections.add(lastSectionInfo);
// Create a new section item, this item is used to break the flow of items in the
// list
AdapterItem sectionItem = AdapterItem.asSection(position++, sectionName);
AdapterItem sectionItem = AdapterItem.asSection(position++, lastSectionInfo);
if (!AppsContainerView.GRID_HIDE_SECTION_HEADERS && !hasFilter()) {
lastSectionInfo.sectionItem = sectionItem;
mSectionedFilteredApps.add(sectionItem);
@@ -337,7 +349,8 @@ public class AlphabeticalAppsList {
}
// Create an app item
AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, appIndex++);
AdapterItem appItem = AdapterItem.asApp(position++, lastSectionInfo, sectionName,
sectionAppIndex++, info, appIndex++);
lastSectionInfo.numAppsInSection++;
if (lastSectionInfo.firstAppItem == null) {
lastSectionInfo.firstAppItem = appItem;
@@ -361,25 +374,33 @@ public class AlphabeticalAppsList {
// some limit, and also if there are no lessons to merge.
while (0 < (sectionAppCount % mNumAppsPerRow) &&
(sectionAppCount % mNumAppsPerRow) < minNumAppsPerRow &&
(int) Math.ceil(sectionAppCount / mNumAppsPerRow) < MAX_ROWS_IN_MERGED_SECTION &&
(sectionAppCount / mNumAppsPerRow) < MAX_ROWS_IN_MERGED_SECTION &&
(i + 1) < mSections.size()) {
SectionInfo nextSection = mSections.remove(i + 1);
// Merge the section names
if (AppsContainerView.GRID_MERGE_SECTION_HEADERS) {
mergedSectionName += nextSection.sectionName;
}
// Remove the next section break
mSectionedFilteredApps.remove(nextSection.sectionItem);
int pos = mSectionedFilteredApps.indexOf(section.firstAppItem);
if (AppsContainerView.GRID_MERGE_SECTION_HEADERS) {
// Update the section names for the two sections
int pos = mSectionedFilteredApps.indexOf(section.firstAppItem);
for (int j = pos; j < (pos + section.numAppsInSection + nextSection.numAppsInSection); j++) {
AdapterItem item = mSectionedFilteredApps.get(j);
item.sectionName = mergedSectionName;
item.sectionInfo = section;
}
}
// Update the following adapter items of the removed section
int pos = mSectionedFilteredApps.indexOf(nextSection.firstAppItem);
// Point the section for these new apps to the merged section
for (int j = pos + section.numAppsInSection; j < (pos + section.numAppsInSection + nextSection.numAppsInSection); j++) {
AdapterItem item = mSectionedFilteredApps.get(j);
item.sectionInfo = section;
item.sectionAppIndex += section.numAppsInSection;
}
// Update the following adapter items of the removed section item
pos = mSectionedFilteredApps.indexOf(nextSection.firstAppItem);
for (int j = pos; j < mSectionedFilteredApps.size(); j++) {
AdapterItem item = mSectionedFilteredApps.get(j);
item.position--;
@@ -0,0 +1,65 @@
/*
* 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.util.AttributeSet;
import android.view.KeyEvent;
import android.widget.EditText;
/**
* The edit text for the search container
*/
public class AppsContainerSearchEditTextView extends EditText {
/**
* Implemented by listeners of the back key.
*/
public interface OnBackKeyListener {
public void onBackKey();
}
private OnBackKeyListener mBackKeyListener;
public AppsContainerSearchEditTextView(Context context) {
this(context, null);
}
public AppsContainerSearchEditTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AppsContainerSearchEditTextView(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);
}
}
@@ -51,9 +51,11 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
private static final boolean ALLOW_SINGLE_APP_LAUNCH = true;
private static final boolean DYNAMIC_HEADER_ELEVATION = false;
private static final boolean DISMISS_SEARCH_ON_BACK = true;
private static final float HEADER_ELEVATION_DP = 4;
private static final int FADE_IN_DURATION = 175;
private static final int FADE_OUT_DURATION = 125;
private static final int FADE_OUT_DURATION = 100;
private static final int SEARCH_TRANSLATION_X_DP = 18;
@Thunk Launcher mLauncher;
@Thunk AlphabeticalAppsList mApps;
@@ -67,7 +69,7 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
private View mSearchBarContainerView;
private View mSearchButtonView;
private View mDismissSearchButtonView;
private EditText mSearchBarEditView;
private AppsContainerSearchEditTextView mSearchBarEditView;
private int mNumAppsPerRow;
private Point mLastTouchDownPos = new Point(-1, -1);
@@ -192,10 +194,19 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
mSearchBarContainerView = findViewById(R.id.app_search_container);
mDismissSearchButtonView = mSearchBarContainerView.findViewById(R.id.dismiss_search_button);
mDismissSearchButtonView.setOnClickListener(this);
mSearchBarEditView = (EditText) findViewById(R.id.app_search_box);
mSearchBarEditView = (AppsContainerSearchEditTextView) findViewById(R.id.app_search_box);
if (mSearchBarEditView != null) {
mSearchBarEditView.addTextChangedListener(this);
mSearchBarEditView.setOnEditorActionListener(this);
if (DISMISS_SEARCH_ON_BACK) {
mSearchBarEditView.setOnBackKeyListener(
new AppsContainerSearchEditTextView.OnBackKeyListener() {
@Override
public void onBackKey() {
hideSearchField(true, true);
}
});
}
}
mAppsRecyclerView = (AppsContainerRecyclerView) findViewById(R.id.apps_list_view);
mAppsRecyclerView.setApps(mApps);
@@ -563,9 +574,16 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
*/
private void showSearchField() {
// Show the search bar and focus the search
final int translationX = DynamicGrid.pxFromDp(SEARCH_TRANSLATION_X_DP,
getContext().getResources().getDisplayMetrics());
mSearchBarContainerView.setVisibility(View.VISIBLE);
mSearchBarContainerView.setAlpha(0f);
mSearchBarContainerView.animate().alpha(1f).setDuration(FADE_IN_DURATION).withLayer()
mSearchBarContainerView.setTranslationX(translationX);
mSearchBarContainerView.animate()
.alpha(1f)
.translationX(0)
.setDuration(FADE_IN_DURATION)
.withLayer()
.withEndAction(new Runnable() {
@Override
public void run() {
@@ -574,38 +592,57 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
InputMethodManager.SHOW_IMPLICIT);
}
});
mSearchButtonView.animate().alpha(0f).setDuration(FADE_OUT_DURATION).withLayer();
mSearchButtonView.animate()
.alpha(0f)
.translationX(-translationX)
.setDuration(FADE_OUT_DURATION)
.withLayer();
}
/**
* Hides the search field.
*/
private void hideSearchField(boolean animated, final boolean returnFocusToRecyclerView) {
final boolean resetTextField = mSearchBarEditView.getText().toString().length() > 0;
final int translationX = DynamicGrid.pxFromDp(SEARCH_TRANSLATION_X_DP,
getContext().getResources().getDisplayMetrics());
if (animated) {
// Hide the search bar and focus the recycler view
mSearchBarContainerView.animate().alpha(0f).setDuration(FADE_IN_DURATION).withLayer()
mSearchBarContainerView.animate()
.alpha(0f)
.translationX(0)
.setDuration(FADE_IN_DURATION)
.withLayer()
.withEndAction(new Runnable() {
@Override
public void run() {
mSearchBarContainerView.setVisibility(View.INVISIBLE);
mSearchBarEditView.setText("");
if (resetTextField) {
mSearchBarEditView.setText("");
}
mApps.setFilter(null);
if (returnFocusToRecyclerView) {
mAppsRecyclerView.requestFocus();
}
scrollToTop();
}
});
mSearchButtonView.animate().alpha(1f).setDuration(FADE_OUT_DURATION).withLayer();
mSearchButtonView.setTranslationX(-translationX);
mSearchButtonView.animate()
.alpha(1f)
.translationX(0)
.setDuration(FADE_OUT_DURATION)
.withLayer();
} else {
mSearchBarContainerView.setVisibility(View.INVISIBLE);
mSearchBarEditView.setText("");
if (resetTextField) {
mSearchBarEditView.setText("");
}
mApps.setFilter(null);
mSearchButtonView.setAlpha(1f);
mSearchButtonView.setTranslationX(0f);
if (returnFocusToRecyclerView) {
mAppsRecyclerView.requestFocus();
}
scrollToTop();
}
getInputMethodManager().hideSoftInputFromWindow(getWindowToken(), 0);
}
+99 -62
View File
@@ -5,10 +5,10 @@ import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -84,8 +84,10 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
private static final boolean FADE_OUT_SECTIONS = false;
private HashMap<String, Point> mCachedSectionBounds = new HashMap<>();
private HashMap<String, PointF> mCachedSectionBounds = new HashMap<>();
private Rect mTmpBounds = new Rect();
private String[] mTmpSections = new String[2];
private PointF[] mTmpSectionBounds = new PointF[2];
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
@@ -94,75 +96,83 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
}
List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
String lastSectionName = null;
int appIndexInSection = 0;
int childCount = parent.getChildCount();
int lastSectionTop = 0;
int lastSectionHeight = 0;
for (int i = 0; i < parent.getChildCount(); i++) {
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
ViewHolder holder = (ViewHolder) parent.getChildViewHolder(child);
if (shouldDrawItemSection(holder, child, i, items)) {
int cellTopOffset = (2 * child.getPaddingTop());
// 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);
if (!item.sectionName.equals(lastSectionName)) {
lastSectionName = item.sectionName;
AlphabeticalAppsList.SectionInfo sectionInfo = item.sectionInfo;
// Draw all the sections for this index
String lastSectionName = item.sectionName;
for (int j = item.sectionAppIndex; j < sectionInfo.numAppsInSection;j++, pos++) {
AlphabeticalAppsList.AdapterItem nextItem = items.get(pos);
if (nextItem.sectionInfo != sectionInfo) {
break;
}
if (j > item.sectionAppIndex && nextItem.sectionName.equals(lastSectionName)) {
continue;
}
// Find the section code points
String sectionBegin = null;
String sectionEnd = null;
int charOffset = 0;
while (charOffset < item.sectionName.length()) {
int codePoint = item.sectionName.codePointAt(charOffset);
int codePointSize = Character.charCount(codePoint);
if (charOffset == 0) {
// The first code point
sectionBegin = item.sectionName.substring(charOffset, charOffset + codePointSize);
} else if ((charOffset + codePointSize) >= item.sectionName.length()) {
// The last code point
sectionEnd = item.sectionName.substring(charOffset, charOffset + codePointSize);
}
charOffset += codePointSize;
getSectionLetters(nextItem.sectionName, mTmpSections, mTmpSectionBounds);
String sectionBegin = mTmpSections[0];
String sectionEnd = mTmpSections[1];
PointF sectionBeginBounds = mTmpSectionBounds[0];
PointF sectionEndBounds = mTmpSectionBounds[1];
// Calculate where to draw the section
int sectionBaseline = (int) (viewTopOffset + sectionBeginBounds.y);
int x = mIsRtl ? parent.getWidth() - mPaddingStart - mStartMargin :
mPaddingStart;
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));
boolean fixedToRow = !items.get(nextRowPos).sectionName.equals(nextItem.sectionName);
if (!fixedToRow) {
y = Math.max(sectionBaseline, y);
}
Point sectionBeginBounds = getAndCacheSectionBounds(sectionBegin);
int minTop = cellTopOffset + sectionBeginBounds.y;
int top = child.getTop() + cellTopOffset + sectionBeginBounds.y;
int left = mIsRtl ? parent.getWidth() - mPaddingStart - mStartMargin :
mPaddingStart;
int col = appIndexInSection % mAppsPerRow;
int nextRowPos = Math.min(pos - col + mAppsPerRow, items.size() - 1);
int alpha = 255;
boolean fixedToRow = !items.get(nextRowPos).sectionName.equals(item.sectionName);
if (fixedToRow) {
alpha = Math.min(255, (int) (255 * (Math.max(0, top) / (float) minTop)));
} else {
// If we aren't fixed to the current row, then bound into the viewport
top = Math.max(minTop, top);
}
if (lastSectionHeight > 0 && top <= (lastSectionTop + lastSectionHeight)) {
top += lastSectionTop - top + lastSectionHeight;
// 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);
}
if (sectionEnd != null) {
Point sectionEndBounds = getAndCacheSectionBounds(sectionEnd);
// If there is a range, draw the range
c.drawText(sectionBegin + "/" + sectionEnd,
left + (mStartMargin - sectionBeginBounds.x - sectionEndBounds.x) / 2, top,
x + (mStartMargin - sectionBeginBounds.x - sectionEndBounds.x) / 2, y,
mSectionTextPaint);
} else {
c.drawText(sectionBegin, left + (mStartMargin - sectionBeginBounds.x) / 2, top,
c.drawText(sectionBegin, (int) (x + (mStartMargin / 2f) - (sectionBeginBounds.x / 2f)), y,
mSectionTextPaint);
}
lastSectionTop = top;
lastSectionHeight = sectionBeginBounds.y + mSectionHeaderOffset;
lastSectionTop = y;
lastSectionHeight = (int) (sectionBeginBounds.y + mSectionHeaderOffset);
lastSectionName = nextItem.sectionName;
}
}
if (holder.mIsSectionHeader) {
appIndexInSection = 0;
} else {
appIndexInSection++;
i += (sectionInfo.numAppsInSection - item.sectionAppIndex);
}
}
}
@@ -173,16 +183,50 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
// Do nothing
}
private Point getAndCacheSectionBounds(String sectionName) {
Point bounds = mCachedSectionBounds.get(sectionName);
/**
* Given a section name, return the first and last section letters.
*/
private void getSectionLetters(String sectionName, String[] lettersOut, PointF[] boundsOut) {
lettersOut[0] = lettersOut[1] = null;
boundsOut[0] = boundsOut[1] = null;
if (AppsContainerView.GRID_MERGE_SECTION_HEADERS) {
int charOffset = 0;
while (charOffset < sectionName.length()) {
int codePoint = sectionName.codePointAt(charOffset);
int codePointSize = Character.charCount(codePoint);
if (charOffset == 0) {
// The first code point
lettersOut[0] = sectionName.substring(charOffset, charOffset + codePointSize);
boundsOut[0] = getAndCacheSectionBounds(lettersOut[0]);
} else if ((charOffset + codePointSize) >= sectionName.length()) {
// The last code point
lettersOut[1] = sectionName.substring(charOffset, charOffset + codePointSize);
boundsOut[0] = getAndCacheSectionBounds(lettersOut[1]);
}
charOffset += codePointSize;
}
} else {
lettersOut[0] = sectionName;
boundsOut[0] = getAndCacheSectionBounds(lettersOut[0]);
}
}
/**
* Given a section name, return the first and last section letters.
*/
private PointF getAndCacheSectionBounds(String sectionName) {
PointF bounds = mCachedSectionBounds.get(sectionName);
if (bounds == null) {
mSectionTextPaint.getTextBounds(sectionName, 0, sectionName.length(), mTmpBounds);
bounds = new Point(mTmpBounds.width(), mTmpBounds.height());
bounds = new PointF(mSectionTextPaint.measureText(sectionName), mTmpBounds.height());
mCachedSectionBounds.put(sectionName, bounds);
}
return bounds;
}
/**
* Returns whether to draw the section for the given child.
*/
private boolean shouldDrawItemSection(ViewHolder holder, View child, int childIndex,
List<AlphabeticalAppsList.AdapterItem> items) {
// Ensure item is not already removed
@@ -201,19 +245,12 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
}
// Ensure we have a holder position
int pos = holder.getPosition();
if (pos < 0 || pos >= items.size()) {
if (pos <= 0 || pos >= items.size()) {
return false;
}
// Ensure this is not a section header
if (items.get(pos).isSectionHeader) {
return false;
}
// Only draw the header for the first item in a section, or whenever the sub-sections
// changes (if AppsContainerView.GRID_MERGE_SECTIONS is true, but
// AppsContainerView.GRID_MERGE_SECTION_HEADERS is false)
// Draw the section header for the first item in each section
return (childIndex == 0) ||
items.get(pos - 1).isSectionHeader && !items.get(pos).isSectionHeader ||
(!items.get(pos - 1).sectionName.equals(items.get(pos).sectionName));
(items.get(pos - 1).isSectionHeader && !items.get(pos).isSectionHeader);
}
}