From: cretin45 Date: Sat, 14 Nov 2015 00:51:43 +0000 (-0800) Subject: Reimplement the CM scrubber against the new Launcher X-Git-Url: http://git.osdn.net/view?a=commitdiff_plain;h=acab44fc939b4083ab7ec889b0c6d4fe0db00cca;p=android-x86%2Fpackages-apps-Trebuchet.git Reimplement the CM scrubber against the new Launcher PS4: Implement RTL support Change-Id: I4456d54b5924913d1b36e1cfa9a2269150f6fb3e --- 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-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/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. */