diff options
author | Winson <winsonc@google.com> | 2015-09-29 17:09:03 +0000 |
---|---|---|
committer | Android Git Automerger <android-git-automerger@android.com> | 2015-09-29 17:09:03 +0000 |
commit | 0b90814f8617d4078fc7f7219317a0fc3ac0f0f3 (patch) | |
tree | 19d6e7767e7089043a0f95a232b44628033be092 | |
parent | 9e3a4d8573ba8537e7a30ead2e543a5ac7089b4b (diff) | |
parent | c088049113c261331b5685e64050d14a31cd72df (diff) | |
download | android_packages_apps_Trebuchet-0b90814f8617d4078fc7f7219317a0fc3ac0f0f3.tar.gz android_packages_apps_Trebuchet-0b90814f8617d4078fc7f7219317a0fc3ac0f0f3.tar.bz2 android_packages_apps_Trebuchet-0b90814f8617d4078fc7f7219317a0fc3ac0f0f3.zip |
am c0880491: Highlighting sectioned apps on fast-scroll.
* commit 'c088049113c261331b5685e64050d14a31cd72df':
Highlighting sectioned apps on fast-scroll.
-rw-r--r-- | proguard.flags | 11 | ||||
-rw-r--r-- | src/com/android/launcher3/BaseRecyclerView.java | 67 | ||||
-rw-r--r-- | src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java | 11 | ||||
-rw-r--r-- | src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java | 12 | ||||
-rw-r--r-- | src/com/android/launcher3/BubbleTextView.java | 131 | ||||
-rw-r--r-- | src/com/android/launcher3/FastBitmapDrawable.java | 301 | ||||
-rw-r--r-- | src/com/android/launcher3/FolderIcon.java | 10 | ||||
-rw-r--r-- | src/com/android/launcher3/Launcher.java | 13 | ||||
-rw-r--r-- | src/com/android/launcher3/PendingAppWidgetHostView.java | 2 | ||||
-rw-r--r-- | src/com/android/launcher3/PreloadIconDrawable.java | 3 | ||||
-rw-r--r-- | src/com/android/launcher3/allapps/AllAppsContainerView.java | 14 | ||||
-rw-r--r-- | src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java | 228 | ||||
-rw-r--r-- | src/com/android/launcher3/allapps/AllAppsGridAdapter.java | 21 | ||||
-rw-r--r-- | src/com/android/launcher3/allapps/AllAppsRecyclerView.java | 190 | ||||
-rw-r--r-- | src/com/android/launcher3/widget/WidgetsRecyclerView.java | 23 |
15 files changed, 682 insertions, 355 deletions
diff --git a/proguard.flags b/proguard.flags index 22ffa3c8c..05963f7bc 100644 --- a/proguard.flags +++ b/proguard.flags @@ -19,11 +19,6 @@ public float getAlpha(); } --keep class com.android.launcher3.BubbleTextView { - public void setFastScrollFocus(float); - public float getFastScrollFocus(); -} - -keep class com.android.launcher3.ButtonDropTarget { public int getTextColor(); } @@ -56,8 +51,10 @@ } -keep class com.android.launcher3.FastBitmapDrawable { - public int getBrightness(); - public void setBrightness(int); + public void setDesaturation(float); + public float getDesaturation(); + public void setBrightness(float); + public float getBrightness(); } -keep class com.android.launcher3.MemoryDumpActivity { diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java index 9d713e338..f8ef1e156 100644 --- a/src/com/android/launcher3/BaseRecyclerView.java +++ b/src/com/android/launcher3/BaseRecyclerView.java @@ -52,8 +52,8 @@ public abstract class BaseRecyclerView extends RecyclerView public int rowIndex; // The offset of the first visible row public int rowTopOffset; - // The height of a given row (they are currently all the same height) - public int rowHeight; + // The adapter position of the first visible item + public int itemPos; } protected BaseRecyclerViewFastScrollBar mScrollbar; @@ -187,15 +187,21 @@ public abstract class BaseRecyclerView extends RecyclerView } /** + * Returns the visible height of the recycler view: + * VisibleHeight = View height - top padding - bottom padding + */ + protected int getVisibleHeight() { + int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; + return visibleHeight; + } + + /** * Returns the available scroll height: * AvailableScrollHeight = Total height of the all items - last page height - * - * This assumes that all rows are the same height. */ - protected int getAvailableScrollHeight(int rowCount, int rowHeight) { - int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; - int scrollHeight = getPaddingTop() + rowCount * rowHeight + getPaddingBottom(); - int availableScrollHeight = scrollHeight - visibleHeight; + protected int getAvailableScrollHeight(int rowCount) { + int totalHeight = getPaddingTop() + getTop(rowCount) + getPaddingBottom(); + int availableScrollHeight = totalHeight - getVisibleHeight(); return availableScrollHeight; } @@ -204,8 +210,7 @@ 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(); + int availableScrollBarHeight = getVisibleHeight() - mScrollbar.getThumbHeight(); return availableScrollBarHeight; } @@ -223,6 +228,13 @@ public abstract class BaseRecyclerView extends RecyclerView return defaultInactiveThumbColor; } + /** + * Returns the scrollbar for this recycler view. + */ + public BaseRecyclerViewFastScrollBar getScrollBar() { + return mScrollbar; + } + @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); @@ -243,7 +255,7 @@ public abstract class BaseRecyclerView extends RecyclerView int rowCount) { // Only show the scrollbar if there is height to be scrolled int availableScrollBarHeight = getAvailableScrollBarHeight(); - int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight); + int availableScrollHeight = getAvailableScrollHeight(rowCount); if (availableScrollHeight <= 0) { mScrollbar.setThumbOffset(-1, -1); return; @@ -252,8 +264,7 @@ public abstract class BaseRecyclerView extends RecyclerView // Calculate the current scroll position, the scrollY of the recycler view accounts for the // view padding, while the scrollBarY is drawn right up to the background padding (ignoring // padding) - int scrollY = getPaddingTop() + - (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset; + int scrollY = getScrollTop(scrollPosState); int scrollBarY = mBackgroundPadding.top + (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); @@ -268,7 +279,7 @@ public abstract class BaseRecyclerView extends RecyclerView } /** - * Returns whether fast scrolling is supported in the current state. + * @return whether fast scrolling is supported in the current state. */ protected boolean supportsFastScrolling() { return true; @@ -277,22 +288,38 @@ public abstract class BaseRecyclerView extends RecyclerView /** * Maps the touch (from 0..1) to the adapter position that should be visible. * <p>Override in each subclass of this base class. + * + * @return the scroll top of this recycler view. */ - public abstract String scrollToPositionAtProgress(float touchFraction); + protected int getScrollTop(ScrollPositionState scrollPosState) { + return getPaddingTop() + getTop(scrollPosState.rowIndex) - + scrollPosState.rowTopOffset; + } /** - * Updates the bounds for the scrollbar. + * Returns information about the item that the recycler view is currently scrolled to. + */ + protected abstract void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask); + + /** + * Returns the top (or y position) of the row at the specified index. + */ + protected abstract int getTop(int rowIndex); + + /** + * Maps the touch (from 0..1) to the adapter position that should be visible. * <p>Override in each subclass of this base class. */ - public abstract void onUpdateScrollbar(int dy); + protected abstract String scrollToPositionAtProgress(float touchFraction); /** + * Updates the bounds for the scrollbar. * <p>Override in each subclass of this base class. */ - public void onFastScrollCompleted() {} + protected abstract void onUpdateScrollbar(int dy); /** - * Returns information about the item that the recycler view is currently scrolled to. + * <p>Override in each subclass of this base class. */ - protected abstract void getCurScrollState(ScrollPositionState stateOut); + protected void onFastScrollCompleted() {} }
\ No newline at end of file diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java index 32ea57667..a6801696a 100644 --- a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java +++ b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java @@ -27,6 +27,7 @@ import android.graphics.Path; import android.graphics.Point; import android.graphics.Rect; import android.view.MotionEvent; +import android.view.VelocityTracker; import android.view.ViewConfiguration; import com.android.launcher3.util.Thunk; @@ -37,7 +38,7 @@ import com.android.launcher3.util.Thunk; public class BaseRecyclerViewFastScrollBar { public interface FastScrollFocusableView { - void setFastScrollFocused(boolean focused, boolean animated); + void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated); } private final static int MAX_TRACK_ALPHA = 30; @@ -199,7 +200,7 @@ public class BaseRecyclerViewFastScrollBar { } mTouchOffset += (lastY - downY); mPopup.animateVisibility(true); - animateScrollbar(true); + showActiveScrollbar(true); } if (mIsDragging) { // Update the fastscroller section name at this touch position @@ -210,7 +211,7 @@ public class BaseRecyclerViewFastScrollBar { (bottom - top)); mPopup.setSectionName(sectionName); mPopup.animateVisibility(!sectionName.isEmpty()); - mRv.invalidate(mPopup.updateFastScrollerBounds(mRv, lastY)); + mRv.invalidate(mPopup.updateFastScrollerBounds(lastY)); mLastTouchY = boundedY; } break; @@ -222,7 +223,7 @@ public class BaseRecyclerViewFastScrollBar { if (mIsDragging) { mIsDragging = false; mPopup.animateVisibility(false); - animateScrollbar(false); + showActiveScrollbar(false); } break; } @@ -246,7 +247,7 @@ public class BaseRecyclerViewFastScrollBar { /** * Animates the width and color of the scrollbar. */ - private void animateScrollbar(boolean isScrolling) { + private void showActiveScrollbar(boolean isScrolling) { if (mScrollbarAnimator != null) { mScrollbarAnimator.cancel(); } diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java index aeeb5156d..ebaba1880 100644 --- a/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java +++ b/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java @@ -77,26 +77,26 @@ public class BaseRecyclerViewFastScrollPopup { * Updates the bounds for the fast scroller. * @return the invalidation rect for this update. */ - public Rect updateFastScrollerBounds(BaseRecyclerView rv, int lastTouchY) { + public Rect updateFastScrollerBounds(int lastTouchY) { mInvalidateRect.set(mBgBounds); if (isVisible()) { // Calculate the dimensions and position of the fast scroller popup - int edgePadding = rv.getMaxScrollbarWidth(); + int edgePadding = mRv.getMaxScrollbarWidth(); int bgPadding = (mBgOriginalSize - mTextBounds.height()) / 2; int bgHeight = mBgOriginalSize; int bgWidth = Math.max(mBgOriginalSize, mTextBounds.width() + (2 * bgPadding)); if (Utilities.isRtl(mRes)) { - mBgBounds.left = rv.getBackgroundPadding().left + (2 * rv.getMaxScrollbarWidth()); + mBgBounds.left = mRv.getBackgroundPadding().left + (2 * mRv.getMaxScrollbarWidth()); mBgBounds.right = mBgBounds.left + bgWidth; } else { - mBgBounds.right = rv.getWidth() - rv.getBackgroundPadding().right - - (2 * rv.getMaxScrollbarWidth()); + mBgBounds.right = mRv.getWidth() - mRv.getBackgroundPadding().right - + (2 * mRv.getMaxScrollbarWidth()); mBgBounds.left = mBgBounds.right - bgWidth; } mBgBounds.top = lastTouchY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgHeight); mBgBounds.top = Math.max(edgePadding, - Math.min(mBgBounds.top, rv.getHeight() - edgePadding - bgHeight)); + Math.min(mBgBounds.top, mRv.getHeight() - edgePadding - bgHeight)); mBgBounds.bottom = mBgBounds.top + bgHeight; } else { mBgBounds.setEmpty(); diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index c8af600ba..db1e4f5c4 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -16,7 +16,6 @@ package com.android.launcher3; -import android.animation.ObjectAnimator; import android.annotation.TargetApi; import android.content.Context; import android.content.res.ColorStateList; @@ -25,7 +24,6 @@ import android.content.res.Resources.Theme; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.Paint; import android.graphics.Region; import android.graphics.drawable.Drawable; import android.os.Build; @@ -37,8 +35,6 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewParent; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.DecelerateInterpolator; import android.widget.TextView; import com.android.launcher3.IconCache.IconLoadRequest; @@ -63,13 +59,6 @@ public class BubbleTextView extends TextView private static final int DISPLAY_WORKSPACE = 0; private static final int DISPLAY_ALL_APPS = 1; - private static final float FAST_SCROLL_FOCUS_MAX_SCALE = 1.15f; - private static final int FAST_SCROLL_FOCUS_MODE_NONE = 0; - private static final int FAST_SCROLL_FOCUS_MODE_SCALE_ICON = 1; - private static final int FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG = 2; - private static final int FAST_SCROLL_FOCUS_FADE_IN_DURATION = 175; - private static final int FAST_SCROLL_FOCUS_FADE_OUT_DURATION = 125; - private final Launcher mLauncher; private Drawable mIcon; private final Drawable mBackground; @@ -93,12 +82,6 @@ public class BubbleTextView extends TextView private boolean mIgnorePressedStateChange; private boolean mDisableRelayout = false; - private ObjectAnimator mFastScrollFocusAnimator; - private Paint mFastScrollFocusBgPaint; - private float mFastScrollFocusFraction; - private boolean mFastScrollFocused; - private final int mFastScrollMode = FAST_SCROLL_FOCUS_MODE_SCALE_ICON; - private IconLoadRequest mIconLoadRequest; public BubbleTextView(Context context) { @@ -151,13 +134,6 @@ public class BubbleTextView extends TextView setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR); } - if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG) { - mFastScrollFocusBgPaint = new Paint(); - mFastScrollFocusBgPaint.setAntiAlias(true); - mFastScrollFocusBgPaint.setColor( - getResources().getColor(R.color.container_fastscroll_thumb_active_color)); - } - setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate()); } @@ -170,8 +146,9 @@ public class BubbleTextView extends TextView Bitmap b = info.getIcon(iconCache); FastBitmapDrawable iconDrawable = mLauncher.createIconDrawable(b); - iconDrawable.setGhostModeEnabled(info.isDisabled != 0); - + if (info.isDisabled != 0) { + iconDrawable.setState(FastBitmapDrawable.State.DISABLED); + } setIcon(iconDrawable, mIconSize); if (info.contentDescription != null) { setContentDescription(info.contentDescription); @@ -259,7 +236,12 @@ public class BubbleTextView extends TextView private void updateIconState() { if (mIcon instanceof FastBitmapDrawable) { - ((FastBitmapDrawable) mIcon).setPressed(isPressed() || mStayPressed); + FastBitmapDrawable d = (FastBitmapDrawable) mIcon; + if (isPressed() || mStayPressed) { + d.animateState(FastBitmapDrawable.State.PRESSED); + } else { + d.animateState(FastBitmapDrawable.State.NORMAL); + } } } @@ -362,18 +344,7 @@ public class BubbleTextView extends TextView @Override public void draw(Canvas canvas) { if (!mCustomShadowsEnabled) { - // Draw the fast scroll focus bg if we have one - if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG && - mFastScrollFocusFraction > 0f) { - DeviceProfile grid = mLauncher.getDeviceProfile(); - int iconCenterX = getScrollX() + (getWidth() / 2); - int iconCenterY = getScrollY() + getPaddingTop() + (grid.iconSizePx / 2); - canvas.drawCircle(iconCenterX, iconCenterY, - mFastScrollFocusFraction * (getWidth() / 2), mFastScrollFocusBgPaint); - } - super.draw(canvas); - return; } @@ -533,8 +504,13 @@ public class BubbleTextView extends TextView */ public void reapplyItemInfo(final ItemInfo info) { if (getTag() == info) { + FastBitmapDrawable.State prevState = FastBitmapDrawable.State.NORMAL; + if (mIcon instanceof FastBitmapDrawable) { + prevState = ((FastBitmapDrawable) mIcon).getCurrentState(); + } mIconLoadRequest = null; mDisableRelayout = true; + if (info instanceof AppInfo) { applyFromApplicationInfo((AppInfo) info); } else if (info instanceof ShortcutInfo) { @@ -550,6 +526,13 @@ public class BubbleTextView extends TextView } else if (info instanceof PackageItemInfo) { applyFromPackageItemInfo((PackageItemInfo) info); } + + // If we are reapplying over an old icon, then we should update the new icon to the same + // state as the old icon + if (mIcon instanceof FastBitmapDrawable) { + ((FastBitmapDrawable) mIcon).setState(prevState); + } + mDisableRelayout = false; } } @@ -583,55 +566,53 @@ public class BubbleTextView extends TextView } } - // Setters & getters for the animation - public void setFastScrollFocus(float fraction) { - mFastScrollFocusFraction = fraction; - if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_SCALE_ICON) { - setScaleX(1f + fraction * (FAST_SCROLL_FOCUS_MAX_SCALE - 1f)); - setScaleY(1f + fraction * (FAST_SCROLL_FOCUS_MAX_SCALE - 1f)); - } else { - invalidate(); - } - } - - public float getFastScrollFocus() { - return mFastScrollFocusFraction; - } - @Override - public void setFastScrollFocused(final boolean focused, boolean animated) { - if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_NONE) { + public void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated) { + // We can only set the fast scroll focus state on a FastBitmapDrawable + if (!(mIcon instanceof FastBitmapDrawable)) { return; } - if (mFastScrollFocused != focused) { - mFastScrollFocused = focused; + FastBitmapDrawable d = (FastBitmapDrawable) mIcon; + if (animated) { + FastBitmapDrawable.State prevState = d.getCurrentState(); + if (d.animateState(focusState)) { + // If the state was updated, then update the view accordingly + animate().scaleX(focusState.viewScale) + .scaleY(focusState.viewScale) + .setStartDelay(getStartDelayForStateChange(prevState, focusState)) + .setDuration(d.getDurationForStateChange(prevState, focusState)) + .start(); + } + } else { + if (d.setState(focusState)) { + // If the state was updated, then update the view accordingly + animate().cancel(); + setScaleX(focusState.viewScale); + setScaleY(focusState.viewScale); + } + } + } - if (animated) { - // Clean up the previous focus animator - if (mFastScrollFocusAnimator != null) { - mFastScrollFocusAnimator.cancel(); - } - mFastScrollFocusAnimator = ObjectAnimator.ofFloat(this, "fastScrollFocus", - focused ? 1f : 0f); - if (focused) { - mFastScrollFocusAnimator.setInterpolator(new DecelerateInterpolator()); - } else { - mFastScrollFocusAnimator.setInterpolator(new AccelerateInterpolator()); + /** + * Returns the start delay when animating between certain {@link FastBitmapDrawable} states. + */ + private static int getStartDelayForStateChange(final FastBitmapDrawable.State fromState, + final FastBitmapDrawable.State toState) { + switch (toState) { + case NORMAL: + switch (fromState) { + case FAST_SCROLL_HIGHLIGHTED: + return FastBitmapDrawable.FAST_SCROLL_INACTIVE_DURATION / 4; } - mFastScrollFocusAnimator.setDuration(focused ? - FAST_SCROLL_FOCUS_FADE_IN_DURATION : FAST_SCROLL_FOCUS_FADE_OUT_DURATION); - mFastScrollFocusAnimator.start(); - } else { - mFastScrollFocusFraction = focused ? 1f : 0f; - } } + return 0; } /** * Interface to be implemented by the grand parent to allow click shadow effect. */ - public static interface BubbleTextShadowHandler { + public interface BubbleTextShadowHandler { void setPressedIcon(BubbleTextView icon, Bitmap background); } } diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java index 28e923e67..30bc7eaa9 100644 --- a/src/com/android/launcher3/FastBitmapDrawable.java +++ b/src/com/android/launcher3/FastBitmapDrawable.java @@ -16,6 +16,7 @@ package com.android.launcher3; +import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.graphics.Bitmap; @@ -28,13 +29,40 @@ import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; -import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.SparseArray; +import android.view.animation.DecelerateInterpolator; public class FastBitmapDrawable extends Drawable { - static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() { + /** + * The possible states that a FastBitmapDrawable can be in. + */ + public enum State { + + NORMAL (0f, 0f, 1f, new DecelerateInterpolator()), + PRESSED (0f, 100f / 255f, 1f, CLICK_FEEDBACK_INTERPOLATOR), + FAST_SCROLL_HIGHLIGHTED (0f, 0f, 1.1f, new DecelerateInterpolator()), + FAST_SCROLL_UNHIGHLIGHTED (0.8f, 0.35f, 1f, new DecelerateInterpolator()), + DISABLED (1f, 0.5f, 1f, new DecelerateInterpolator()); + + public final float desaturation; + public final float brightness; + /** + * Used specifically by the view drawing this FastBitmapDrawable. + */ + public final float viewScale; + public final TimeInterpolator interpolator; + + State(float desaturation, float brightness, float viewScale, TimeInterpolator interpolator) { + this.desaturation = desaturation; + this.brightness = brightness; + this.viewScale = viewScale; + this.interpolator = interpolator; + } + } + + public static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() { @Override public float getInterpolation(float input) { @@ -47,42 +75,46 @@ public class FastBitmapDrawable extends Drawable { } } }; - static final long CLICK_FEEDBACK_DURATION = 2000; + public static final int CLICK_FEEDBACK_DURATION = 2000; + public static final int FAST_SCROLL_HIGHLIGHT_DURATION = 225; + public static final int FAST_SCROLL_UNHIGHLIGHT_DURATION = 150; + public static final int FAST_SCROLL_UNHIGHLIGHT_FROM_NORMAL_DURATION = 225; + public static final int FAST_SCROLL_INACTIVE_DURATION = 275; - private static final int PRESSED_BRIGHTNESS = 100; - private static ColorMatrix sGhostModeMatrix; - private static final ColorMatrix sTempMatrix = new ColorMatrix(); + // Since we don't need 256^2 values for combinations of both the brightness and saturation, we + // reduce the value space to a smaller value V, which reduces the number of cached + // ColorMatrixColorFilters that we need to keep to V^2 + private static final int REDUCED_FILTER_VALUE_SPACE = 48; - /** - * Store the brightness colors filters to optimize animations during icon press. This - * only works for non-ghost-mode icons. - */ - private static final SparseArray<ColorFilter> sCachedBrightnessFilter = - new SparseArray<ColorFilter>(); + // A cache of ColorFilters for optimizing brightness and saturation animations + private static final SparseArray<ColorFilter> sCachedFilter = new SparseArray<>(); - private static final int GHOST_MODE_MIN_COLOR_RANGE = 130; + // Temporary matrices used for calculation + private static final ColorMatrix sTempBrightnessMatrix = new ColorMatrix(); + private static final ColorMatrix sTempFilterMatrix = new ColorMatrix(); private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); private final Bitmap mBitmap; - private int mAlpha; + private State mState = State.NORMAL; + // The saturation and brightness are values that are mapped to REDUCED_FILTER_VALUE_SPACE and + // as a result, can be used to compose the key for the cached ColorMatrixColorFilters + private int mDesaturation = 0; private int mBrightness = 0; - private boolean mGhostModeEnabled = false; + private int mAlpha = 255; + private int mPrevUpdateKey = Integer.MAX_VALUE; - private boolean mPressed = false; - private ObjectAnimator mPressedAnimator; + // Animators for the fast bitmap drawable's properties + private AnimatorSet mPropertyAnimator; public FastBitmapDrawable(Bitmap b) { - mAlpha = 255; mBitmap = b; setBounds(0, 0, b.getWidth(), b.getHeight()); } @Override public void draw(Canvas canvas) { - final Rect r = getBounds(); - // Draw the bitmap into the bounding rect - canvas.drawBitmap(mBitmap, null, r, mPaint); + canvas.drawBitmap(mBitmap, null, getBounds(), mPaint); } @Override @@ -136,96 +168,191 @@ public class FastBitmapDrawable extends Drawable { } /** - * When enabled, the icon is grayed out and the contrast is increased to give it a 'ghost' - * appearance. + * Animates this drawable to a new state. + * + * @return whether the state has changed. */ - public void setGhostModeEnabled(boolean enabled) { - if (mGhostModeEnabled != enabled) { - mGhostModeEnabled = enabled; - updateFilter(); + public boolean animateState(State newState) { + State prevState = mState; + if (mState != newState) { + mState = newState; + + mPropertyAnimator = cancelAnimator(mPropertyAnimator); + mPropertyAnimator = new AnimatorSet(); + mPropertyAnimator.playTogether( + ObjectAnimator + .ofFloat(this, "desaturation", newState.desaturation), + ObjectAnimator + .ofFloat(this, "brightness", newState.brightness)); + mPropertyAnimator.setInterpolator(newState.interpolator); + mPropertyAnimator.setDuration(getDurationForStateChange(prevState, newState)); + mPropertyAnimator.setStartDelay(getStartDelayForStateChange(prevState, newState)); + mPropertyAnimator.start(); + return true; } + return false; } - public void setPressed(boolean pressed) { - if (mPressed != pressed) { - mPressed = pressed; - if (mPressed) { - mPressedAnimator = ObjectAnimator - .ofInt(this, "brightness", PRESSED_BRIGHTNESS) - .setDuration(CLICK_FEEDBACK_DURATION); - mPressedAnimator.setInterpolator(CLICK_FEEDBACK_INTERPOLATOR); - mPressedAnimator.start(); - } else if (mPressedAnimator != null) { - mPressedAnimator.cancel(); - setBrightness(0); - } + /** + * Immediately sets this drawable to a new state. + * + * @return whether the state has changed. + */ + public boolean setState(State newState) { + if (mState != newState) { + mState = newState; + + mPropertyAnimator = cancelAnimator(mPropertyAnimator); + + setDesaturation(newState.desaturation); + setBrightness(newState.brightness); + return true; } - invalidateSelf(); + return false; + } + + /** + * Returns the current state. + */ + public State getCurrentState() { + return mState; } - public boolean isGhostModeEnabled() { - return mGhostModeEnabled; + /** + * Returns the duration for the state change animation. + */ + public static int getDurationForStateChange(State fromState, State toState) { + switch (toState) { + case NORMAL: + switch (fromState) { + case PRESSED: + return 0; + case FAST_SCROLL_HIGHLIGHTED: + case FAST_SCROLL_UNHIGHLIGHTED: + return FAST_SCROLL_INACTIVE_DURATION; + } + case PRESSED: + return CLICK_FEEDBACK_DURATION; + case FAST_SCROLL_HIGHLIGHTED: + return FAST_SCROLL_HIGHLIGHT_DURATION; + case FAST_SCROLL_UNHIGHLIGHTED: + switch (fromState) { + case NORMAL: + // When animating from normal state, take a little longer + return FAST_SCROLL_UNHIGHLIGHT_FROM_NORMAL_DURATION; + default: + return FAST_SCROLL_UNHIGHLIGHT_DURATION; + } + } + return 0; } - public int getBrightness() { - return mBrightness; + /** + * Returns the start delay when animating between certain fast scroll states. + */ + public static int getStartDelayForStateChange(State fromState, State toState) { + switch (toState) { + case FAST_SCROLL_UNHIGHLIGHTED: + switch (fromState) { + case NORMAL: + return FAST_SCROLL_UNHIGHLIGHT_DURATION / 4; + } + } + return 0; } - public void setBrightness(int brightness) { - if (mBrightness != brightness) { - mBrightness = brightness; + /** + * Sets the saturation of this icon, 0 [full color] -> 1 [desaturated] + */ + public void setDesaturation(float desaturation) { + int newDesaturation = (int) Math.floor(desaturation * REDUCED_FILTER_VALUE_SPACE); + if (mDesaturation != newDesaturation) { + mDesaturation = newDesaturation; + updateFilter(); + } + } + + public float getDesaturation() { + return (float) mDesaturation / REDUCED_FILTER_VALUE_SPACE; + } + + /** + * Sets the brightness of this icon, 0 [no add. brightness] -> 1 [2bright2furious] + */ + public void setBrightness(float brightness) { + int newBrightness = (int) Math.floor(brightness * REDUCED_FILTER_VALUE_SPACE); + if (mBrightness != newBrightness) { + mBrightness = newBrightness; updateFilter(); - invalidateSelf(); } } + public float getBrightness() { + return (float) mBrightness / REDUCED_FILTER_VALUE_SPACE; + } + + /** + * Updates the paint to reflect the current brightness and saturation. + */ private void updateFilter() { - if (mGhostModeEnabled) { - if (sGhostModeMatrix == null) { - sGhostModeMatrix = new ColorMatrix(); - sGhostModeMatrix.setSaturation(0); - - // For ghost mode, set the color range to [GHOST_MODE_MIN_COLOR_RANGE, 255] - float range = (255 - GHOST_MODE_MIN_COLOR_RANGE) / 255.0f; - sTempMatrix.set(new float[] { - range, 0, 0, 0, GHOST_MODE_MIN_COLOR_RANGE, - 0, range, 0, 0, GHOST_MODE_MIN_COLOR_RANGE, - 0, 0, range, 0, GHOST_MODE_MIN_COLOR_RANGE, - 0, 0, 0, 1, 0 }); - sGhostModeMatrix.preConcat(sTempMatrix); - } + boolean usePorterDuffFilter = false; + int key = -1; + if (mDesaturation > 0) { + key = (mDesaturation << 16) | mBrightness; + } else if (mBrightness > 0) { + // Compose a key with a fully saturated icon if we are just animating brightness + key = (1 << 16) | mBrightness; + + // We found that in L, ColorFilters cause drawing artifacts with shadows baked into + // icons, so just use a PorterDuff filter when we aren't animating saturation + usePorterDuffFilter = true; + } - if (mBrightness == 0) { - mPaint.setColorFilter(new ColorMatrixColorFilter(sGhostModeMatrix)); - } else { - setBrightnessMatrix(sTempMatrix, mBrightness); - sTempMatrix.postConcat(sGhostModeMatrix); - mPaint.setColorFilter(new ColorMatrixColorFilter(sTempMatrix)); - } - } else if (mBrightness != 0) { - ColorFilter filter = sCachedBrightnessFilter.get(mBrightness); + // Debounce multiple updates on the same frame + if (key == mPrevUpdateKey) { + return; + } + mPrevUpdateKey = key; + + if (key != -1) { + ColorFilter filter = sCachedFilter.get(key); if (filter == null) { - filter = new PorterDuffColorFilter(Color.argb(mBrightness, 255, 255, 255), - PorterDuff.Mode.SRC_ATOP); - sCachedBrightnessFilter.put(mBrightness, filter); + float brightnessF = getBrightness(); + int brightnessI = (int) (255 * brightnessF); + if (usePorterDuffFilter) { + filter = new PorterDuffColorFilter(Color.argb(brightnessI, 255, 255, 255), + PorterDuff.Mode.SRC_ATOP); + } else { + float saturationF = 1f - getDesaturation(); + sTempFilterMatrix.setSaturation(saturationF); + if (mBrightness > 0) { + // Brightness: C-new = C-old*(1-amount) + amount + float scale = 1f - brightnessF; + float[] mat = sTempBrightnessMatrix.getArray(); + mat[0] = scale; + mat[6] = scale; + mat[12] = scale; + mat[4] = brightnessI; + mat[9] = brightnessI; + mat[14] = brightnessI; + sTempFilterMatrix.preConcat(sTempBrightnessMatrix); + } + filter = new ColorMatrixColorFilter(sTempFilterMatrix); + } + sCachedFilter.append(key, filter); } mPaint.setColorFilter(filter); } else { mPaint.setColorFilter(null); } + invalidateSelf(); } - private static void setBrightnessMatrix(ColorMatrix matrix, int brightness) { - // Brightness: C-new = C-old*(1-amount) + amount - float scale = 1 - brightness / 255.0f; - matrix.setScale(scale, scale, scale, 1); - float[] array = matrix.getArray(); - - // Add the amount to RGB components of the matrix, as per the above formula. - // Fifth elements in the array correspond to the constant being added to - // red, blue, green, and alpha channel respectively. - array[4] = brightness; - array[9] = brightness; - array[14] = brightness; + private AnimatorSet cancelAnimator(AnimatorSet animator) { + if (animator != null) { + animator.removeAllListeners(); + animator.cancel(); + } + return null; } } diff --git a/src/com/android/launcher3/FolderIcon.java b/src/com/android/launcher3/FolderIcon.java index bd61a6de5..07bd0aafa 100644 --- a/src/com/android/launcher3/FolderIcon.java +++ b/src/com/android/launcher3/FolderIcon.java @@ -521,7 +521,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { } class PreviewItemDrawingParams { - PreviewItemDrawingParams(float transX, float transY, float scale, int overlayAlpha) { + PreviewItemDrawingParams(float transX, float transY, float scale, float overlayAlpha) { this.transX = transX; this.transY = transY; this.scale = scale; @@ -530,7 +530,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { float transX; float transY; float scale; - int overlayAlpha; + float overlayAlpha; Drawable drawable; } @@ -562,7 +562,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { float transY = mAvailableSpaceInPreview - (offset + scaledSize + scaleOffsetCorrection) + getPaddingTop(); float transX = (mAvailableSpaceInPreview - scaledSize) / 2; float totalScale = mBaselineIconScale * scale; - final int overlayAlpha = (int) (80 * (1 - r)); + final float overlayAlpha = (80 * (1 - r)) / 255f; if (params == null) { params = new PreviewItemDrawingParams(transX, transY, totalScale, overlayAlpha); @@ -586,12 +586,12 @@ public class FolderIcon extends FrameLayout implements FolderListener { d.setBounds(0, 0, mIntrinsicIconSize, mIntrinsicIconSize); if (d instanceof FastBitmapDrawable) { FastBitmapDrawable fd = (FastBitmapDrawable) d; - int oldBrightness = fd.getBrightness(); + float oldBrightness = fd.getBrightness(); fd.setBrightness(params.overlayAlpha); d.draw(canvas); fd.setBrightness(oldBrightness); } else { - d.setColorFilter(Color.argb(params.overlayAlpha, 255, 255, 255), + d.setColorFilter(Color.argb((int) (params.overlayAlpha * 255), 255, 255, 255), PorterDuff.Mode.SRC_ATOP); d.draw(canvas); d.clearColorFilter(); diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index a4f684f02..143d19b9b 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -46,6 +46,7 @@ import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.database.sqlite.SQLiteDatabase; import android.graphics.Bitmap; @@ -4573,6 +4574,18 @@ public class Launcher extends Activity UserHandleCompat.myUserHandle()); } + /** + * Generates a dummy AppInfo for us to use to calculate BubbleTextView sizes. + */ + public AppInfo createDummyAppInfo() { + Intent intent = new Intent(); + intent.setComponent(new ComponentName(this, Launcher.class)); + PackageManager pm = getPackageManager(); + ResolveInfo info = pm.resolveActivity(intent, 0); + return new AppInfo(this, LauncherActivityInfoCompat.fromResolveInfo(info, this), + UserHandleCompat.myUserHandle(), mIconCache); + } + // TODO: This method should be a part of LauncherSearchCallback public void startDrag(View dragView, ItemInfo dragInfo, DragSource source) { dragView.setTag(dragInfo); diff --git a/src/com/android/launcher3/PendingAppWidgetHostView.java b/src/com/android/launcher3/PendingAppWidgetHostView.java index 40eadabd5..1d7694598 100644 --- a/src/com/android/launcher3/PendingAppWidgetHostView.java +++ b/src/com/android/launcher3/PendingAppWidgetHostView.java @@ -133,7 +133,7 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen // 3) Setup icon in the center and app icon in the top right corner. if (mDisabledForSafeMode) { FastBitmapDrawable disabledIcon = mLauncher.createIconDrawable(mIcon); - disabledIcon.setGhostModeEnabled(true); + disabledIcon.setState(FastBitmapDrawable.State.DISABLED); mCenterDrawable = disabledIcon; mSettingIconDrawable = null; } else if (isReadyForClickSetup()) { diff --git a/src/com/android/launcher3/PreloadIconDrawable.java b/src/com/android/launcher3/PreloadIconDrawable.java index 45e4b2c4a..908c8b9e2 100644 --- a/src/com/android/launcher3/PreloadIconDrawable.java +++ b/src/com/android/launcher3/PreloadIconDrawable.java @@ -179,7 +179,8 @@ class PreloadIconDrawable extends Drawable { mPaint.setColor(getIndicatorColor()); } if (mIcon instanceof FastBitmapDrawable) { - ((FastBitmapDrawable) mIcon).setGhostModeEnabled(level <= 0); + ((FastBitmapDrawable) mIcon).setState(level <= 0 ? + FastBitmapDrawable.State.DISABLED : FastBitmapDrawable.State.NORMAL); } invalidateSelf(); diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java index 979a7332b..c26463e5b 100644 --- a/src/com/android/launcher3/allapps/AllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java @@ -28,6 +28,7 @@ import android.text.SpannableStringBuilder; import android.text.method.TextKeyListener; import android.util.AttributeSet; import android.view.KeyEvent; +import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; @@ -35,6 +36,7 @@ import android.view.ViewGroup; import android.widget.LinearLayout; import com.android.launcher3.AppInfo; import com.android.launcher3.BaseContainerView; +import com.android.launcher3.BubbleTextView; import com.android.launcher3.CellLayout; import com.android.launcher3.DeleteDropTarget; import com.android.launcher3.DeviceProfile; @@ -332,6 +334,18 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc mAppsRecyclerView.addItemDecoration(mItemDecoration); } + // Precalculate the prediction icon and normal icon sizes + LayoutInflater layoutInflater = LayoutInflater.from(getContext()); + BubbleTextView icon = (BubbleTextView) layoutInflater.inflate(R.layout.all_apps_icon, this, false); + icon.applyFromApplicationInfo(mLauncher.createDummyAppInfo()); + icon.measure(MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE, MeasureSpec.AT_MOST)); + BubbleTextView predIcon = (BubbleTextView) layoutInflater.inflate(R.layout.all_apps_prediction_bar_icon, this, false); + predIcon.applyFromApplicationInfo(mLauncher.createDummyAppInfo()); + predIcon.measure(MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE, MeasureSpec.AT_MOST)); + mAppsRecyclerView.setPremeasuredIconHeights(predIcon.getMeasuredHeight(), icon.getMeasuredHeight()); + updateBackgroundAndPaddings(); } diff --git a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java new file mode 100644 index 000000000..10453420a --- /dev/null +++ b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.allapps; + +import android.support.v7.widget.RecyclerView; +import android.view.View; + +import com.android.launcher3.BaseRecyclerView; +import com.android.launcher3.BaseRecyclerViewFastScrollBar; +import com.android.launcher3.FastBitmapDrawable; +import com.android.launcher3.util.Thunk; + +import java.util.HashSet; + +public class AllAppsFastScrollHelper implements AllAppsGridAdapter.BindViewCallback { + + private static final int INITIAL_TOUCH_SETTLING_DURATION = 300; + private static final int REPEAT_TOUCH_SETTLING_DURATION = 200; + private static final float FAST_SCROLL_TOUCH_VELOCITY_BARRIER = 1900f; + + private AllAppsRecyclerView mRv; + private AlphabeticalAppsList mApps; + + // Keeps track of the current and targetted fast scroll section (the section to scroll to after + // the initial delay) + int mTargetFastScrollPosition = -1; + @Thunk String mCurrentFastScrollSection; + @Thunk String mTargetFastScrollSection; + + // The settled states affect the delay before the fast scroll animation is applied + private boolean mHasFastScrollTouchSettled; + private boolean mHasFastScrollTouchSettledAtLeastOnce; + + // Set of all views animated during fast scroll. We keep track of these ourselves since there + // is no way to reset a view once it gets scrapped or recycled without other hacks + private HashSet<BaseRecyclerViewFastScrollBar.FastScrollFocusableView> mTrackedFastScrollViews = + new HashSet<>(); + + // Smooth fast-scroll animation frames + @Thunk int mFastScrollFrameIndex; + @Thunk final int[] mFastScrollFrames = new int[10]; + + /** + * This runnable runs a single frame of the smooth scroll animation and posts the next frame + * if necessary. + */ + @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() { + @Override + public void run() { + if (mFastScrollFrameIndex < mFastScrollFrames.length) { + mRv.scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]); + mFastScrollFrameIndex++; + mRv.postOnAnimation(mSmoothSnapNextFrameRunnable); + } + } + }; + + /** + * This runnable updates the current fast scroll section to the target fastscroll section. + */ + Runnable mFastScrollToTargetSectionRunnable = new Runnable() { + @Override + public void run() { + // Update to the target section + mCurrentFastScrollSection = mTargetFastScrollSection; + mHasFastScrollTouchSettled = true; + mHasFastScrollTouchSettledAtLeastOnce = true; + updateTrackedViewsFastScrollFocusState(); + } + }; + + public AllAppsFastScrollHelper(AllAppsRecyclerView rv, AlphabeticalAppsList apps) { + mRv = rv; + mApps = apps; + } + + public void onSetAdapter(AllAppsGridAdapter adapter) { + adapter.setBindViewCallback(this); + } + + /** + * Smooth scrolls the recycler view to the given section. + * + * @return whether the fastscroller can scroll to the new section. + */ + public boolean smoothScrollToSection(int scrollY, int availableScrollHeight, + AlphabeticalAppsList.FastScrollSectionInfo info) { + if (mTargetFastScrollPosition != info.fastScrollToItem.position) { + mTargetFastScrollPosition = info.fastScrollToItem.position; + smoothSnapToPosition(scrollY, availableScrollHeight, info); + return true; + } + return false; + } + + /** + * Smoothly snaps to a given position. We do this manually by calculating the keyframes + * ourselves and animating the scroll on the recycler view. + */ + private void smoothSnapToPosition(int scrollY, int availableScrollHeight, + AlphabeticalAppsList.FastScrollSectionInfo info) { + mRv.removeCallbacks(mSmoothSnapNextFrameRunnable); + mRv.removeCallbacks(mFastScrollToTargetSectionRunnable); + + trackAllChildViews(); + if (mHasFastScrollTouchSettled) { + // In this case, the user has already settled once (and the fast scroll state has + // animated) and they are just fine-tuning their section from the last section, so + // we should make it feel fast and update immediately. + mCurrentFastScrollSection = info.sectionName; + mTargetFastScrollSection = null; + updateTrackedViewsFastScrollFocusState(); + } else { + // Otherwise, the user has scrubbed really far, and we don't want to distract the user + // with the flashing fast scroll state change animation in addition to the fast scroll + // section popup, so reset the views to normal, and wait for the touch to settle again + // before animating the fast scroll state. + mCurrentFastScrollSection = null; + mTargetFastScrollSection = info.sectionName; + mHasFastScrollTouchSettled = false; + updateTrackedViewsFastScrollFocusState(); + + // Delay scrolling to a new section until after some duration. If the user has been + // scrubbing a while and makes multiple big jumps, then reduce the time needed for the + // fast scroll to settle so it doesn't feel so long. + mRv.postDelayed(mFastScrollToTargetSectionRunnable, + mHasFastScrollTouchSettledAtLeastOnce ? + REPEAT_TOUCH_SETTLING_DURATION : + INITIAL_TOUCH_SETTLING_DURATION); + } + + // Calculate the full animation from the current scroll position to the final scroll + // position, and then run the animation for the duration. + int newScrollY = Math.min(availableScrollHeight, + mRv.getPaddingTop() + mRv.getTop(info.fastScrollToItem.rowIndex)); + int numFrames = mFastScrollFrames.length; + for (int i = 0; i < numFrames; i++) { + // TODO(winsonc): We can interpolate this as well. + mFastScrollFrames[i] = (newScrollY - scrollY) / numFrames; + } + mFastScrollFrameIndex = 0; + mRv.postOnAnimation(mSmoothSnapNextFrameRunnable); + } + + public void onFastScrollCompleted() { + // TODO(winsonc): Handle the case when the user scrolls and releases before the animation + // runs + + // Stop animating the fast scroll position and state + mRv.removeCallbacks(mSmoothSnapNextFrameRunnable); + mRv.removeCallbacks(mFastScrollToTargetSectionRunnable); + + // Reset the tracking variables + mHasFastScrollTouchSettled = false; + mHasFastScrollTouchSettledAtLeastOnce = false; + mCurrentFastScrollSection = null; + mTargetFastScrollSection = null; + mTargetFastScrollPosition = -1; + + updateTrackedViewsFastScrollFocusState(); + mTrackedFastScrollViews.clear(); + } + + @Override + public void onBindView(AllAppsGridAdapter.ViewHolder holder) { + // Update newly bound views to the current fast scroll state if we are fast scrolling + if (mCurrentFastScrollSection != null || mTargetFastScrollSection != null) { + if (holder.mContent instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) { + BaseRecyclerViewFastScrollBar.FastScrollFocusableView v = + (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) holder.mContent; + updateViewFastScrollFocusState(v, holder.getPosition(), false /* animated */); + mTrackedFastScrollViews.add(v); + } + } + } + + /** + * Starts tracking all the recycler view's children which are FastScrollFocusableViews. + */ + private void trackAllChildViews() { + int childCount = mRv.getChildCount(); + for (int i = 0; i < childCount; i++) { + View v = mRv.getChildAt(i); + if (v instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) { + mTrackedFastScrollViews.add((BaseRecyclerViewFastScrollBar.FastScrollFocusableView) v); + } + } + } + + /** + * Updates the fast scroll focus on all the children. + */ + private void updateTrackedViewsFastScrollFocusState() { + for (BaseRecyclerViewFastScrollBar.FastScrollFocusableView v : mTrackedFastScrollViews) { + RecyclerView.ViewHolder viewHolder = mRv.getChildViewHolder((View) v); + int pos = (viewHolder != null) ? viewHolder.getPosition() : -1; + updateViewFastScrollFocusState(v, pos, true); + } + } + + /** + * Updates the fast scroll focus on all a given view. + */ + private void updateViewFastScrollFocusState(BaseRecyclerViewFastScrollBar.FastScrollFocusableView v, + int pos, boolean animated) { + FastBitmapDrawable.State newState = FastBitmapDrawable.State.NORMAL; + if (mCurrentFastScrollSection != null && pos > -1) { + AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(pos); + newState = item.sectionName.equals(mCurrentFastScrollSection) ? + FastBitmapDrawable.State.FAST_SCROLL_HIGHLIGHTED : + FastBitmapDrawable.State.FAST_SCROLL_UNHIGHLIGHTED; + } + v.setFastScrollFocusState(newState, animated); + } +} diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java index f885567ac..b7fa43d44 100644 --- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java +++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java @@ -69,6 +69,10 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. // The message to continue to a market search when there are no filtered results public static final int SEARCH_MARKET_VIEW_TYPE = 5; + public interface BindViewCallback { + public void onBindView(ViewHolder holder); + } + /** * ViewHolder for each icon. */ @@ -328,6 +332,7 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. private View.OnTouchListener mTouchListener; private View.OnClickListener mIconClickListener; private View.OnLongClickListener mIconLongClickListener; + private BindViewCallback mBindViewCallback; @Thunk final Rect mBackgroundPadding = new Rect(); @Thunk int mPredictionBarDividerOffset; @Thunk int mAppsPerRow; @@ -424,6 +429,13 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. } /** + * Sets the callback for when views are bound. + */ + public void setBindViewCallback(BindViewCallback cb) { + mBindViewCallback = cb; + } + + /** * Notifies the adapter of the background padding so that it can draw things correctly in the * item decorator. */ @@ -528,6 +540,15 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. } break; } + if (mBindViewCallback != null) { + mBindViewCallback.onBindView(holder); + } + } + + @Override + public boolean onFailedToRecycleView(ViewHolder holder) { + // Always recycle and we will reset the view when it is bound + return true; } @Override diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java index 10d10f11b..48b9494b7 100644 --- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java @@ -15,24 +15,20 @@ */ package com.android.launcher3.allapps; -import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.view.View; import com.android.launcher3.BaseRecyclerView; -import com.android.launcher3.BaseRecyclerViewFastScrollBar; import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.Stats; import com.android.launcher3.Utilities; -import com.android.launcher3.util.Thunk; import java.util.List; @@ -42,25 +38,17 @@ import java.util.List; public class AllAppsRecyclerView extends BaseRecyclerView implements Stats.LaunchSourceProvider { - private static final int FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON = 0; - private static final int FAST_SCROLL_MODE_FREE_SCROLL = 1; - - private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW = 0; - private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS = 1; - private AlphabeticalAppsList mApps; + private AllAppsFastScrollHelper mFastScrollHelper; + private BaseRecyclerView.ScrollPositionState mScrollPosState = + new BaseRecyclerView.ScrollPositionState(); private int mNumAppsPerRow; - @Thunk BaseRecyclerViewFastScrollBar.FastScrollFocusableView mLastFastScrollFocusedView; - @Thunk int mPrevFastScrollFocusedPosition; - @Thunk int mFastScrollFrameIndex; - @Thunk final int[] mFastScrollFrames = new int[10]; - - private final int mFastScrollMode = FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON; - private final int mScrollBarMode = FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW; - - private ScrollPositionState mScrollPosState = new ScrollPositionState(); + // The specific icon heights that we use to calculate scroll + private int mPredictionIconHeight; + private int mIconHeight; + // The empty-search result background private AllAppsBackgroundDrawable mEmptySearchBackground; private int mEmptySearchBackgroundTopOffset; @@ -79,8 +67,8 @@ public class AllAppsRecyclerView extends BaseRecyclerView public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr); - Resources res = getResources(); + addOnItemTouchListener(this); mScrollbar.setDetachThumbOnFastScroll(); mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize( R.dimen.all_apps_empty_search_bg_top_offset); @@ -91,6 +79,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView */ public void setApps(AlphabeticalAppsList apps) { mApps = apps; + mFastScrollHelper = new AllAppsFastScrollHelper(this, apps); } /** @@ -110,6 +99,14 @@ public class AllAppsRecyclerView extends BaseRecyclerView } /** + * Sets the heights of the icons in this view (for scroll calculations). + */ + public void setPremeasuredIconHeights(int predictionIconHeight, int iconHeight) { + mPredictionIconHeight = predictionIconHeight; + mIconHeight = iconHeight; + } + + /** * Scrolls this recycler view to the top. */ public void scrollToTop() { @@ -126,6 +123,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView */ @Override protected void dispatchDraw(Canvas canvas) { + // Clip to ensure that we don't draw the overscroll effect beyond the background bounds canvas.clipRect(mBackgroundPadding.left, mBackgroundPadding.top, getWidth() - mBackgroundPadding.right, getHeight() - mBackgroundPadding.bottom); @@ -157,14 +155,6 @@ public class AllAppsRecyclerView extends BaseRecyclerView } @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - // Bind event handlers - addOnItemTouchListener(this); - } - - @Override public void fillInLaunchSourceData(Bundle sourceData) { sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_ALL_APPS); if (mApps.hasFilter()) { @@ -212,63 +202,31 @@ public class AllAppsRecyclerView extends BaseRecyclerView List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections = mApps.getFastScrollerSections(); AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0); - if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW) { - for (int i = 1; i < fastScrollSections.size(); i++) { - AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i); - if (info.touchFraction > touchFraction) { - break; - } - lastInfo = info; + for (int i = 1; i < fastScrollSections.size(); i++) { + AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i); + if (info.touchFraction > touchFraction) { + break; } - } else if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS){ - lastInfo = fastScrollSections.get((int) (touchFraction * (fastScrollSections.size() - 1))); - } else { - throw new RuntimeException("Unexpected scroll bar mode"); + lastInfo = info; } - // Map the touch position back to the scroll of the recycler view - getCurScrollState(mScrollPosState); - int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight); - LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager(); - if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) { - layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); - } - - if (mPrevFastScrollFocusedPosition != lastInfo.fastScrollToItem.position) { - mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position; - - // Reset the last focused view - if (mLastFastScrollFocusedView != null) { - mLastFastScrollFocusedView.setFastScrollFocused(false, true); - mLastFastScrollFocusedView = null; - } - - 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); - } - } else { - throw new RuntimeException("Unexpected fast scroll mode"); - } - } + // Update the fast scroll + int scrollY = getScrollTop(mScrollPosState); + int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows()); + mFastScrollHelper.smoothScrollToSection(scrollY, availableScrollHeight, lastInfo); return lastInfo.sectionName; } @Override public void onFastScrollCompleted() { super.onFastScrollCompleted(); - // Reset and clean up the last focused view - if (mLastFastScrollFocusedView != null) { - mLastFastScrollFocusedView.setFastScrollFocused(false, true); - mLastFastScrollFocusedView = null; - } - mPrevFastScrollFocusedPosition = -1; + mFastScrollHelper.onFastScrollCompleted(); + } + + @Override + public void setAdapter(Adapter adapter) { + super.setAdapter(adapter); + mFastScrollHelper.onSetAdapter((AllAppsGridAdapter) adapter); } /** @@ -286,7 +244,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView // Find the index and height of the first visible row (all rows have the same height) int rowCount = mApps.getNumAppRows(); - getCurScrollState(mScrollPosState); + getCurScrollState(mScrollPosState, -1); if (mScrollPosState.rowIndex < 0) { mScrollbar.setThumbOffset(-1, -1); return; @@ -294,7 +252,7 @@ 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()); if (availableScrollHeight <= 0) { mScrollbar.setThumbOffset(-1, -1); return; @@ -303,8 +261,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView // Calculate the current scroll position, the scrollY of the recycler view accounts for the // view padding, while the scrollBarY is drawn right up to the background padding (ignoring // padding) - int scrollY = getPaddingTop() + - (mScrollPosState.rowIndex * mScrollPosState.rowHeight) - mScrollPosState.rowTopOffset; + int scrollY = getScrollTop(mScrollPosState); int scrollBarY = mBackgroundPadding.top + (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); @@ -355,58 +312,12 @@ public class AllAppsRecyclerView extends BaseRecyclerView } /** - * This runnable runs a single frame of the smooth scroll animation and posts the next frame - * if necessary. - */ - @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() { - @Override - public void run() { - if (mFastScrollFrameIndex < mFastScrollFrames.length) { - 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); - } - } - } - }; - - /** - * Smoothly snaps to a given position. We do this manually by calculating the keyframes - * ourselves and animating the scroll on the recycler view. - */ - private void smoothSnapToPosition(final int position, ScrollPositionState scrollPosState) { - removeCallbacks(mSmoothSnapNextFrameRunnable); - - // Calculate the full animation from the current scroll position to the final scroll - // position, and then run the animation for the duration. - int curScrollY = getPaddingTop() + - (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset; - int newScrollY = getScrollAtPosition(position, scrollPosState.rowHeight); - int numFrames = mFastScrollFrames.length; - for (int i = 0; i < numFrames; i++) { - // TODO(winsonc): We can interpolate this as well. - mFastScrollFrames[i] = (newScrollY - curScrollY) / numFrames; - } - mFastScrollFrameIndex = 0; - postOnAnimation(mSmoothSnapNextFrameRunnable); - } - - /** * Returns the current scroll state of the apps rows. */ - protected void getCurScrollState(ScrollPositionState stateOut) { + protected void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask) { stateOut.rowIndex = -1; stateOut.rowTopOffset = -1; - stateOut.rowHeight = -1; + stateOut.itemPos = -1; // Return early if there are no items or we haven't been measured List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); @@ -420,15 +331,15 @@ public class AllAppsRecyclerView extends BaseRecyclerView int position = getChildPosition(child); if (position != NO_POSITION) { AlphabeticalAppsList.AdapterItem item = items.get(position); - if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE || - item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { + if ((item.viewType & viewTypeMask) != 0) { stateOut.rowIndex = item.rowIndex; stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child); - stateOut.rowHeight = child.getHeight(); - break; + stateOut.itemPos = position; + return; } } } + return; } @Override @@ -438,18 +349,13 @@ public class AllAppsRecyclerView extends BaseRecyclerView return !mApps.hasFilter(); } - /** - * Returns the scrollY for the given position in the adapter. - */ - private int getScrollAtPosition(int position, int rowHeight) { - AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); - if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE || - item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { - int offset = item.rowIndex > 0 ? getPaddingTop() : 0; - return offset + item.rowIndex * rowHeight; - } else { + protected int getTop(int rowIndex) { + if (getChildCount() == 0 || rowIndex <= 0) { return 0; } + + // The prediction bar icons have more padding, so account for that in the row offset + return mPredictionIconHeight + (rowIndex - 1) * mIconHeight; } /** diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/WidgetsRecyclerView.java index 884bdc418..fe9c51c44 100644 --- a/src/com/android/launcher3/widget/WidgetsRecyclerView.java +++ b/src/com/android/launcher3/widget/WidgetsRecyclerView.java @@ -102,9 +102,9 @@ public class WidgetsRecyclerView extends BaseRecyclerView { // Stop the scroller if it is scrolling stopScroll(); - getCurScrollState(mScrollPosState); + getCurScrollState(mScrollPosState, -1); float pos = rowCount * touchFraction; - int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight); + int availableScrollHeight = getAvailableScrollHeight(rowCount); LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager()); layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); @@ -131,7 +131,7 @@ public class WidgetsRecyclerView extends BaseRecyclerView { } // Skip early if, there no child laid out in the container. - getCurScrollState(mScrollPosState); + getCurScrollState(mScrollPosState, -1); if (mScrollPosState.rowIndex < 0) { mScrollbar.setThumbOffset(-1, -1); return; @@ -143,10 +143,10 @@ public class WidgetsRecyclerView extends BaseRecyclerView { /** * Returns the current scroll state. */ - protected void getCurScrollState(ScrollPositionState stateOut) { + protected void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask) { stateOut.rowIndex = -1; stateOut.rowTopOffset = -1; - stateOut.rowHeight = -1; + stateOut.itemPos = -1; // Skip early if widgets are not bound. if (mWidgets == null) { @@ -163,6 +163,17 @@ public class WidgetsRecyclerView extends BaseRecyclerView { stateOut.rowIndex = position; stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child); - stateOut.rowHeight = child.getHeight(); + stateOut.itemPos = position; + } + + @Override + protected int getTop(int rowIndex) { + if (getChildCount() == 0) { + return 0; + } + + // All the rows are the same height, return any child height + View child = getChildAt(0); + return child.getMeasuredHeight() * rowIndex; } }
\ No newline at end of file |