From e002b691b3233629598c77ef50f1ef91eb1877f0 Mon Sep 17 00:00:00 2001 From: Linus Lee Date: Tue, 24 Mar 2015 17:15:10 -0700 Subject: App drawer: add animations and letters to make things pretty Change-Id: Iaf6718893e09df715b4524b864be9a7c22addc59 --- res/drawable-hdpi/letter_indicator.png | Bin 0 -> 21583 bytes res/drawable-mdpi/letter_indicator.png | Bin 0 -> 4095 bytes res/drawable-nodpi/letter_indicator.9.png | Bin 3614 -> 0 bytes res/drawable-xhdpi/letter_indicator.png | Bin 0 -> 25268 bytes res/drawable-xxhdpi/letter_indicator.png | Bin 0 -> 34597 bytes res/drawable/empty_seek_bar.xml | 19 -- res/layout/app_drawer_container.xml | 12 +- res/layout/app_drawer_item.xml | 58 +++-- res/layout/scrub_layout.xml | 46 ++-- res/values/colors.xml | 3 + res/values/dimens.xml | 2 + .../android/launcher3/AppDrawerListAdapter.java | 215 +++++++++++++++++- src/com/android/launcher3/AppDrawerScrubber.java | 239 ++++++++++++++++---- .../launcher3/AppDrawerScrubberSections.java | 244 ++++++++++++++++++++ src/com/android/launcher3/AutoExpandTextView.java | 246 +++++++++++++++++++++ src/com/android/launcher3/Launcher.java | 47 +++- 16 files changed, 1010 insertions(+), 121 deletions(-) create mode 100644 res/drawable-hdpi/letter_indicator.png create mode 100644 res/drawable-mdpi/letter_indicator.png delete mode 100644 res/drawable-nodpi/letter_indicator.9.png create mode 100644 res/drawable-xhdpi/letter_indicator.png create mode 100644 res/drawable-xxhdpi/letter_indicator.png delete mode 100644 res/drawable/empty_seek_bar.xml create mode 100644 src/com/android/launcher3/AppDrawerScrubberSections.java create mode 100644 src/com/android/launcher3/AutoExpandTextView.java diff --git a/res/drawable-hdpi/letter_indicator.png b/res/drawable-hdpi/letter_indicator.png new file mode 100644 index 000000000..4770d819d Binary files /dev/null and b/res/drawable-hdpi/letter_indicator.png differ diff --git a/res/drawable-mdpi/letter_indicator.png b/res/drawable-mdpi/letter_indicator.png new file mode 100644 index 000000000..2ecfe7c34 Binary files /dev/null and b/res/drawable-mdpi/letter_indicator.png differ diff --git a/res/drawable-nodpi/letter_indicator.9.png b/res/drawable-nodpi/letter_indicator.9.png deleted file mode 100644 index af3578ece..000000000 Binary files a/res/drawable-nodpi/letter_indicator.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/letter_indicator.png b/res/drawable-xhdpi/letter_indicator.png new file mode 100644 index 000000000..6f2186017 Binary files /dev/null and b/res/drawable-xhdpi/letter_indicator.png differ diff --git a/res/drawable-xxhdpi/letter_indicator.png b/res/drawable-xxhdpi/letter_indicator.png new file mode 100644 index 000000000..acbacb067 Binary files /dev/null and b/res/drawable-xxhdpi/letter_indicator.png differ diff --git a/res/drawable/empty_seek_bar.xml b/res/drawable/empty_seek_bar.xml deleted file mode 100644 index e3cf61d64..000000000 --- a/res/drawable/empty_seek_bar.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/layout/app_drawer_container.xml b/res/layout/app_drawer_container.xml index f5db08ce1..6024ddd19 100644 --- a/res/layout/app_drawer_container.xml +++ b/res/layout/app_drawer_container.xml @@ -71,12 +71,14 @@ diff --git a/res/layout/app_drawer_item.xml b/res/layout/app_drawer_item.xml index 08043eb1f..43e2562ae 100644 --- a/res/layout/app_drawer_item.xml +++ b/res/layout/app_drawer_item.xml @@ -14,34 +14,44 @@ See the License for the specific language governing permissions and limitations under the License. --> - + - - - - + android:orientation="horizontal" /> + + + + + + diff --git a/res/layout/scrub_layout.xml b/res/layout/scrub_layout.xml index 90a9d094d..11ee381d0 100644 --- a/res/layout/scrub_layout.xml +++ b/res/layout/scrub_layout.xml @@ -14,31 +14,31 @@ See the License for the specific language governing permissions and limitations under the License. --> - + android:layout_height="@dimen/scrubber_height" + android:paddingLeft="@dimen/app_drawer_scrubber_padding" + android:paddingRight="@dimen/app_drawer_scrubber_padding" + android:layout_alignParentBottom="true" + android:background="@drawable/seek_back"> - - - + - + - + diff --git a/res/values/colors.xml b/res/values/colors.xml index 3cf7d39a6..082489d53 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -51,6 +51,9 @@ #FF000000 #76000000 + #bf14191e + @android:color/white + @android:color/darker_gray #CC14191E diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 81e40bfb2..5394ea761 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -124,4 +124,6 @@ 5dp + + 20dp diff --git a/src/com/android/launcher3/AppDrawerListAdapter.java b/src/com/android/launcher3/AppDrawerListAdapter.java index a16937308..ea3243534 100644 --- a/src/com/android/launcher3/AppDrawerListAdapter.java +++ b/src/com/android/launcher3/AppDrawerListAdapter.java @@ -16,25 +16,29 @@ package com.android.launcher3; +import android.animation.ValueAnimator; import android.content.ComponentName; import android.content.Context; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.provider.Settings; import android.text.TextUtils; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.support.v7.widget.RecyclerView; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; import android.widget.LinearLayout; import android.widget.SectionIndexer; import com.android.launcher3.locale.LocaleSetManager; import com.android.launcher3.locale.LocaleUtils; import com.android.launcher3.settings.SettingsProvider; -import java.text.Collator; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -61,6 +65,8 @@ public class AppDrawerListAdapter extends RecyclerView.Adapter mViewHolderSet; + private final Interpolator mInterpolator; + private final View.OnLayoutChangeListener mLayoutChangeListener; + + private boolean mDragging; + private boolean mExpanding; + private boolean mPendingShrink; + private long mStartTime; + private int mScrollState; + private float mFastScrollSpeed; + private float mLastScrollSpeed; + + public ItemAnimatorSet(Context ctx) { + mDragging = false; + mExpanding = false; + mPendingShrink = false; + mScrollState = RecyclerView.SCROLL_STATE_IDLE; + mViewHolderSet = new HashSet<>(); + mInterpolator = new DecelerateInterpolator(); + YDPI = ctx.getResources().getDisplayMetrics().ydpi; + mLayoutChangeListener = new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + // set the pivot of the text view + v.setPivotX(0); + v.setPivotY(v.getMeasuredHeight() / 2); + } + }; + } + + public void add(ViewHolder holder) { + mViewHolderSet.add(holder); + holder.mTextView.addOnLayoutChangeListener(mLayoutChangeListener); + + createAnimationHook(holder); + } + + public void remove(ViewHolder holder) { + mViewHolderSet.remove(holder); + holder.mTextView.removeOnLayoutChangeListener(mLayoutChangeListener); + } + + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + if (newState != mScrollState) { + mScrollState = newState; + mFastScrollSpeed = 0; + checkAnimationState(); + } + } + + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + if (mScrollState == RecyclerView.SCROLL_STATE_SETTLING) { + mLastScrollSpeed = Math.abs(dy / YDPI); + // get the max of the current scroll speed and the previous fastest scroll speed + mFastScrollSpeed = Math.max(mFastScrollSpeed, mLastScrollSpeed); + checkAnimationState(); + } + } + + public void setDragging(boolean dragging) { + mDragging = dragging; + checkAnimationState(); + } + + private void checkAnimationState() { + // if the user is dragging or if we're settling at a fast speed, then show animation + showAnimation(mDragging || + (mScrollState == RecyclerView.SCROLL_STATE_SETTLING && + mFastScrollSpeed >= FAST_SCROLL)); + } + + private void showAnimation(boolean expanding) { + if (mExpanding != expanding) { + // if near the top or bottom and flick to that side of the list, the scroll speed + // will hit 0 and the animation will cut straight to shrinking. This code + // is here to allow the expand animation to complete in that specific scenario + // before shrinking + // if the user isn't dragging, the scroll state is idle, the last scroll is fast and + // the expand animation is still playing, then mark pending shrink as true + if (!mDragging + && mScrollState == RecyclerView.SCROLL_STATE_IDLE + && mLastScrollSpeed > FAST_SCROLL + && System.currentTimeMillis() - mStartTime < ANIMATION_DURATION) { + mPendingShrink = true; + return; + } + + mExpanding = expanding; + mPendingShrink = false; + mStartTime = System.currentTimeMillis(); + + for (ViewHolder holder : mViewHolderSet) { + createAnimationHook(holder); + } + } + } + + public void createAnimationHook(ViewHolder holder) { + holder.mTextView.animate().cancel(); + holder.mTextView.animate() + .setUpdateListener(new ItemAnimator(holder, mItemAnimatorSet)) + .setDuration(ANIMATION_DURATION) + .start(); + } + + public void animate(ViewHolder holder, ValueAnimator animation) { + long diffTime = System.currentTimeMillis() - mStartTime; + + float percentage = Math.min(diffTime / (float) ANIMATION_DURATION, 1f); + percentage = mInterpolator.getInterpolation(percentage); + + if (!mExpanding) { + percentage = 1 - percentage; + } + + final float targetScale = (MAX_SCALE - MIN_SCALE) * percentage + MIN_SCALE; + holder.mTextView.setScaleX(targetScale); + holder.mTextView.setScaleY(targetScale); + + holder.mFadingBackground.setAlpha(percentage); + + if (diffTime >= ANIMATION_DURATION) { + animation.cancel(); + + if (mPendingShrink) { + mPendingShrink = false; + mLastScrollSpeed = 0; + checkAnimationState(); + } + + } + } + } + + private static class ItemAnimator implements ValueAnimator.AnimatorUpdateListener { + private ViewHolder mViewHolder; + private ItemAnimatorSet mAnimatorSet; + + public ItemAnimator(final ViewHolder holder, final ItemAnimatorSet animatorSet) { + mViewHolder = holder; + mAnimatorSet = animatorSet; + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mAnimatorSet.animate(mViewHolder, animation); } } @@ -101,11 +270,25 @@ public class AppDrawerListAdapter extends RecyclerView.Adapter mSections; + private String[] mHeaders; + + public SectionContainer(String[] headers) { + mSections = AppDrawerScrubberSections.createSections(headers); + mHeaders = headers; + } + + public int size() { + return showLetters() ? mSections.size() : mHeaders.length; + } + + public String getHeader(int idx) { + return showLetters() ? mSections.get(idx).getText() : mHeaders[idx]; + } + + /** + * Because the list section headers is not necessarily the same size as the scrubber + * letters, we need to map from the larger list to the smaller list. + * In the case that curIdx is not highlighted, it will use the directional index to + * determine the adapter index + * @return the mHeaders index (aka the underlying adapter index). + */ + public int getAdapterIndex(int prevIdx, int curIdx) { + if (!showLetters()) { + return curIdx; + } + + // because we have some unhighlighted letters, we need to first get the directional + // index before getting the adapter index + return mSections.get(getDirectionalIndex(prevIdx, curIdx)).getAdapterIndex(); + } + + /** + * Given the direction the user is scrolling in, return the closest index which is a + * highlighted index + */ + public int getDirectionalIndex(int prevIdx, int curIdx) { + if (!showLetters() || mSections.get(curIdx).getHighlight()) { + return curIdx; + } + + if (prevIdx < curIdx) { + return mSections.get(curIdx).getNextIndex(); + } else { + return mSections.get(curIdx).getPreviousIndex(); + } + } + + /** + * @return true if the scrubber is showing characters as opposed to a line + */ + public boolean showLetters() { + return mSections != null; + } + + /** + * Initializes the scrubber text with the proper characters + */ + public void initializeScrubberText(AutoExpandTextView scrubberText) { + scrubberText.setSections(AppDrawerScrubberSections.getHighlightText(mSections)); + } + } + public void updateSections() { - mSections = (String[]) mAdapter.getSections(); - mSeekBar.setMax(mSections.length - 1); + mSectionContainer = new SectionContainer((String[]) mAdapter.getSections()); + mSectionContainer.initializeScrubberText(mScrubberText); + mSeekBar.setMax(mSectionContainer.size() - 1); + + // show a white line if there are no letters, otherwise show transparent + Drawable d = mSectionContainer.showLetters() ? new ColorDrawable(Color.TRANSPARENT) + : getContext().getResources().getDrawable(R.drawable.seek_back); + ((ViewGroup)mSeekBar.getParent()).setBackground(d); + } public void setSource(RecyclerView listView) { @@ -68,63 +148,128 @@ public class AppDrawerScrubber extends LinearLayout { private boolean isReady() { return mListView != null && mAdapter != null && - mSections != null; + mSectionContainer != null; } private void init(Context context) { LayoutInflater.from(context).inflate(R.layout.scrub_layout, this); + mScrubberAnimationState = new ScrubberAnimationState(); mSeekBar = (SeekBar) findViewById(R.id.scrubber); + mScrubberText = (AutoExpandTextView) findViewById(R.id.scrubberText); + mSeekBar.setOnSeekBarChangeListener(mScrubberAnimationState); + } - mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, final int progress, boolean fromUser) { - if (!isReady()) { - return; - } - resetScrubber(); + /** + * Handles the animations of the scrubber indicator + */ + private class ScrubberAnimationState implements SeekBar.OnSeekBarChangeListener { + private static final long SCRUBBER_DISPLAY_DURATION = 150; + private static final float SCRUBBER_SCALE_START = 0f; + private static final float SCRUBBER_SCALE_END = 1f; + private static final float SCRUBBER_ALPHA_START = 0f; + private static final float SCRUBBER_ALPHA_END = 1f; + + private boolean mTouchingTrack = false; + private boolean mAnimatingIn = false; + private int mLastIndex = -1; - String section = String.valueOf(mSections[progress]); + private void touchTrack(boolean touching) { + mTouchingTrack = touching; - if (mScrubberIndicator != null) { - float translateX = (progress * seekBar.getWidth()) / mSections.length; - translateX -= (mScrubberIndicator.getWidth() / 6); // offset for alignment - mScrubberIndicator.setTranslationX(translateX); - mScrubberIndicator.setText(section); + if (mScrubberIndicator != null) { + if (mTouchingTrack) { + animateIn(); + } else if (!mAnimatingIn) { // finish animating in before animating out + animateOut(); } - mLayoutManager.smoothScrollToPosition(mListView, null, - mAdapter.getPositionForSection(progress)); + mAdapter.setDragging(mTouchingTrack); } + } - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - resetScrubber(); - if (mScrubberIndicator != null) { - mScrubberIndicator.setAlpha(1f); - mScrubberIndicator.setVisibility(View.VISIBLE); - } - } + private void animateIn() { + // start from a scratch position when animating in + mScrubberIndicator.animate().cancel(); + mScrubberIndicator.setPivotX(mScrubberIndicator.getMeasuredWidth() / 2); + mScrubberIndicator.setPivotY(mScrubberIndicator.getMeasuredHeight() * 0.8f); + mScrubberIndicator.setAlpha(SCRUBBER_ALPHA_START); + mScrubberIndicator.setScaleX(SCRUBBER_SCALE_START); + mScrubberIndicator.setScaleY(SCRUBBER_SCALE_START); + mScrubberIndicator.setVisibility(View.VISIBLE); + mAnimatingIn = true; - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - resetScrubber(); - if (mScrubberIndicator != null) { - mScrubberIndicator.animate().alpha(0f).translationYBy(20f) - .setDuration(200).setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mScrubberIndicator.setVisibility(View.INVISIBLE); + mScrubberIndicator.animate() + .alpha(SCRUBBER_ALPHA_END) + .scaleX(SCRUBBER_SCALE_END) + .scaleY(SCRUBBER_SCALE_END) + .setDuration(SCRUBBER_DISPLAY_DURATION) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mAnimatingIn = false; + // if the user has stopped touching the seekbar, animate back out + if (!mTouchingTrack) { + animateOut(); } - }); - } + } + }) + .start(); + } + + private void animateOut() { + mScrubberIndicator.animate() + .alpha(SCRUBBER_ALPHA_START) + .scaleX(SCRUBBER_SCALE_START) + .scaleY(SCRUBBER_SCALE_START) + .setDuration(SCRUBBER_DISPLAY_DURATION) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mScrubberIndicator.setVisibility(View.INVISIBLE); + } + }); + } + + @Override + public void onProgressChanged(SeekBar seekBar, int index, boolean fromUser) { + if (!isReady()) { + return; } - private void resetScrubber() { - if (mScrubberIndicator != null) { - mScrubberIndicator.animate().cancel(); - mScrubberIndicator.setTranslationY(0f); + if (mScrubberIndicator != null) { + // get the index based on the direction the user is scrolling + int directionalIndex = mSectionContainer.getDirectionalIndex(mLastIndex, index); + String sectionText = mSectionContainer.getHeader(directionalIndex); + + float translateX = (index * seekBar.getWidth()) / (float)mSectionContainer.size(); + // if we are showing letters, grab the position based on the text view + if (mSectionContainer.showLetters()) { + translateX = mScrubberText.getPositionOfSection(index); } + + // center the x position + translateX -= mScrubberIndicator.getMeasuredWidth() / 2; + + mScrubberIndicator.setTranslationX(translateX); + mScrubberIndicator.setText(sectionText); } - }); + + // get the index of the underlying list + int adapterIndex = mSectionContainer.getAdapterIndex(mLastIndex, index); + mLayoutManager.smoothScrollToPosition(mListView, null, + mAdapter.getPositionForSection(adapterIndex)); + + mLastIndex = index; + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + touchTrack(true); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + touchTrack(false); + } } } \ No newline at end of file diff --git a/src/com/android/launcher3/AppDrawerScrubberSections.java b/src/com/android/launcher3/AppDrawerScrubberSections.java new file mode 100644 index 000000000..2d4fcaaaa --- /dev/null +++ b/src/com/android/launcher3/AppDrawerScrubberSections.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2015 The CyanogenMod 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.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; + +public class AppDrawerScrubberSections { + private static final String TAG = AppDrawerScrubber.class.getSimpleName(); + private static final String ALPHA_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final int MAX_NUMBER_CUSTOM_HEADERS = 8; + private static final int MAX_HEADERS = ALPHA_LETTERS.length() + MAX_NUMBER_CUSTOM_HEADERS; + public static final int INVALID_INDEX = -1; + + private AutoExpandTextView.HighlightedText mHighlightedText; + private int mPreviousValidIndex; + private int mNextValidIndex; + private int mAdapterIndex; + + public AppDrawerScrubberSections(String text, boolean highlight, int idx) { + mHighlightedText = new AutoExpandTextView.HighlightedText(text, highlight); + mAdapterIndex = idx; + mPreviousValidIndex = mNextValidIndex = idx; + } + + public boolean getHighlight() { + return mHighlightedText.mHighlight; + } + + public String getText() { + return mHighlightedText.mText; + } + + public int getPreviousIndex() { + return mPreviousValidIndex; + } + + public int getNextIndex() { + return mNextValidIndex; + } + + public int getAdapterIndex() { + return mAdapterIndex; + } + + private static int getFirstValidIndex(ArrayList sections) { + for (int i = 0; i < sections.size(); i++) { + if (sections.get(i).getHighlight()) { + return i; + } + } + + return INVALID_INDEX; + } + + private static void createIndices(ArrayList sections) { + if (sections == null || sections.size() == 0) { + return; + } + + // walk forwards and fill out the previous valid index based on the previous highlight + int currentIdx = getFirstValidIndex(sections); + for (int i = 0; i < sections.size(); i++) { + if (sections.get(i).getHighlight()) { + currentIdx = i; + } + + sections.get(i).mPreviousValidIndex = currentIdx; + } + + // currentIdx should be now on the last valid index so walk back and fill the other way + for (int i = sections.size() - 1; i >= 0; i--) { + if (sections.get(i).getHighlight()) { + currentIdx = i; + } + + sections.get(i).mNextValidIndex = currentIdx; + } + } + + public static ArrayList getHighlightText( + ArrayList sections) { + if (sections == null) { + return null; + } + + ArrayList highlights = new ArrayList<>(sections.size()); + for (AppDrawerScrubberSections section : sections) { + highlights.add(section.mHighlightedText); + } + + return highlights; + } + + private static void addAlphaLetters(ArrayList sections, + HashMap foundAlphaLetters) { + for (int i = 0; i < ALPHA_LETTERS.length(); i++) { + boolean highlighted = foundAlphaLetters.containsKey(i); + int index = highlighted + ? foundAlphaLetters.get(i) : AppDrawerScrubberSections.INVALID_INDEX; + + sections.add(new AppDrawerScrubberSections(ALPHA_LETTERS.substring(i, i + 1), + highlighted, index)); + } + } + + /** + * Takes the headers and runs some checks to see if we can create a valid + * appDrawerScrubberSection out of it. This list will contain the original header list plus + * fill out the remaining sections based on the ALPHA_LETTERS. It will then determine which + * ones to highlight as well as what letters to highlight when scrolling over the + * grayed out sections + * @param headers list of header Strings + * @return the list of scrubber sections + */ + public static ArrayList createSections(String[] headers) { + // check if we have a valid header section + if (!validHeaderList(headers)) { + return null; + } + + // this will track the mapping of ALPHA_LETTERS index to the headers index + HashMap foundAlphaLetters = new HashMap<>(); + ArrayList sections = new ArrayList<>(headers.length); + boolean inAlphaLetterSection = false; + + for (int i = 0; i < headers.length; i++) { + int alphaLetterIndex = TextUtils.isEmpty(headers[i]) + ? -1 : ALPHA_LETTERS.indexOf(headers[i]); + + // if we found an ALPHA_LETTERS store that in foundAlphaLetters and continue + if (alphaLetterIndex >= 0) { + foundAlphaLetters.put(alphaLetterIndex, i); + inAlphaLetterSection = true; + } else { + // if we are exiting the ALPHA_LETTERS section, add it here + if (inAlphaLetterSection) { + addAlphaLetters(sections, foundAlphaLetters); + inAlphaLetterSection = false; + } + + // add the custom header + sections.add(new AppDrawerScrubberSections(headers[i], true, i)); + } + } + + // if the last section are the alpha letters, then add it + if (inAlphaLetterSection) { + addAlphaLetters(sections, foundAlphaLetters); + } + + // create the forward and backwards indices for scrolling over the grayed out sections + AppDrawerScrubberSections.createIndices(sections); + + return sections; + } + + /** + * Walk through the headers and check for a few things: + * 1) No more than MAX_NUMBER_CUSTOM_HEADERS headers exist in the headers list or no more + * than MAX_HEADERS headers exist in the list + * 2) the headers that fall in the ALPHA_LETTERS category are in the same order as ALPHA_LETTERS + * 3) There are no headers that exceed length of 1 + * 4) The alpha letter section is together and not separated by other things + */ + private static boolean validHeaderList(String[] headers) { + int numCustomHeaders = 0; + int previousAlphaIndex = -1; + boolean foundAlphaHeaders = false; + + for (String s : headers) { + if (TextUtils.isEmpty(s)) { + numCustomHeaders++; + continue; + } + + if (s.length() > 1) { + Log.w(TAG, "Found header " + s + " with length: " + s.length()); + return false; + } + + int alphaIndex = ALPHA_LETTERS.indexOf(s); + if (alphaIndex >= 0) { + if (previousAlphaIndex != -1) { + // if the previous alpha index is >= alphaIndex then it is in the wrong order + if (previousAlphaIndex >= alphaIndex) { + Log.w(TAG, "Found letter index " + previousAlphaIndex + + " which is greater than " + alphaIndex); + return false; + } + } + + // if we've found headers previously and the index is -1 that means the alpha + // letters are separated out into two sections so return false + if (foundAlphaHeaders && previousAlphaIndex == -1) { + Log.w(TAG, "Found alpha letters twice"); + return false; + } + + previousAlphaIndex = alphaIndex; + foundAlphaHeaders = true; + } else { + numCustomHeaders++; + previousAlphaIndex = -1; + } + } + + final int listSize = foundAlphaHeaders + ? numCustomHeaders + ALPHA_LETTERS.length() + : numCustomHeaders; + + // if one of these conditions are satisfied, then return true + if (numCustomHeaders <= MAX_NUMBER_CUSTOM_HEADERS || listSize <= MAX_HEADERS) { + return true; + } + + if (numCustomHeaders > MAX_NUMBER_CUSTOM_HEADERS) { + Log.w(TAG, "Found " + numCustomHeaders + "# custom headers when " + + MAX_NUMBER_CUSTOM_HEADERS + " is allowed!"); + } else if (listSize > MAX_HEADERS) { + Log.w(TAG, "Found " + listSize + " headers when " + + MAX_HEADERS + " is allowed!"); + } + + return false; + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/AutoExpandTextView.java b/src/com/android/launcher3/AutoExpandTextView.java new file mode 100644 index 000000000..ea7ac896e --- /dev/null +++ b/src/com/android/launcher3/AutoExpandTextView.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2014 Grantland Chew + * Copyright (C) 2015 The CyanogenMod 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.content.res.TypedArray; +import android.graphics.Color; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.TextPaint; +import android.text.TextUtils; +import android.text.method.TransformationMethod; +import android.text.style.ForegroundColorSpan; +import android.util.AttributeSet; +import android.widget.TextView; + +import java.util.ArrayList; + +/** + * A single-line TextView that resizes it's letter spacing to fit the width of the view + * + * @author Grantland Chew + * @author Linus Lee + */ +public class AutoExpandTextView extends TextView { + // How precise we want to be when reaching the target textWidth size + private static final float PRECISION = 0.01f; + + // Attributes + private float mPrecision; + private TextPaint mPaint; + private float[] mPositions; + + public static class HighlightedText { + public String mText; + public boolean mHighlight; + + public HighlightedText(String text, boolean highlight) { + mText = text; + mHighlight = highlight; + } + } + + public AutoExpandTextView(Context context) { + super(context); + init(context, null, 0); + } + + public AutoExpandTextView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + public AutoExpandTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs, defStyle); + } + + private void init(Context context, AttributeSet attrs, int defStyle) { + float precision = PRECISION; + + if (attrs != null) { + TypedArray ta = context.obtainStyledAttributes( + attrs, + R.styleable.AutofitTextView, + defStyle, + 0); + precision = ta.getFloat(R.styleable.AutofitTextView_precision, precision); + } + + mPaint = new TextPaint(); + setPrecision(precision); + } + + /** + * @return the amount of precision used to calculate the correct text size to fit within it's + * bounds. + */ + public float getPrecision() { + return mPrecision; + } + + /** + * Set the amount of precision used to calculate the correct text size to fit within it's + * bounds. Lower precision is more precise and takes more time. + * + * @param precision The amount of precision. + */ + public void setPrecision(float precision) { + if (precision != mPrecision) { + mPrecision = precision; + refitText(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void setLines(int lines) { + super.setLines(1); + refitText(); + } + + /** + * Only allow max lines of 1 + */ + @Override + public void setMaxLines(int maxLines) { + super.setMaxLines(1); + refitText(); + } + + /** + * Re size the font so the specified text fits in the text box assuming the text box is the + * specified width. + */ + private void refitText() { + CharSequence text = getText(); + + if (TextUtils.isEmpty(text)) { + return; + } + + TransformationMethod method = getTransformationMethod(); + if (method != null) { + text = method.getTransformation(text, this); + } + int targetWidth = getWidth() - getPaddingLeft() - getPaddingRight(); + if (targetWidth > 0) { + float high = 100; + float low = 0; + + mPaint.set(getPaint()); + mPaint.setTextSize(getTextSize()); + float letterSpacing = getLetterSpacing(text, mPaint, targetWidth, low, high, + mPrecision); + mPaint.setLetterSpacing(letterSpacing); + calculateSections(text); + + super.setLetterSpacing(letterSpacing); + } + } + + public float getPositionOfSection(int position) { + if (mPositions == null || position >= mPositions.length) { + return 0; + } + return mPositions[position]; + } + + /** + * This calculates the different horizontal positions of each character + */ + private void calculateSections(CharSequence text) { + mPositions = new float[text.length()]; + for (int i = 0; i < text.length(); i++) { + if (i == 0) { + mPositions[0] = mPaint.measureText(text, 0, 1) / 2; + } else { + // try to be lazy and just add the width of the newly added char + mPositions[i] = mPaint.measureText(text, i, i + 1) + mPositions[i - 1]; + } + } + } + + /** + * Sets the list of sections in the text view. This will take the first character of each + * and space it out in the text view using letter spacing + */ + public void setSections(ArrayList sections) { + mPositions = null; + if (sections == null || sections.size() == 0) { + setText(""); + return; + } + + Resources r = getContext().getResources(); + int highlightColor = r.getColor(R.color.app_scrubber_highlight_color); + int grayColor = r.getColor(R.color.app_scrubber_gray_color); + + SpannableStringBuilder builder = new SpannableStringBuilder(); + for (HighlightedText highlightText : sections) { + SpannableString spannable = new SpannableString(highlightText.mText.substring(0, 1)); + spannable.setSpan( + new ForegroundColorSpan(highlightText.mHighlight ? highlightColor : grayColor), + 0, spannable.length(), 0); + builder.append(spannable); + } + + setText(builder); + } + + private static float getLetterSpacing(CharSequence text, TextPaint paint, float targetWidth, + float low, float high, float precision) { + float mid = (low + high) / 2.0f; + paint.setLetterSpacing(mid); + + float measuredWidth = paint.measureText(text, 0, text.length()); + + if (high - low < precision) { + if (measuredWidth < targetWidth) { + return mid; + } else { + return low; + } + } else if (measuredWidth > targetWidth) { + return getLetterSpacing(text, paint, targetWidth, low, mid, precision); + } else if (measuredWidth < targetWidth) { + return getLetterSpacing(text, paint, targetWidth, mid, high, precision); + } else { + return mid; + } + } + + @Override + protected void onTextChanged(final CharSequence text, final int start, + final int lengthBefore, final int lengthAfter) { + super.onTextChanged(text, start, lengthBefore, lengthAfter); + refitText(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (w != oldw) { + refitText(); + } + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 5d857ce8c..be39e522c 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -19,6 +19,7 @@ package com.android.launcher3; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; +import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.TimeInterpolator; @@ -59,6 +60,7 @@ import android.graphics.Color; import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; @@ -1724,6 +1726,17 @@ public class Launcher extends Activity } mAppDrawer.setHasFixedSize(true); mAppDrawer.setAdapter(mAppDrawerAdapter); + mAppDrawer.setOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + mAppDrawerAdapter.onScrollStateChanged(recyclerView, newState); + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + mAppDrawerAdapter.onScrolled(recyclerView, dx, dy); + } + }); initializeScrubber(); } } @@ -3645,7 +3658,7 @@ public class Launcher extends Activity } boolean material = Utilities.isLmpOrAbove(); - boolean drawer = mDrawerType == AppDrawerListAdapter.DrawerType.Drawer; + final boolean drawer = mDrawerType == AppDrawerListAdapter.DrawerType.Drawer; final Resources res = getResources(); final int duration = res.getInteger(R.integer.config_appsCustomizeZoomInTime); @@ -3827,6 +3840,14 @@ public class Launcher extends Activity } mStateAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + if (drawer && mAppsCustomizeContent.getContentType() + == AppsCustomizePagedView.ContentType.Applications) { + updateStatusBarColor(res.getColor(R.color.app_drawer_drag_background)); + } + } + @Override public void onAnimationEnd(Animator animation) { dispatchOnLauncherTransitionEnd(fromView, animated, false); @@ -3893,6 +3914,11 @@ public class Launcher extends Activity toView.setScaleY(1.0f); toView.setVisibility(View.VISIBLE); toView.bringToFront(); + if (drawer && mAppsCustomizeContent.getContentType() + == AppsCustomizePagedView.ContentType.Applications) { + updateStatusBarColor(res.getColor(R.color.app_drawer_drag_background)); + toView.setBackgroundColor(res.getColor(R.color.app_drawer_background)); + } if (!springLoaded && !LauncherAppState.getInstance().isScreenLarge()) { // Hide the search bar @@ -4012,6 +4038,7 @@ public class Launcher extends Activity content.setPageBackgroundsVisible(false); } else { fromView.setBackgroundColor(Color.TRANSPARENT); + updateStatusBarColor(Color.TRANSPARENT); } final View allAppsButton = getAllAppsButton(); @@ -4201,6 +4228,11 @@ public class Launcher extends Activity fromView.post(startAnimRunnable); } else { fromView.setVisibility(View.GONE); + if (drawer && mAppsCustomizeContent.getContentType() + == AppsCustomizePagedView.ContentType.Applications) { + fromView.setBackgroundColor(Color.TRANSPARENT); + updateStatusBarColor(Color.TRANSPARENT, 0); + } dispatchOnLauncherTransitionPrepare(fromView, animated, true); dispatchOnLauncherTransitionStart(fromView, animated, true); dispatchOnLauncherTransitionEnd(fromView, animated, true); @@ -5591,6 +5623,19 @@ public class Launcher extends Activity if (mSearchDropTargetBar != null) mSearchDropTargetBar.showSearchBar(false); } + private void updateStatusBarColor(int color) { + updateStatusBarColor(color, 300); + } + + private void updateStatusBarColor(int color, int duration) { + final Window window = getWindow(); + ObjectAnimator animator = ObjectAnimator.ofInt(window, + "statusBarColor", window.getStatusBarColor(), color); + animator.setEvaluator(new ArgbEvaluator()); + animator.setDuration(duration); + animator.start(); + } + public ItemInfo createAppDragInfo(Intent appLaunchIntent) { // Called from search suggestion, not supported in other profiles. final UserHandleCompat myUser = UserHandleCompat.myUserHandle(); -- cgit v1.2.3