From acab44fc939b4083ab7ec889b0c6d4fe0db00cca Mon Sep 17 00:00:00 2001 From: cretin45 Date: Fri, 13 Nov 2015 16:51:43 -0800 Subject: [PATCH] Reimplement the CM scrubber against the new Launcher PS4: Implement RTL support Change-Id: I4456d54b5924913d1b36e1cfa9a2269150f6fb3e --- res/drawable-hdpi/letter_indicator.png | Bin 0 -> 21583 bytes res/drawable-mdpi/letter_indicator.png | Bin 0 -> 4095 bytes res/drawable-xhdpi/letter_indicator.png | Bin 0 -> 25268 bytes res/drawable-xxhdpi/letter_indicator.png | Bin 0 -> 34597 bytes res/drawable/scrubber_back.xml | 19 + res/drawable/seek_back.xml | 30 ++ res/layout/all_apps_container.xml | 9 + res/layout/scrub_layout.xml | 44 +++ res/layout/scrubber_container.xml | 53 +++ res/layout/widgets_view.xml | 9 + res/values/attrs.xml | 6 + res/values/colors.xml | 8 +- res/values/dimens.xml | 12 +- src/com/android/launcher3/AutoExpandTextView.java | 246 ++++++++++++ src/com/android/launcher3/BaseContainerView.java | 59 ++- src/com/android/launcher3/BaseRecyclerView.java | 66 +++- .../launcher3/BaseRecyclerViewFastScrollBar.java | 3 + .../launcher3/BaseRecyclerViewScrubber.java | 424 +++++++++++++++++++++ .../launcher3/BaseRecyclerViewScrubberSection.java | 267 +++++++++++++ src/com/android/launcher3/BubbleTextView.java | 32 +- src/com/android/launcher3/DeviceProfile.java | 14 +- src/com/android/launcher3/Launcher.java | 7 + .../launcher3/allapps/AllAppsContainerView.java | 95 ++++- .../launcher3/allapps/AllAppsGridAdapter.java | 72 +++- .../launcher3/allapps/AllAppsRecyclerView.java | 183 +++++++-- .../allapps/AllAppsRecyclerViewContainerView.java | 2 - .../launcher3/widget/WidgetsContainerView.java | 6 + .../launcher3/widget/WidgetsRecyclerView.java | 18 +- 28 files changed, 1618 insertions(+), 66 deletions(-) create mode 100644 res/drawable-hdpi/letter_indicator.png create mode 100644 res/drawable-mdpi/letter_indicator.png create mode 100644 res/drawable-xhdpi/letter_indicator.png create mode 100644 res/drawable-xxhdpi/letter_indicator.png create mode 100644 res/drawable/scrubber_back.xml create mode 100644 res/drawable/seek_back.xml create mode 100644 res/layout/scrub_layout.xml create mode 100644 res/layout/scrubber_container.xml create mode 100644 src/com/android/launcher3/AutoExpandTextView.java create mode 100644 src/com/android/launcher3/BaseRecyclerViewScrubber.java create mode 100644 src/com/android/launcher3/BaseRecyclerViewScrubberSection.java diff --git a/res/drawable-hdpi/letter_indicator.png b/res/drawable-hdpi/letter_indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..4770d819dab13d96750259440daf71b12f38e994 GIT binary patch literal 21583 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 literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/letter_indicator.png b/res/drawable-xhdpi/letter_indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..6f218601776df93e182e54544bb30ccf9ccac80d GIT binary patch literal 25268 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 Noa{rHVkv#U{{y2Bjb{J= literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/letter_indicator.png b/res/drawable-xxhdpi/letter_indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..acbacb067dedbfbb87b4807dba4c4c7c89ab722d GIT binary patch literal 34597 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 P2)Jx59Z(JCeuV!66fIX9 literal 0 HcmV?d00001 diff --git a/res/drawable/scrubber_back.xml b/res/drawable/scrubber_back.xml new file mode 100644 index 000000000..c5022dec5 --- /dev/null +++ b/res/drawable/scrubber_back.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/res/drawable/seek_back.xml b/res/drawable/seek_back.xml new file mode 100644 index 000000000..d97a870ea --- /dev/null +++ b/res/drawable/seek_back.xml @@ -0,0 +1,30 @@ + + + + + + + \ No newline at end of file diff --git a/res/layout/all_apps_container.xml b/res/layout/all_apps_container.xml index 626edafab..3a2c96cd0 100644 --- a/res/layout/all_apps_container.xml +++ b/res/layout/all_apps_container.xml @@ -35,4 +35,13 @@ android:focusable="true" android:descendantFocusability="afterDescendants" /> + + \ No newline at end of file diff --git a/res/layout/scrub_layout.xml b/res/layout/scrub_layout.xml new file mode 100644 index 000000000..11ee381d0 --- /dev/null +++ b/res/layout/scrub_layout.xml @@ -0,0 +1,44 @@ + + + + + + + + + diff --git a/res/layout/scrubber_container.xml b/res/layout/scrubber_container.xml new file mode 100644 index 000000000..4fe84755f --- /dev/null +++ b/res/layout/scrubber_container.xml @@ -0,0 +1,53 @@ + + + + + + + + + + diff --git a/res/layout/widgets_view.xml b/res/layout/widgets_view.xml index 755634f82..1f276adb9 100644 --- a/res/layout/widgets_view.xml +++ b/res/layout/widgets_view.xml @@ -46,6 +46,15 @@ android:layout_gravity="center" android:elevation="15dp" android:visibility="gone" /> + + \ No newline at end of file diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 7ffebce9b..d7f9ef4fa 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -128,4 +128,10 @@ + + + + + + diff --git a/res/values/colors.xml b/res/values/colors.xml index 8a7f62743..b70e1f895 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -33,8 +33,9 @@ #FF757575 #FF666666 + #FFF #FFF5F5F5 - #FF374248 + #76000000 #FFFFFFFF @@ -44,10 +45,15 @@ #009688 + #FFF #DDDDDD #FFFFFF #C4C4C4 #263238 + + @android:color/white + @android:color/darker_gray + #CC14191E diff --git a/res/values/dimens.xml b/res/values/dimens.xml index e3c81941c..799ea9803 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -64,12 +64,15 @@ 0dp + 36dp 8dp 24sp 48dp 8dp 8dp 24dp + 12dp + 48dp 8dp 18dp @@ -78,6 +81,8 @@ 144dp 700dp 475dp + 55dp + 48dp 8dp @@ -140,10 +145,15 @@ 4dp 2dp - + 20dp 8dp 2dp + + 30dp + 48dp + 20dp + 300 100 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/BaseContainerView.java b/src/com/android/launcher3/BaseContainerView.java index c11824054..ac2afa944 100644 --- a/src/com/android/launcher3/BaseContainerView.java +++ b/src/com/android/launcher3/BaseContainerView.java @@ -20,7 +20,10 @@ import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; +import android.view.View; +import android.view.ViewStub; import android.widget.LinearLayout; +import android.widget.TextView; /** * A base container view, which supports resizing. @@ -43,6 +46,11 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab // The inset to apply to the edges and between the search bar and the container private int mContainerBoundsInset; private boolean mHasSearchBar; + private boolean mUseScrubber; + + protected View mScrubberContainerView; + protected BaseRecyclerViewScrubber mScrubber; + protected final int mScrubberHeight; public BaseContainerView(Context context) { this(context, null); @@ -55,6 +63,7 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab public BaseContainerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mContainerBoundsInset = getResources().getDimensionPixelSize(R.dimen.container_bounds_inset); + mScrubberHeight = getResources().getDimensionPixelSize(R.dimen.scrubber_height); } @Override @@ -67,6 +76,10 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab mHasSearchBar = true; } + protected boolean hasSearchBar() { + return mHasSearchBar; + } + /** * Sets the search bar bounds for this container view to match. */ @@ -87,10 +100,46 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab }); } + public final void setUseScrubber(boolean use) { + mUseScrubber = use; + if (use) { + ViewStub stub = (ViewStub) findViewById(R.id.scrubber_container_stub); + mScrubberContainerView = stub.inflate(); + if (mScrubberContainerView == null) { + throw new IllegalStateException( + "Layout must contain an id: R.id.scrubber_container"); + } + mScrubber = (BaseRecyclerViewScrubber) + mScrubberContainerView.findViewById(R.id.base_scrubber); + BaseRecyclerView recyclerView = getRecyclerView(); + if (recyclerView != null) { + mScrubber.setRecycler(recyclerView); + mScrubber + .setScrubberIndicator((TextView) mScrubberContainerView + .findViewById(R.id.scrubberIndicator)); + mScrubber.updateSections(); + } + } else { + removeView(mScrubberContainerView); + BaseRecyclerView recyclerView = getRecyclerView(); + if (recyclerView != null) { + recyclerView.setUseScrollbar(true); + } + } + } + + public final boolean userScrubber() { + return mUseScrubber; + } + + protected void updateBackgroundAndPaddings() { + updateBackgroundAndPaddings(false); + } + /** * Update the backgrounds and padding in response to a change in the bounds or insets. */ - protected void updateBackgroundAndPaddings() { + protected void updateBackgroundAndPaddings(boolean force) { Rect padding; Rect searchBarBounds = new Rect(); if (!isValidSearchBarBounds(mFixedSearchBarBounds)) { @@ -119,7 +168,8 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab // If either the computed container padding has changed, or the computed search bar bounds // has changed, then notify the container - if (!padding.equals(mContentPadding) || !searchBarBounds.equals(mSearchBarBounds)) { + if (force || !padding.equals(mContentPadding) || + !searchBarBounds.equals(mSearchBarBounds)) { mContentPadding.set(padding); mContentBounds.set(padding.left, padding.top, getMeasuredWidth() - padding.right, @@ -135,6 +185,11 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab protected abstract void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding); /** + * This might be null if the container doesn't have a recycler. + */ + protected abstract BaseRecyclerView getRecyclerView(); + + /** * Returns whether the search bar bounds we got are considered valid. */ private boolean isValidSearchBarBounds(Rect searchBarBounds) { diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java index f0d8b3b3d..77925b5b3 100644 --- a/src/com/android/launcher3/BaseRecyclerView.java +++ b/src/com/android/launcher3/BaseRecyclerView.java @@ -57,6 +57,7 @@ public abstract class BaseRecyclerView extends RecyclerView } protected BaseRecyclerViewFastScrollBar mScrollbar; + protected boolean mUseScrollbar = false; private int mDownX; private int mDownY; @@ -74,7 +75,6 @@ public abstract class BaseRecyclerView extends RecyclerView public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mDeltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP; - mScrollbar = new BaseRecyclerViewFastScrollBar(this, getResources()); ScrollListener listener = new ScrollListener(); setOnScrollListener(listener); @@ -93,12 +93,16 @@ public abstract class BaseRecyclerView extends RecyclerView // initiate that here if the recycler view scroll state is not // RecyclerView.SCROLL_STATE_IDLE. - onUpdateScrollbar(dy); + if (mUseScrollbar) { + onUpdateScrollbar(dy); + } } } public void reset() { - mScrollbar.reattachThumbToScroll(); + if (mUseScrollbar) { + mScrollbar.reattachThumbToScroll(); + } } @Override @@ -137,19 +141,28 @@ public abstract class BaseRecyclerView extends RecyclerView if (shouldStopScroll(ev)) { stopScroll(); } - mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); + if (mScrollbar != null) { + mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); + } break; case MotionEvent.ACTION_MOVE: mLastY = y; - mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); + if (mScrollbar != null) { + mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); + } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: onFastScrollCompleted(); - mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); + if (mScrollbar != null) { + mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); + } break; } - return mScrollbar.isDraggingThumb(); + if (mUseScrollbar) { + return mScrollbar.isDraggingThumb(); + } + return false; } public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { @@ -183,7 +196,10 @@ public abstract class BaseRecyclerView extends RecyclerView * Returns the scroll bar width when the user is scrolling. */ public int getMaxScrollbarWidth() { - return mScrollbar.getThumbMaxWidth(); + if (mUseScrollbar) { + return mScrollbar.getThumbMaxWidth(); + } + return 0; } /** @@ -204,9 +220,12 @@ public abstract class BaseRecyclerView extends RecyclerView * AvailableScrollBarHeight = Total height of the visible view - thumb height */ protected int getAvailableScrollBarHeight() { - int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; - int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight(); - return availableScrollBarHeight; + if (mUseScrollbar) { + int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; + int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight(); + return availableScrollBarHeight; + } + return 0; } /** @@ -223,11 +242,23 @@ public abstract class BaseRecyclerView extends RecyclerView return defaultInactiveThumbColor; } + public void setUseScrollbar(boolean useScrollbar) { + mUseScrollbar = useScrollbar; + if (useScrollbar) { + mScrollbar = new BaseRecyclerViewFastScrollBar(this, getResources()); + } else { + mScrollbar = null; + } + invalidate(); + } + @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); - onUpdateScrollbar(0); - mScrollbar.draw(canvas); + if (mUseScrollbar) { + onUpdateScrollbar(0); + mScrollbar.draw(canvas); + } } /** @@ -241,6 +272,9 @@ public abstract class BaseRecyclerView extends RecyclerView */ protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState, int rowCount) { + if (!mUseScrollbar) { + return; + } // Only show the scrollbar if there is height to be scrolled int availableScrollBarHeight = getAvailableScrollBarHeight(); int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight); @@ -273,6 +307,12 @@ public abstract class BaseRecyclerView extends RecyclerView */ public abstract String scrollToPositionAtProgress(float touchFraction); + public abstract String scrollToSection(String sectionName); + + public abstract String[] getSectionNames(); + + public void setFastScrollDragging(boolean dragging) {} + /** * Updates the bounds for the scrollbar. *

Override in each subclass of this base class. diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java index fcee7e8dd..e7b79927a 100644 --- a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java +++ b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java @@ -38,6 +38,7 @@ public class BaseRecyclerViewFastScrollBar { public interface FastScrollFocusableView { void setFastScrollFocused(boolean focused, boolean animated); + void setFastScrollDimmed(boolean dimmed, boolean animated); } private final static int MAX_TRACK_ALPHA = 30; @@ -193,6 +194,7 @@ public class BaseRecyclerViewFastScrollBar { Math.abs(y - downY) > config.getScaledTouchSlop()) { mRv.getParent().requestDisallowInterceptTouchEvent(true); mIsDragging = true; + mRv.setFastScrollDragging(mIsDragging); if (mCanThumbDetach) { mIsThumbDetached = true; } @@ -220,6 +222,7 @@ public class BaseRecyclerViewFastScrollBar { mIgnoreDragGesture = false; if (mIsDragging) { mIsDragging = false; + mRv.setFastScrollDragging(mIsDragging); mPopup.animateVisibility(false); animateScrollbar(false); } diff --git a/src/com/android/launcher3/BaseRecyclerViewScrubber.java b/src/com/android/launcher3/BaseRecyclerViewScrubber.java new file mode 100644 index 000000000..1692548a4 --- /dev/null +++ b/src/com/android/launcher3/BaseRecyclerViewScrubber.java @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2013 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.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.content.Context; +import android.graphics.Color; +import android.graphics.PointF; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Message; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.LinearSmoothScroller; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.TextView; + +import java.lang.IllegalArgumentException; +import java.util.ArrayList; +import java.util.Collections; + +/** + * BaseRecyclerViewScrubber + *

+ *     This is the scrubber at the bottom of a BaseRecyclerView
+ * 
+ * + * @see {@link LinearLayout} + */ +public class BaseRecyclerViewScrubber extends LinearLayout { + private BaseRecyclerView mBaseRecyclerView; + private TextView mScrubberIndicator; + private SeekBar mSeekBar; + private AutoExpandTextView mScrubberText; + private SectionContainer mSectionContainer; + private ScrubberAnimationState mScrubberAnimationState; + private Drawable mTransparentDrawable; + private boolean mIsRtl; + + private static final int MSG_SET_TARGET = 1000; + private static final int MSG_ANIMATE_PICK = MSG_SET_TARGET + 1; + + /** + * UiHandler + *
+     *     Using a handler for sending signals to perform certain actions.  The reason for
+     *     using this is to be able to remove and replace a signal if signals are being
+     *     sent too fast (e.g. user scrubbing like crazy). This allows the touch loop to
+     *     complete then later run the animations in their own loops.
+     * 
+ */ + private class UiHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SET_TARGET: + int adapterIndex = msg.arg1; + performSetTarget(adapterIndex); + + break; + case MSG_ANIMATE_PICK: + int index = msg.arg1; + int width = msg.arg2; + int lastIndex = (Integer)msg.obj; + performAnimatePickMessage(index, width, lastIndex); + break; + default: + super.handleMessage(msg); + } + } + + /** + * Overidden to remove identical calls if they are called subsequently fast enough. + * + * This is the final point that is public in the call chain. Other calls to sendMessageXXX + * will eventually call this function which calls "enqueueMessage" which is private. + * + * @param msg {@link Message} + * @param uptimeMillis {@link Long} + * + * @throws IllegalArgumentException {@link IllegalArgumentException} + */ + @Override + public boolean sendMessageAtTime(Message msg, long uptimeMillis) throws + IllegalArgumentException { + if (msg == null) { + throw new IllegalArgumentException("'msg' cannot be null!"); + } + if (hasMessages(msg.what)) { + removeMessages(msg.what); + } + return super.sendMessageAtTime(msg, uptimeMillis); + } + + } + private Handler mUiHandler = new UiHandler(); + private void sendSetTargetMessage(int adapterIndex) { + Message msg = mUiHandler.obtainMessage(MSG_SET_TARGET); + msg.what = MSG_SET_TARGET; + msg.arg1 = adapterIndex; + mUiHandler.sendMessage(msg); + } + private void performSetTarget(int adapterIndex) { + mBaseRecyclerView.scrollToSection(mSectionContainer.getSectionName(adapterIndex, mIsRtl)); + } + private void sendAnimatePickMessage(int index, int width, int lastIndex) { + Message msg = mUiHandler.obtainMessage(MSG_ANIMATE_PICK); + msg.what = MSG_ANIMATE_PICK; + msg.arg1 = index; + msg.arg2 = width; + msg.obj = lastIndex; + mUiHandler.sendMessage(msg); + } + private void performAnimatePickMessage(int index, int width, int lastIndex) { + if (mScrubberIndicator != null) { + // get the index based on the direction the user is scrolling + int directionalIndex = mSectionContainer.getDirectionalIndex(lastIndex, index); + String sectionText = mSectionContainer.getSectionName(directionalIndex, mIsRtl); + float translateX = (index * width) / (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; + if (mIsRtl) { + translateX = -translateX; + } + mScrubberIndicator.setTranslationX(translateX); + mScrubberIndicator.setText(sectionText); + } + } + + /** + * Constructor + * + * @param context {@link Context} + * @param attrs {@link AttributeSet} + */ + public BaseRecyclerViewScrubber(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + /** + * Constructor + * + * @param context {@link Context} + */ + public BaseRecyclerViewScrubber(Context context) { + super(context); + init(context); + } + + @Override + public void onRtlPropertiesChanged(int layoutDirection) { + super.onRtlPropertiesChanged(layoutDirection); + mIsRtl = Utilities.isRtl(getResources()); + updateSections(); + } + + /** + * Simple container class that tries to abstract out the knowledge of complex sections vs + * simple string sections + */ + private static class SectionContainer { + private BaseRecyclerViewScrubberSection. + RtlIndexArrayList mSections; + private String[] mSectionNames; + private final boolean mIsRtl; + + public SectionContainer(String[] sections, boolean isRtl) { + mIsRtl = isRtl; + mSections = BaseRecyclerViewScrubberSection.createSections(sections, isRtl); + mSectionNames = sections; + if (isRtl) { + final int N = mSectionNames.length; + for(int i = 0; i < N / 2; i++) { + String temp = mSectionNames[i]; + mSectionNames[i] = mSectionNames[N - i - 1]; + mSectionNames[N - i - 1] = temp; + } + Collections.reverse(mSections); + } + } + + public int size() { + return showLetters() ? mSections.size() : mSectionNames.length; + } + + public String getSectionName(int idx, boolean isRtl) { + if (size() == 0) { + return null; + } + return showLetters() ? mSections.get(idx, isRtl).getText() : mSectionNames[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 mSectionNames index (aka the underlying adapter index). + */ + public int getAdapterIndex(int prevIdx, int curIdx) { + if (!showLetters() || size() == 0) { + 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), mIsRtl).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() || size() == 0 || mSections.get(curIdx, mIsRtl).getHighlight()) { + return curIdx; + } + + if (prevIdx < curIdx) { + if (mIsRtl) { + return mSections.get(curIdx).getPreviousIndex(); + } else { + return mSections.get(curIdx).getNextIndex(); + } + } else { + if (mIsRtl) { + 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(BaseRecyclerViewScrubberSection.getHighlightText(mSections)); + } + } + + public void updateSections() { + if (mBaseRecyclerView != null) { + mSectionContainer = new SectionContainer(mBaseRecyclerView.getSectionNames(), mIsRtl); + mSectionContainer.initializeScrubberText(mScrubberText); + mSeekBar.setMax(mSectionContainer.size() - 1); + + // show a white line if there are no letters, otherwise show transparent + Drawable d = mSectionContainer.showLetters() ? mTransparentDrawable + : getContext().getResources().getDrawable(R.drawable.seek_back); + ((ViewGroup) mSeekBar.getParent()).setBackground(d); + } + } + + public void setRecycler(BaseRecyclerView baseRecyclerView) { + mBaseRecyclerView = baseRecyclerView; + } + + public void setScrubberIndicator(TextView scrubberIndicator) { + mScrubberIndicator = scrubberIndicator; + } + + private boolean isReady() { + return mBaseRecyclerView != null && + mSectionContainer != null; + } + + private void init(Context context) { + mIsRtl = Utilities.isRtl(context.getResources()); + LayoutInflater.from(context).inflate(R.layout.scrub_layout, this); + mTransparentDrawable = new ColorDrawable(Color.TRANSPARENT); + mScrubberAnimationState = new ScrubberAnimationState(); + mSeekBar = (SeekBar) findViewById(R.id.scrubber); + mScrubberText = (AutoExpandTextView) findViewById(R.id.scrubberText); + mSeekBar.setOnSeekBarChangeListener(mScrubberAnimationState); + } + + /** + * Handles the animations of the scrubber indicator + */ + private class ScrubberAnimationState implements SeekBar.OnSeekBarChangeListener { + private static final long SCRUBBER_DISPLAY_DURATION_IN = 60; + private static final long SCRUBBER_DISPLAY_DURATION_OUT = 150; + private static final long SCRUBBER_DISPLAY_DELAY_IN = 0; + private static final long SCRUBBER_DISPLAY_DELAY_OUT = 200; + 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; + + private void touchTrack(boolean touching) { + mTouchingTrack = touching; + + if (mScrubberIndicator != null) { + if (mTouchingTrack) { + animateIn(); + } else if (!mAnimatingIn) { // finish animating in before animating out + animateOut(); + } + + mBaseRecyclerView.setFastScrollDragging(mTouchingTrack); + } + } + + private void animateIn() { + if (mScrubberIndicator == null) { + return; + } + // start from a scratch position when animating in + mScrubberIndicator.animate().cancel(); + mScrubberIndicator.setPivotX(mScrubberIndicator.getMeasuredWidth() / 2); + mScrubberIndicator.setPivotY(mScrubberIndicator.getMeasuredHeight() * 0.9f); + mScrubberIndicator.setAlpha(SCRUBBER_ALPHA_START); + mScrubberIndicator.setScaleX(SCRUBBER_SCALE_START); + mScrubberIndicator.setScaleY(SCRUBBER_SCALE_START); + mScrubberIndicator.setVisibility(View.VISIBLE); + mAnimatingIn = true; + + mScrubberIndicator.animate() + .alpha(SCRUBBER_ALPHA_END) + .scaleX(SCRUBBER_SCALE_END) + .scaleY(SCRUBBER_SCALE_END) + .setStartDelay(SCRUBBER_DISPLAY_DELAY_IN) + .setDuration(SCRUBBER_DISPLAY_DURATION_IN) + .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() { + if (mScrubberIndicator == null) { + return; + } + mScrubberIndicator.animate() + .alpha(SCRUBBER_ALPHA_START) + .scaleX(SCRUBBER_SCALE_START) + .scaleY(SCRUBBER_SCALE_START) + .setStartDelay(SCRUBBER_DISPLAY_DELAY_OUT) + .setDuration(SCRUBBER_DISPLAY_DURATION_OUT) + .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; + } + progressChanged(seekBar, index, fromUser); + } + + private void progressChanged(SeekBar seekBar, int index, boolean fromUser) { + + sendAnimatePickMessage(index, seekBar.getWidth(), mLastIndex); + + // get the index of the underlying list + int adapterIndex = mSectionContainer.getDirectionalIndex(mLastIndex, index); + // Post set target index on queue to get processed by Looper later + sendSetTargetMessage(adapterIndex); + + mLastIndex = index; + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + touchTrack(true); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + touchTrack(false); + } + } +} diff --git a/src/com/android/launcher3/BaseRecyclerViewScrubberSection.java b/src/com/android/launcher3/BaseRecyclerViewScrubberSection.java new file mode 100644 index 000000000..1d17ea887 --- /dev/null +++ b/src/com/android/launcher3/BaseRecyclerViewScrubberSection.java @@ -0,0 +1,267 @@ +/* + * 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 android.util.SparseArray; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; + +public class BaseRecyclerViewScrubberSection { + private static final String TAG = "BRVScrubberSections"; + private static final String ALPHA_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final int MAX_NUMBER_CUSTOM_SECTIONS = 8; + private static final int MAX_SECTIONS = ALPHA_LETTERS.length() + MAX_NUMBER_CUSTOM_SECTIONS; + public static final int INVALID_INDEX = -1; + + private AutoExpandTextView.HighlightedText mHighlightedText; + private int mPreviousValidIndex; + private int mNextValidIndex; + private int mAdapterIndex; + + public BaseRecyclerViewScrubberSection(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(RtlIndexArrayList sections, + boolean isRtl) { + for (int i = 0; i < sections.size(); i++) { + if (sections.get(i, isRtl).getHighlight()) { + return i; + } + } + + return INVALID_INDEX; + } + + private static void createIndices(RtlIndexArrayList sections, + boolean isRtl) { + 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, isRtl); + for (int i = 0; i < sections.size(); i++) { + if (sections.get(i, isRtl).getHighlight()) { + currentIdx = i; + } + + sections.get(i, isRtl).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, isRtl).getHighlight()) { + currentIdx = i; + } + + sections.get(i, isRtl).mNextValidIndex = currentIdx; + } + } + + public static ArrayList getHighlightText( + RtlIndexArrayList sections) { + if (sections == null) { + return null; + } + + ArrayList highlights = new ArrayList<>(sections.size()); + for (BaseRecyclerViewScrubberSection section : sections) { + highlights.add(section.mHighlightedText); + } + + return highlights; + } + + private static void addAlphaLetters(RtlIndexArrayList sections, + HashMap foundAlphaLetters) { + for (int i = 0; i < ALPHA_LETTERS.length(); i++) { + boolean highlighted = foundAlphaLetters.containsKey(i); + int index = highlighted + ? foundAlphaLetters.get(i) : BaseRecyclerViewScrubberSection.INVALID_INDEX; + + sections.add(new BaseRecyclerViewScrubberSection(ALPHA_LETTERS.substring(i, i + 1), + highlighted, index)); + } + } + + /** + * Takes the sections 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 sectionNames list of sectionName Strings + * @return the list of scrubber sections + */ + public static RtlIndexArrayList + createSections(String[] sectionNames, boolean isRtl) { + // check if we have a valid header section + if (!validSectionNameList(sectionNames)) { + return null; + } + + // this will track the mapping of ALPHA_LETTERS index to the headers index + HashMap foundAlphaLetters = new HashMap<>(); + RtlIndexArrayList sections = + new RtlIndexArrayList<>(sectionNames.length); + boolean inAlphaLetterSection = false; + + for (int i = 0; i < sectionNames.length; i++) { + int alphaLetterIndex = TextUtils.isEmpty(sectionNames[i]) + ? -1 : ALPHA_LETTERS.indexOf(sectionNames[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 BaseRecyclerViewScrubberSection(sectionNames[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 + BaseRecyclerViewScrubberSection.createIndices(sections, isRtl); + + return sections; + } + + /** + * Walk through the sectionNames and check for a few things: + * 1) No more than MAX_NUMBER_CUSTOM_SECTIONS sectionNames exist in the sectionNames list or no more + * than MAX_SECTIONS sectionNames 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 sectionNames that exceed length of 1 + * 4) The alpha letter sectionName is together and not separated by other things + */ + private static boolean validSectionNameList(String[] sectionNames) { + int numCustomSections = 0; + int previousAlphaIndex = -1; + boolean foundAlphaSections = false; + + for (String s : sectionNames) { + if (TextUtils.isEmpty(s)) { + numCustomSections++; + continue; + } + + if (s.length() > 1) { + Log.w(TAG, "Found section " + 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 (foundAlphaSections && previousAlphaIndex == -1) { + Log.w(TAG, "Found alpha letters twice"); + return false; + } + + previousAlphaIndex = alphaIndex; + foundAlphaSections = true; + } else { + numCustomSections++; + previousAlphaIndex = -1; + } + } + + final int listSize = foundAlphaSections + ? numCustomSections + ALPHA_LETTERS.length() + : numCustomSections; + + // if one of these conditions are satisfied, then return true + if (numCustomSections <= MAX_NUMBER_CUSTOM_SECTIONS || listSize <= MAX_SECTIONS) { + return true; + } + + if (numCustomSections > MAX_NUMBER_CUSTOM_SECTIONS) { + Log.w(TAG, "Found " + numCustomSections + "# custom sections when " + + MAX_NUMBER_CUSTOM_SECTIONS + " is allowed!"); + } else if (listSize > MAX_SECTIONS) { + Log.w(TAG, "Found " + listSize + " sections when " + + MAX_SECTIONS + " is allowed!"); + } + + return false; + } + + public static class RtlIndexArrayList extends ArrayList { + + public RtlIndexArrayList(int capacity) { + super(capacity); + } + + public T get(int index, boolean isRtl) { + if (isRtl) { + index = size() - 1 - index; + } + return super.get(index); + } + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 507087824..205c113a7 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -86,7 +86,7 @@ public class BubbleTextView extends TextView private final boolean mDeferShadowGenerationOnTouch; private final boolean mCustomShadowsEnabled; private final boolean mLayoutHorizontal; - private final int mIconSize; + private int mIconSize; private int mTextColor; private boolean mStayPressed; @@ -94,9 +94,11 @@ public class BubbleTextView extends TextView private boolean mDisableRelayout = false; private ObjectAnimator mFastScrollFocusAnimator; + private ObjectAnimator mFastScrollDimAnimator; private Paint mFastScrollFocusBgPaint; private float mFastScrollFocusFraction; private boolean mFastScrollFocused; + private boolean mFastScrollDimmed; private final int mFastScrollMode = FAST_SCROLL_FOCUS_MODE_SCALE_ICON; private IconLoadRequest mIconLoadRequest; @@ -433,6 +435,10 @@ public class BubbleTextView extends TextView if (mBackground != null) mBackground.setCallback(null); } + public void setIconSize(int iconSize) { + mIconSize = iconSize; + } + @Override public void setTextColor(int color) { mTextColor = color; @@ -628,6 +634,30 @@ public class BubbleTextView extends TextView } } + @Override + public void setFastScrollDimmed(boolean dimmed, boolean animated) { + if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_NONE) { + return; + } + + if (!animated) { + mFastScrollDimmed = dimmed; + setAlpha(dimmed ? 0.4f : 1f); + } else if (mFastScrollDimmed != dimmed) { + mFastScrollDimmed = dimmed; + + // Clean up the previous dim animator + if (mFastScrollDimAnimator != null) { + mFastScrollDimAnimator.cancel(); + } + mFastScrollDimAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, + dimmed ? 0.4f : 1f); + mFastScrollDimAnimator.setDuration(dimmed ? + FAST_SCROLL_FOCUS_FADE_IN_DURATION : FAST_SCROLL_FOCUS_FADE_OUT_DURATION); + mFastScrollDimAnimator.start(); + } + } + /** * Interface to be implemented by the grand parent to allow click shadow effect. */ diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index 774594fe2..f77b34862 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -33,6 +33,8 @@ import android.view.ViewGroup.MarginLayoutParams; import android.widget.FrameLayout; import android.widget.LinearLayout; +import com.android.launcher3.allapps.AllAppsContainerView; + public class DeviceProfile { public final InvariantDeviceProfile inv; @@ -225,11 +227,13 @@ public class DeviceProfile { /** * @param recyclerViewWidth the available width of the AllAppsRecyclerView */ - public void updateAppsViewNumCols(Resources res, int recyclerViewWidth) { - int appsViewLeftMarginPx = - res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin); - int allAppsCellWidthGap = - res.getDimensionPixelSize(R.dimen.all_apps_icon_width_gap); + public void updateAppsViewNumCols(Resources res, int recyclerViewWidth, int gridStrategy) { + int appsViewLeftMarginPx = gridStrategy == AllAppsContainerView.SECTION_STRATEGY_GRID ? + res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin) : + res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin_with_sections); + int allAppsCellWidthGap = gridStrategy == AllAppsContainerView.SECTION_STRATEGY_GRID ? + res.getDimensionPixelSize(R.dimen.all_apps_icon_width_gap) : + res.getDimensionPixelSize(R.dimen.all_apps_icon_width_gap_with_sections); int availableAppsWidthPx = (recyclerViewWidth > 0) ? recyclerViewWidth : availableWidthPx; int numAppsCols = (availableAppsWidthPx - appsViewLeftMarginPx) / (allAppsIconSizePx + allAppsCellWidthGap); diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 6faea2084..1976ca982 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -348,6 +348,8 @@ public class Launcher extends Activity private DeviceProfile mDeviceProfile; + private boolean mUseScrubber = true; + // This is set to the view that launched the activity that navigated the user away from // launcher. Since there is no callback for when the activity has finished launching, enable // the press state and keep this reference to reset the press state when we return to launcher. @@ -1448,6 +1450,11 @@ public class Launcher extends Activity mAppsView.setSearchBarController(mAppsView.newDefaultAppSearchController()); } + mAppsView.setUseScrubber(mUseScrubber); + mAppsView.setSectionStrategy(AllAppsContainerView.SECTION_STRATEGY_RAGGED); + mAppsView.setGridTheme(AllAppsContainerView.GRID_THEME_DARK); + mWidgetsView.setUseScrubber(false); + // Setup the drag controller (drop targets have to be added in reverse order in priority) dragController.setDragScoller(mWorkspace); dragController.setScrollView(mDragLayer); diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java index 88c6acada..bff7752af 100644 --- a/src/com/android/launcher3/allapps/AllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java @@ -35,6 +35,7 @@ import android.view.ViewGroup; import android.widget.LinearLayout; import com.android.launcher3.AppInfo; import com.android.launcher3.BaseContainerView; +import com.android.launcher3.BaseRecyclerView; import com.android.launcher3.CellLayout; import com.android.launcher3.DeleteDropTarget; import com.android.launcher3.DeviceProfile; @@ -130,8 +131,14 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc LauncherTransitionable, View.OnTouchListener, View.OnLongClickListener, AllAppsSearchBarController.Callbacks { + public static final int SECTION_STRATEGY_GRID = 1; + public static final int SECTION_STRATEGY_RAGGED = 2; + + public static final int GRID_THEME_LIGHT = 1; + public static final int GRID_THEME_DARK = 2; + private static final int MIN_ROWS_IN_MERGED_SECTION_PHONE = 3; - private static final int MAX_NUM_MERGES_PHONE = 2; + private static final int MAX_NUM_MERGES_PHONE = 1; @Thunk Launcher mLauncher; @Thunk AlphabeticalAppsList mApps; @@ -148,6 +155,10 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc private View mSearchBarView; private SpannableStringBuilder mSearchQueryBuilder = null; + private int mSectionStrategy = SECTION_STRATEGY_RAGGED; + private int mGridTheme = GRID_THEME_DARK; + private int mLastGridTheme = -1; + private int mSectionNamesMargin; private int mNumAppsPerRow; private int mNumPredictedAppsPerRow; @@ -178,9 +189,12 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc Resources res = context.getResources(); mLauncher = (Launcher) context; - mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin); + mSectionNamesMargin = mSectionStrategy == SECTION_STRATEGY_GRID ? + res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin) : + res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin_with_sections); mApps = new AlphabeticalAppsList(context); - mAdapter = new AllAppsGridAdapter(mLauncher, mApps, this, mLauncher, this); + mAdapter = new AllAppsGridAdapter(mLauncher, mApps, this, mLauncher, + this, mSectionStrategy, mGridTheme); mApps.setAdapter(mAdapter); mLayoutManager = mAdapter.getLayoutManager(); mItemDecoration = mAdapter.getItemDecoration(); @@ -196,6 +210,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc */ public void setPredictedApps(List apps) { mApps.setPredictedApps(apps); + updateScrubber(); } /** @@ -203,6 +218,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc */ public void setApps(List apps) { mApps.setApps(apps); + updateScrubber(); } /** @@ -210,6 +226,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc */ public void addApps(List apps) { mApps.addApps(apps); + updateScrubber(); } /** @@ -217,6 +234,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc */ public void updateApps(List apps) { mApps.updateApps(apps); + updateScrubber(); } /** @@ -224,6 +242,29 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc */ public void removeApps(List apps) { mApps.removeApps(apps); + updateScrubber(); + } + + private void updateScrubber() { + if (userScrubber()) { + mScrubber.updateSections(); + } + } + + public void setSectionStrategy(int sectionStrategy) { + Resources res = getResources(); + mSectionStrategy = sectionStrategy; + mSectionNamesMargin = mSectionStrategy == SECTION_STRATEGY_GRID ? + res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin) : + res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin_with_sections); + mAdapter.setSectionStrategy(mSectionStrategy); + mAppsRecyclerView.setSectionStrategy(mSectionStrategy); + } + + public void setGridTheme(int gridTheme) { + mGridTheme = gridTheme; + mAdapter.setGridTheme(mGridTheme); + updateBackgroundAndPaddings(true); } /** @@ -316,6 +357,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc // Load the all apps recycler view mAppsRecyclerView = (AllAppsRecyclerView) findViewById(R.id.apps_list_view); mAppsRecyclerView.setApps(mApps); + mAppsRecyclerView.setSectionStrategy(mSectionStrategy); mAppsRecyclerView.setLayoutManager(mLayoutManager); mAppsRecyclerView.setAdapter(mAdapter); mAppsRecyclerView.setHasFixedSize(true); @@ -337,7 +379,8 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc int availableWidth = !mContentBounds.isEmpty() ? mContentBounds.width() : MeasureSpec.getSize(widthMeasureSpec); DeviceProfile grid = mLauncher.getDeviceProfile(); - grid.updateAppsViewNumCols(getResources(), availableWidth); + grid.updateAppsViewNumCols(getResources(), availableWidth, + mSectionStrategy); if (mNumAppsPerRow != grid.allAppsNumCols || mNumPredictedAppsPerRow != grid.allAppsNumPredictiveCols) { mNumAppsPerRow = grid.allAppsNumCols; @@ -345,7 +388,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc // If there is a start margin to draw section names, determine how we are going to merge // app sections - boolean mergeSectionsFully = mSectionNamesMargin == 0 || !grid.isPhone; + boolean mergeSectionsFully = mSectionStrategy == SECTION_STRATEGY_GRID; AlphabeticalAppsList.MergeAlgorithm mergeAlgorithm = mergeSectionsFully ? new FullMergeAlgorithm() : new SimpleSectionMergeAlgorithm((int) Math.ceil(mNumAppsPerRow / 2f), @@ -369,8 +412,10 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc boolean isRtl = Utilities.isRtl(getResources()); // TODO: Use quantum_panel instead of quantum_panel_shape + int bgRes = mGridTheme == GRID_THEME_DARK ? R.drawable.quantum_panel_shape_dark : + R.drawable.quantum_panel_shape; InsetDrawable background = new InsetDrawable( - getResources().getDrawable(R.drawable.quantum_panel_shape), padding.left, 0, + getResources().getDrawable(bgRes), padding.left, 0, padding.right, 0); Rect bgPadding = new Rect(); background.getPadding(bgPadding); @@ -389,12 +434,24 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc // names) int startInset = Math.max(mSectionNamesMargin, mAppsRecyclerView.getMaxScrollbarWidth()); int topBottomPadding = mRecyclerViewTopBottomPadding; + final boolean useScubber = userScrubber(); if (isRtl) { mAppsRecyclerView.setPadding(padding.left + mAppsRecyclerView.getMaxScrollbarWidth(), - topBottomPadding, padding.right + startInset, topBottomPadding); + topBottomPadding, padding.right + startInset, useScubber ? + mScrubberHeight + topBottomPadding : topBottomPadding); + if (useScubber) { + mScrubberContainerView + .setPadding(padding.left + mAppsRecyclerView.getMaxScrollbarWidth(), + 0, padding.right, 0); + } } else { mAppsRecyclerView.setPadding(padding.left + startInset, topBottomPadding, - padding.right + mAppsRecyclerView.getMaxScrollbarWidth(), topBottomPadding); + padding.right + mAppsRecyclerView.getMaxScrollbarWidth(), + useScubber ? mScrubberHeight + topBottomPadding : topBottomPadding); + if (useScubber) { + mScrubberContainerView.setPadding(padding.left, 0, + padding.right + mAppsRecyclerView.getMaxScrollbarWidth(), 0); + } } // Inset the search bar to fit its bounds above the container @@ -556,7 +613,9 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { if (toWorkspace) { // Reset the search bar and base recycler view after transitioning home - mSearchBarController.reset(); + if (hasSearchBar()) { + mSearchBarController.reset(); + } mAppsRecyclerView.reset(); } } @@ -614,6 +673,12 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc public void onSearchResult(String query, ArrayList apps) { if (apps != null) { mApps.setOrderedFilter(apps); + if (mGridTheme != GRID_THEME_LIGHT) { + mLastGridTheme = mGridTheme; + mGridTheme = GRID_THEME_LIGHT; + updateBackgroundAndPaddings(true); + mAdapter.setGridTheme(mGridTheme); + } mAdapter.setLastSearchQuery(query); mAppsRecyclerView.onSearchResultsChanged(); } @@ -623,10 +688,20 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc public void clearSearchResult() { mApps.setOrderedFilter(null); mAppsRecyclerView.onSearchResultsChanged(); - + if (mLastGridTheme != -1 && mLastGridTheme != GRID_THEME_LIGHT) { + mGridTheme = mLastGridTheme; + updateBackgroundAndPaddings(true); + mAdapter.setGridTheme(mGridTheme); + mLastGridTheme = -1; + } // Clear the search query mSearchQueryBuilder.clear(); mSearchQueryBuilder.clearSpans(); Selection.setSelection(mSearchQueryBuilder, 0); } + + @Override + protected BaseRecyclerView getRecyclerView() { + return mAppsRecyclerView; + } } diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java index 1f95133d4..a48390732 100644 --- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java +++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java @@ -24,7 +24,6 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PointF; import android.graphics.Rect; -import android.graphics.drawable.Drawable; import android.support.v4.view.accessibility.AccessibilityRecordCompat; import android.support.v4.view.accessibility.AccessibilityEventCompat; import android.net.Uri; @@ -69,6 +68,10 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter mCachedSectionBounds = new HashMap<>(); private Rect mTmpBounds = new Rect(); @@ -349,12 +352,16 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter + mLastFastScrollFocusedViews = new ArrayList(); @Thunk int mPrevFastScrollFocusedPosition; + @Thunk AlphabeticalAppsList.SectionInfo mPrevFastScrollFocusedSection; @Thunk int mFastScrollFrameIndex; @Thunk final int[] mFastScrollFrames = new int[10]; @@ -81,7 +87,9 @@ public class AllAppsRecyclerView extends BaseRecyclerView super(context, attrs, defStyleAttr); Resources res = getResources(); - mScrollbar.setDetachThumbOnFastScroll(); + if (mUseScrollbar) { + mScrollbar.setDetachThumbOnFastScroll(); + } mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize( R.dimen.all_apps_empty_search_bg_top_offset); } @@ -109,13 +117,20 @@ public class AllAppsRecyclerView extends BaseRecyclerView pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows); } + public void setSectionStrategy(int sectionStrategy) { + mSectionStrategy = sectionStrategy; + mFocusSection = mSectionStrategy == AllAppsContainerView.SECTION_STRATEGY_RAGGED; + } + /** * Scrolls this recycler view to the top. */ public void scrollToTop() { - // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling - if (mScrollbar.isThumbDetached()) { - mScrollbar.reattachThumbToScroll(); + if (mUseScrollbar) { + // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling + if (mScrollbar.isThumbDetached()) { + mScrollbar.reattachThumbToScroll(); + } } scrollToPosition(0); } @@ -235,10 +250,18 @@ public class AllAppsRecyclerView extends BaseRecyclerView } if (mPrevFastScrollFocusedPosition != lastInfo.fastScrollToItem.position) { + if (mFocusSection) { + setSectionFastScrollDimmed(mPrevFastScrollFocusedPosition, true, true); + } else if (mLastFastScrollFocusedView != null){ + mLastFastScrollFocusedView.setFastScrollDimmed(true, true); + } mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position; - - // Reset the last focused view - if (mLastFastScrollFocusedView != null) { + mPrevFastScrollFocusedSection = + getSectionInfoForPosition(lastInfo.fastScrollToItem.position); + // Reset the last focused section + if (mFocusSection) { + clearSectionFocusedItems(); + } else if (mLastFastScrollFocusedView != null) { mLastFastScrollFocusedView.setFastScrollFocused(false, true); mLastFastScrollFocusedView = null; } @@ -246,12 +269,17 @@ public class AllAppsRecyclerView extends BaseRecyclerView if (mFastScrollMode == FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON) { smoothSnapToPosition(mPrevFastScrollFocusedPosition, mScrollPosState); } else if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) { - final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition); - if (vh != null && - vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) { - mLastFastScrollFocusedView = - (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView; - mLastFastScrollFocusedView.setFastScrollFocused(true, true); + if (mFocusSection) { + setSectionFastScrollFocused(mPrevFastScrollFocusedPosition); + } else { + final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition); + if (vh != null && + vh.itemView instanceof + BaseRecyclerViewFastScrollBar.FastScrollFocusableView) { + mLastFastScrollFocusedView = + (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView; + mLastFastScrollFocusedView.setFastScrollFocused(true, true); + } } } else { throw new RuntimeException("Unexpected fast scroll mode"); @@ -264,11 +292,14 @@ public class AllAppsRecyclerView extends BaseRecyclerView public void onFastScrollCompleted() { super.onFastScrollCompleted(); // Reset and clean up the last focused view - if (mLastFastScrollFocusedView != null) { + if (mFocusSection) { + clearSectionFocusedItems(); + } else if (mLastFastScrollFocusedView != null) { mLastFastScrollFocusedView.setFastScrollFocused(false, true); mLastFastScrollFocusedView = null; } mPrevFastScrollFocusedPosition = -1; + mPrevFastScrollFocusedSection = null; } /** @@ -276,6 +307,9 @@ public class AllAppsRecyclerView extends BaseRecyclerView */ @Override public void onUpdateScrollbar(int dy) { + if (!mUseScrollbar) { + return; + } List items = mApps.getAdapterItems(); // Skip early if there are no items or we haven't been measured @@ -294,7 +328,8 @@ public class AllAppsRecyclerView extends BaseRecyclerView // Only show the scrollbar if there is height to be scrolled int availableScrollBarHeight = getAvailableScrollBarHeight(); - int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows(), mScrollPosState.rowHeight); + int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows(), + mScrollPosState.rowHeight); if (availableScrollHeight <= 0) { mScrollbar.setThumbOffset(-1, -1); return; @@ -354,6 +389,97 @@ public class AllAppsRecyclerView extends BaseRecyclerView } } + @Override + public String scrollToSection(String sectionName) { + List scrollSectionInfos = + mApps.getFastScrollerSections(); + if (scrollSectionInfos != null) { + for (int i = 0; i < scrollSectionInfos.size(); i++) { + AlphabeticalAppsList.FastScrollSectionInfo info = scrollSectionInfos.get(i); + if (info.sectionName.equals(sectionName)) { + scrollToPositionAtProgress(info.touchFraction); + return info.sectionName; + } + } + } + return null; + } + + @Override + public String[] getSectionNames() { + List scrollSectionInfos = + mApps.getFastScrollerSections(); + if (scrollSectionInfos != null) { + String[] sectionNames = new String[scrollSectionInfos.size()]; + for (int i = 0; i < scrollSectionInfos.size(); i++) { + AlphabeticalAppsList.FastScrollSectionInfo info = scrollSectionInfos.get(i); + sectionNames[i] = info.sectionName; + } + + return sectionNames; + } + return new String[0]; + } + + private AlphabeticalAppsList.SectionInfo getSectionInfoForPosition(int position) { + List sections = + mApps.getSections(); + for (AlphabeticalAppsList.SectionInfo section : sections) { + if (section.firstAppItem.position == position) { + return section; + } + } + return null; + } + + private void setSectionFastScrollFocused(int position) { + if (mPrevFastScrollFocusedSection != null) { + for (int i = 0; i < mPrevFastScrollFocusedSection.numApps; i++) { + int sectionPosition = position+i; + final ViewHolder vh = findViewHolderForAdapterPosition(sectionPosition); + if (vh != null && + vh.itemView instanceof + BaseRecyclerViewFastScrollBar.FastScrollFocusableView) { + final BaseRecyclerViewFastScrollBar.FastScrollFocusableView view = + (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView; + view.setFastScrollFocused(true, true); + mLastFastScrollFocusedViews.add(view); + } + } + } + } + + private void setSectionFastScrollDimmed(int position, boolean dimmed, boolean animate) { + if (mPrevFastScrollFocusedSection != null) { + for (int i = 0; i < mPrevFastScrollFocusedSection.numApps; i++) { + int sectionPosition = position+i; + final ViewHolder vh = findViewHolderForAdapterPosition(sectionPosition); + if (vh != null && + vh.itemView instanceof + BaseRecyclerViewFastScrollBar.FastScrollFocusableView) { + final BaseRecyclerViewFastScrollBar.FastScrollFocusableView view = + (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView; + view.setFastScrollDimmed(dimmed, animate); + } + } + } + } + + private void clearSectionFocusedItems() { + final int N = mLastFastScrollFocusedViews.size(); + for (int i = 0; i < N; i++) { + BaseRecyclerViewFastScrollBar.FastScrollFocusableView view = + mLastFastScrollFocusedViews.get(i); + view.setFastScrollFocused(false, true); + } + mLastFastScrollFocusedViews.clear(); + } + + @Override + public void setFastScrollDragging(boolean dragging) { + ((AllAppsGridAdapter) getAdapter()).setIconsDimmed(dragging); + } + /** * This runnable runs a single frame of the smooth scroll animation and posts the next frame * if necessary. @@ -362,18 +488,27 @@ public class AllAppsRecyclerView extends BaseRecyclerView @Override public void run() { if (mFastScrollFrameIndex < mFastScrollFrames.length) { + if (mFocusSection) { + setSectionFastScrollDimmed(mPrevFastScrollFocusedPosition, false, true); + } scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]); mFastScrollFrameIndex++; postOnAnimation(mSmoothSnapNextFrameRunnable); } else { - // Animation completed, set the fast scroll state on the target view - final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition); - if (vh != null && - vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView && - mLastFastScrollFocusedView != vh.itemView) { - mLastFastScrollFocusedView = - (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView; - mLastFastScrollFocusedView.setFastScrollFocused(true, true); + if (mFocusSection) { + setSectionFastScrollFocused(mPrevFastScrollFocusedPosition); + } else { + // Animation completed, set the fast scroll state on the target view + final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition); + if (vh != null && + vh.itemView instanceof + BaseRecyclerViewFastScrollBar.FastScrollFocusableView && + mLastFastScrollFocusedView != vh.itemView) { + mLastFastScrollFocusedView = + (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView; + mLastFastScrollFocusedView.setFastScrollFocused(true, true); + mLastFastScrollFocusedView.setFastScrollDimmed(false, true); + } } } } diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java index 14e2a1863..d5ebdab07 100644 --- a/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java @@ -17,9 +17,7 @@ package com.android.launcher3.allapps; import android.content.Context; import android.graphics.Bitmap; -import android.graphics.Canvas; import android.util.AttributeSet; -import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import com.android.launcher3.BubbleTextView; diff --git a/src/com/android/launcher3/widget/WidgetsContainerView.java b/src/com/android/launcher3/widget/WidgetsContainerView.java index 0c6ea31bb..268e26ebb 100644 --- a/src/com/android/launcher3/widget/WidgetsContainerView.java +++ b/src/com/android/launcher3/widget/WidgetsContainerView.java @@ -29,6 +29,7 @@ import android.view.View; import android.widget.Toast; import com.android.launcher3.BaseContainerView; +import com.android.launcher3.BaseRecyclerView; import com.android.launcher3.CellLayout; import com.android.launcher3.DeleteDropTarget; import com.android.launcher3.DeviceProfile; @@ -366,4 +367,9 @@ public class WidgetsContainerView extends BaseContainerView } return mWidgetPreviewLoader; } + + @Override + protected BaseRecyclerView getRecyclerView() { + return mView; + } } \ No newline at end of file diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/WidgetsRecyclerView.java index 884bdc418..ac32f154e 100644 --- a/src/com/android/launcher3/widget/WidgetsRecyclerView.java +++ b/src/com/android/launcher3/widget/WidgetsRecyclerView.java @@ -126,20 +126,34 @@ public class WidgetsRecyclerView extends BaseRecyclerView { // Skip early if there are no widgets. int rowCount = mWidgets.getPackageSize(); if (rowCount == 0) { - mScrollbar.setThumbOffset(-1, -1); + if (mUseScrollbar) { + mScrollbar.setThumbOffset(-1, -1); + } return; } // Skip early if, there no child laid out in the container. getCurScrollState(mScrollPosState); if (mScrollPosState.rowIndex < 0) { - mScrollbar.setThumbOffset(-1, -1); + if (mUseScrollbar) { + mScrollbar.setThumbOffset(-1, -1); + } return; } synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount); } + @Override + public String scrollToSection(String sectionName) { + return null; + } + + @Override + public String[] getSectionNames() { + return new String[0]; + } + /** * Returns the current scroll state. */ -- 2.11.0