From de34aa401e08abe10027af208d5d6b339f4c4895 Mon Sep 17 00:00:00 2001 From: Winson Chung Date: Thu, 7 May 2015 18:21:28 -0700 Subject: [PATCH] Updating sticky headers. - The whole section's headers are drawn together, moving as a group until it reaches the individual bounds for each letter in the section - Adding animation to search button -> field transition - Fixing section header text measuring causing sections not to be centered - Forcing the merge to stop if an app has > 3 full rows; on both phone and tablet, merging a large section with anything else seems to be less useful --- res/layout/apps_list_view.xml | 3 +- .../launcher3/AlphabeticalAppsList.java | 43 +++-- .../AppsContainerSearchEditTextView.java | 65 +++++++ .../android/launcher3/AppsContainerView.java | 59 +++++-- .../android/launcher3/AppsGridAdapter.java | 161 +++++++++++------- 5 files changed, 246 insertions(+), 85 deletions(-) create mode 100644 src/com/android/launcher3/AppsContainerSearchEditTextView.java diff --git a/res/layout/apps_list_view.xml b/res/layout/apps_list_view.xml index e29cac5e15..a726cd8fe3 100644 --- a/res/layout/apps_list_view.xml +++ b/res/layout/apps_list_view.xml @@ -43,7 +43,7 @@ android:paddingBottom="12dp" android:contentDescription="@string/all_apps_button_label" android:src="@drawable/ic_arrow_back_grey" /> - mApps = new ArrayList<>(); private List 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--; diff --git a/src/com/android/launcher3/AppsContainerSearchEditTextView.java b/src/com/android/launcher3/AppsContainerSearchEditTextView.java new file mode 100644 index 0000000000..c688237b24 --- /dev/null +++ b/src/com/android/launcher3/AppsContainerSearchEditTextView.java @@ -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); + } +} diff --git a/src/com/android/launcher3/AppsContainerView.java b/src/com/android/launcher3/AppsContainerView.java index 9122427fdd..0aa1e67d11 100644 --- a/src/com/android/launcher3/AppsContainerView.java +++ b/src/com/android/launcher3/AppsContainerView.java @@ -52,9 +52,11 @@ public class AppsContainerView extends FrameLayout implements DragSource, Insett 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; @@ -68,7 +70,7 @@ public class AppsContainerView extends FrameLayout implements DragSource, Insett 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); @@ -199,10 +201,19 @@ public class AppsContainerView extends FrameLayout implements DragSource, Insett 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); @@ -594,9 +605,16 @@ public class AppsContainerView extends FrameLayout implements DragSource, Insett */ 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() { @@ -605,38 +623,57 @@ public class AppsContainerView extends FrameLayout implements DragSource, Insett 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); } diff --git a/src/com/android/launcher3/AppsGridAdapter.java b/src/com/android/launcher3/AppsGridAdapter.java index 62d9129c9f..d83d6c97cb 100644 --- a/src/com/android/launcher3/AppsGridAdapter.java +++ b/src/com/android/launcher3/AppsGridAdapter.java @@ -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 { private static final boolean FADE_OUT_SECTIONS = false; - private HashMap mCachedSectionBounds = new HashMap<>(); + private HashMap 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 { } List 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 { // 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 items) { // Ensure item is not already removed @@ -201,19 +245,12 @@ class AppsGridAdapter extends RecyclerView.Adapter { } // 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); } }