am c0880491: Highlighting sectioned apps on fast-scroll.

* commit 'c088049113c261331b5685e64050d14a31cd72df':
  Highlighting sectioned apps on fast-scroll.
This commit is contained in:
Winson
2015-09-29 17:09:03 +00:00
committed by Android Git Automerger
15 changed files with 688 additions and 361 deletions

View File

@@ -19,11 +19,6 @@
public float getAlpha(); public float getAlpha();
} }
-keep class com.android.launcher3.BubbleTextView {
public void setFastScrollFocus(float);
public float getFastScrollFocus();
}
-keep class com.android.launcher3.ButtonDropTarget { -keep class com.android.launcher3.ButtonDropTarget {
public int getTextColor(); public int getTextColor();
} }
@@ -56,8 +51,10 @@
} }
-keep class com.android.launcher3.FastBitmapDrawable { -keep class com.android.launcher3.FastBitmapDrawable {
public int getBrightness(); public void setDesaturation(float);
public void setBrightness(int); public float getDesaturation();
public void setBrightness(float);
public float getBrightness();
} }
-keep class com.android.launcher3.MemoryDumpActivity { -keep class com.android.launcher3.MemoryDumpActivity {

View File

@@ -52,8 +52,8 @@ public abstract class BaseRecyclerView extends RecyclerView
public int rowIndex; public int rowIndex;
// The offset of the first visible row // The offset of the first visible row
public int rowTopOffset; public int rowTopOffset;
// The height of a given row (they are currently all the same height) // The adapter position of the first visible item
public int rowHeight; public int itemPos;
} }
protected BaseRecyclerViewFastScrollBar mScrollbar; protected BaseRecyclerViewFastScrollBar mScrollbar;
@@ -186,16 +186,22 @@ public abstract class BaseRecyclerView extends RecyclerView
return mScrollbar.getThumbMaxWidth(); return mScrollbar.getThumbMaxWidth();
} }
/**
* 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: * Returns the available scroll height:
* AvailableScrollHeight = Total height of the all items - last page 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) { protected int getAvailableScrollHeight(int rowCount) {
int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; int totalHeight = getPaddingTop() + getTop(rowCount) + getPaddingBottom();
int scrollHeight = getPaddingTop() + rowCount * rowHeight + getPaddingBottom(); int availableScrollHeight = totalHeight - getVisibleHeight();
int availableScrollHeight = scrollHeight - visibleHeight;
return availableScrollHeight; return availableScrollHeight;
} }
@@ -204,8 +210,7 @@ public abstract class BaseRecyclerView extends RecyclerView
* AvailableScrollBarHeight = Total height of the visible view - thumb height * AvailableScrollBarHeight = Total height of the visible view - thumb height
*/ */
protected int getAvailableScrollBarHeight() { protected int getAvailableScrollBarHeight() {
int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; int availableScrollBarHeight = getVisibleHeight() - mScrollbar.getThumbHeight();
int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight();
return availableScrollBarHeight; return availableScrollBarHeight;
} }
@@ -223,6 +228,13 @@ public abstract class BaseRecyclerView extends RecyclerView
return defaultInactiveThumbColor; return defaultInactiveThumbColor;
} }
/**
* Returns the scrollbar for this recycler view.
*/
public BaseRecyclerViewFastScrollBar getScrollBar() {
return mScrollbar;
}
@Override @Override
protected void dispatchDraw(Canvas canvas) { protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas); super.dispatchDraw(canvas);
@@ -243,7 +255,7 @@ public abstract class BaseRecyclerView extends RecyclerView
int rowCount) { int rowCount) {
// Only show the scrollbar if there is height to be scrolled // Only show the scrollbar if there is height to be scrolled
int availableScrollBarHeight = getAvailableScrollBarHeight(); int availableScrollBarHeight = getAvailableScrollBarHeight();
int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight); int availableScrollHeight = getAvailableScrollHeight(rowCount);
if (availableScrollHeight <= 0) { if (availableScrollHeight <= 0) {
mScrollbar.setThumbOffset(-1, -1); mScrollbar.setThumbOffset(-1, -1);
return; 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 // 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 // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
// padding) // padding)
int scrollY = getPaddingTop() + int scrollY = getScrollTop(scrollPosState);
(scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset;
int scrollBarY = mBackgroundPadding.top + int scrollBarY = mBackgroundPadding.top +
(int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); (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() { protected boolean supportsFastScrolling() {
return true; 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. * Maps the touch (from 0..1) to the adapter position that should be visible.
* <p>Override in each subclass of this base class. * <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;
}
/**
* 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.
*/
protected abstract String scrollToPositionAtProgress(float touchFraction);
/** /**
* Updates the bounds for the scrollbar. * Updates the bounds for the scrollbar.
* <p>Override in each subclass of this base class. * <p>Override in each subclass of this base class.
*/ */
public abstract void onUpdateScrollbar(int dy); protected abstract void onUpdateScrollbar(int dy);
/** /**
* <p>Override in each subclass of this base class. * <p>Override in each subclass of this base class.
*/ */
public void onFastScrollCompleted() {} protected void onFastScrollCompleted() {}
/**
* Returns information about the item that the recycler view is currently scrolled to.
*/
protected abstract void getCurScrollState(ScrollPositionState stateOut);
} }

View File

@@ -27,6 +27,7 @@ import android.graphics.Path;
import android.graphics.Point; import android.graphics.Point;
import android.graphics.Rect; import android.graphics.Rect;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewConfiguration; import android.view.ViewConfiguration;
import com.android.launcher3.util.Thunk; import com.android.launcher3.util.Thunk;
@@ -37,7 +38,7 @@ import com.android.launcher3.util.Thunk;
public class BaseRecyclerViewFastScrollBar { public class BaseRecyclerViewFastScrollBar {
public interface FastScrollFocusableView { 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; private final static int MAX_TRACK_ALPHA = 30;
@@ -199,7 +200,7 @@ public class BaseRecyclerViewFastScrollBar {
} }
mTouchOffset += (lastY - downY); mTouchOffset += (lastY - downY);
mPopup.animateVisibility(true); mPopup.animateVisibility(true);
animateScrollbar(true); showActiveScrollbar(true);
} }
if (mIsDragging) { if (mIsDragging) {
// Update the fastscroller section name at this touch position // Update the fastscroller section name at this touch position
@@ -210,7 +211,7 @@ public class BaseRecyclerViewFastScrollBar {
(bottom - top)); (bottom - top));
mPopup.setSectionName(sectionName); mPopup.setSectionName(sectionName);
mPopup.animateVisibility(!sectionName.isEmpty()); mPopup.animateVisibility(!sectionName.isEmpty());
mRv.invalidate(mPopup.updateFastScrollerBounds(mRv, lastY)); mRv.invalidate(mPopup.updateFastScrollerBounds(lastY));
mLastTouchY = boundedY; mLastTouchY = boundedY;
} }
break; break;
@@ -222,7 +223,7 @@ public class BaseRecyclerViewFastScrollBar {
if (mIsDragging) { if (mIsDragging) {
mIsDragging = false; mIsDragging = false;
mPopup.animateVisibility(false); mPopup.animateVisibility(false);
animateScrollbar(false); showActiveScrollbar(false);
} }
break; break;
} }
@@ -246,7 +247,7 @@ public class BaseRecyclerViewFastScrollBar {
/** /**
* Animates the width and color of the scrollbar. * Animates the width and color of the scrollbar.
*/ */
private void animateScrollbar(boolean isScrolling) { private void showActiveScrollbar(boolean isScrolling) {
if (mScrollbarAnimator != null) { if (mScrollbarAnimator != null) {
mScrollbarAnimator.cancel(); mScrollbarAnimator.cancel();
} }

View File

@@ -77,26 +77,26 @@ public class BaseRecyclerViewFastScrollPopup {
* Updates the bounds for the fast scroller. * Updates the bounds for the fast scroller.
* @return the invalidation rect for this update. * @return the invalidation rect for this update.
*/ */
public Rect updateFastScrollerBounds(BaseRecyclerView rv, int lastTouchY) { public Rect updateFastScrollerBounds(int lastTouchY) {
mInvalidateRect.set(mBgBounds); mInvalidateRect.set(mBgBounds);
if (isVisible()) { if (isVisible()) {
// Calculate the dimensions and position of the fast scroller popup // 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 bgPadding = (mBgOriginalSize - mTextBounds.height()) / 2;
int bgHeight = mBgOriginalSize; int bgHeight = mBgOriginalSize;
int bgWidth = Math.max(mBgOriginalSize, mTextBounds.width() + (2 * bgPadding)); int bgWidth = Math.max(mBgOriginalSize, mTextBounds.width() + (2 * bgPadding));
if (Utilities.isRtl(mRes)) { 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; mBgBounds.right = mBgBounds.left + bgWidth;
} else { } else {
mBgBounds.right = rv.getWidth() - rv.getBackgroundPadding().right - mBgBounds.right = mRv.getWidth() - mRv.getBackgroundPadding().right -
(2 * rv.getMaxScrollbarWidth()); (2 * mRv.getMaxScrollbarWidth());
mBgBounds.left = mBgBounds.right - bgWidth; mBgBounds.left = mBgBounds.right - bgWidth;
} }
mBgBounds.top = lastTouchY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgHeight); mBgBounds.top = lastTouchY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgHeight);
mBgBounds.top = Math.max(edgePadding, 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; mBgBounds.bottom = mBgBounds.top + bgHeight;
} else { } else {
mBgBounds.setEmpty(); mBgBounds.setEmpty();

View File

@@ -16,7 +16,6 @@
package com.android.launcher3; package com.android.launcher3;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.content.res.ColorStateList; import android.content.res.ColorStateList;
@@ -25,7 +24,6 @@ import android.content.res.Resources.Theme;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Region; import android.graphics.Region;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
@@ -37,8 +35,6 @@ import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewConfiguration; import android.view.ViewConfiguration;
import android.view.ViewParent; import android.view.ViewParent;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.TextView; import android.widget.TextView;
import com.android.launcher3.IconCache.IconLoadRequest; 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_WORKSPACE = 0;
private static final int DISPLAY_ALL_APPS = 1; 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 final Launcher mLauncher;
private Drawable mIcon; private Drawable mIcon;
private final Drawable mBackground; private final Drawable mBackground;
@@ -93,12 +82,6 @@ public class BubbleTextView extends TextView
private boolean mIgnorePressedStateChange; private boolean mIgnorePressedStateChange;
private boolean mDisableRelayout = false; 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; private IconLoadRequest mIconLoadRequest;
public BubbleTextView(Context context) { 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); 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()); setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate());
} }
@@ -170,8 +146,9 @@ public class BubbleTextView extends TextView
Bitmap b = info.getIcon(iconCache); Bitmap b = info.getIcon(iconCache);
FastBitmapDrawable iconDrawable = mLauncher.createIconDrawable(b); FastBitmapDrawable iconDrawable = mLauncher.createIconDrawable(b);
iconDrawable.setGhostModeEnabled(info.isDisabled != 0); if (info.isDisabled != 0) {
iconDrawable.setState(FastBitmapDrawable.State.DISABLED);
}
setIcon(iconDrawable, mIconSize); setIcon(iconDrawable, mIconSize);
if (info.contentDescription != null) { if (info.contentDescription != null) {
setContentDescription(info.contentDescription); setContentDescription(info.contentDescription);
@@ -259,7 +236,12 @@ public class BubbleTextView extends TextView
private void updateIconState() { private void updateIconState() {
if (mIcon instanceof FastBitmapDrawable) { 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 @Override
public void draw(Canvas canvas) { public void draw(Canvas canvas) {
if (!mCustomShadowsEnabled) { 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); super.draw(canvas);
return; return;
} }
@@ -533,8 +504,13 @@ public class BubbleTextView extends TextView
*/ */
public void reapplyItemInfo(final ItemInfo info) { public void reapplyItemInfo(final ItemInfo info) {
if (getTag() == info) { if (getTag() == info) {
FastBitmapDrawable.State prevState = FastBitmapDrawable.State.NORMAL;
if (mIcon instanceof FastBitmapDrawable) {
prevState = ((FastBitmapDrawable) mIcon).getCurrentState();
}
mIconLoadRequest = null; mIconLoadRequest = null;
mDisableRelayout = true; mDisableRelayout = true;
if (info instanceof AppInfo) { if (info instanceof AppInfo) {
applyFromApplicationInfo((AppInfo) info); applyFromApplicationInfo((AppInfo) info);
} else if (info instanceof ShortcutInfo) { } else if (info instanceof ShortcutInfo) {
@@ -550,6 +526,13 @@ public class BubbleTextView extends TextView
} else if (info instanceof PackageItemInfo) { } else if (info instanceof PackageItemInfo) {
applyFromPackageItemInfo((PackageItemInfo) info); 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; 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 @Override
public void setFastScrollFocused(final boolean focused, boolean animated) { public void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated) {
if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_NONE) { // We can only set the fast scroll focus state on a FastBitmapDrawable
if (!(mIcon instanceof FastBitmapDrawable)) {
return; return;
} }
if (mFastScrollFocused != focused) { FastBitmapDrawable d = (FastBitmapDrawable) mIcon;
mFastScrollFocused = focused; if (animated) {
FastBitmapDrawable.State prevState = d.getCurrentState();
if (animated) { if (d.animateState(focusState)) {
// Clean up the previous focus animator // If the state was updated, then update the view accordingly
if (mFastScrollFocusAnimator != null) { animate().scaleX(focusState.viewScale)
mFastScrollFocusAnimator.cancel(); .scaleY(focusState.viewScale)
} .setStartDelay(getStartDelayForStateChange(prevState, focusState))
mFastScrollFocusAnimator = ObjectAnimator.ofFloat(this, "fastScrollFocus", .setDuration(d.getDurationForStateChange(prevState, focusState))
focused ? 1f : 0f); .start();
if (focused) { }
mFastScrollFocusAnimator.setInterpolator(new DecelerateInterpolator()); } else {
} else { if (d.setState(focusState)) {
mFastScrollFocusAnimator.setInterpolator(new AccelerateInterpolator()); // If the state was updated, then update the view accordingly
} animate().cancel();
mFastScrollFocusAnimator.setDuration(focused ? setScaleX(focusState.viewScale);
FAST_SCROLL_FOCUS_FADE_IN_DURATION : FAST_SCROLL_FOCUS_FADE_OUT_DURATION); setScaleY(focusState.viewScale);
mFastScrollFocusAnimator.start();
} else {
mFastScrollFocusFraction = focused ? 1f : 0f;
} }
} }
} }
/**
* 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;
}
}
return 0;
}
/** /**
* Interface to be implemented by the grand parent to allow click shadow effect. * 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); void setPressedIcon(BubbleTextView icon, Bitmap background);
} }
} }

View File

@@ -16,6 +16,7 @@
package com.android.launcher3; package com.android.launcher3;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator; import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator; import android.animation.TimeInterpolator;
import android.graphics.Bitmap; import android.graphics.Bitmap;
@@ -28,13 +29,40 @@ import android.graphics.Paint;
import android.graphics.PixelFormat; import android.graphics.PixelFormat;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter; import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.util.SparseArray; import android.util.SparseArray;
import android.view.animation.DecelerateInterpolator;
public class FastBitmapDrawable extends Drawable { 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 @Override
public float getInterpolation(float input) { 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; // Since we don't need 256^2 values for combinations of both the brightness and saturation, we
private static ColorMatrix sGhostModeMatrix; // reduce the value space to a smaller value V, which reduces the number of cached
private static final ColorMatrix sTempMatrix = new ColorMatrix(); // ColorMatrixColorFilters that we need to keep to V^2
private static final int REDUCED_FILTER_VALUE_SPACE = 48;
/** // A cache of ColorFilters for optimizing brightness and saturation animations
* Store the brightness colors filters to optimize animations during icon press. This private static final SparseArray<ColorFilter> sCachedFilter = new SparseArray<>();
* only works for non-ghost-mode icons.
*/
private static final SparseArray<ColorFilter> sCachedBrightnessFilter =
new SparseArray<ColorFilter>();
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 Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
private final Bitmap mBitmap; 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 int mBrightness = 0;
private boolean mGhostModeEnabled = false; private int mAlpha = 255;
private int mPrevUpdateKey = Integer.MAX_VALUE;
private boolean mPressed = false; // Animators for the fast bitmap drawable's properties
private ObjectAnimator mPressedAnimator; private AnimatorSet mPropertyAnimator;
public FastBitmapDrawable(Bitmap b) { public FastBitmapDrawable(Bitmap b) {
mAlpha = 255;
mBitmap = b; mBitmap = b;
setBounds(0, 0, b.getWidth(), b.getHeight()); setBounds(0, 0, b.getWidth(), b.getHeight());
} }
@Override @Override
public void draw(Canvas canvas) { public void draw(Canvas canvas) {
final Rect r = getBounds(); canvas.drawBitmap(mBitmap, null, getBounds(), mPaint);
// Draw the bitmap into the bounding rect
canvas.drawBitmap(mBitmap, null, r, mPaint);
} }
@Override @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' * Animates this drawable to a new state.
* appearance. *
* @return whether the state has changed.
*/ */
public void setGhostModeEnabled(boolean enabled) { public boolean animateState(State newState) {
if (mGhostModeEnabled != enabled) { State prevState = mState;
mGhostModeEnabled = enabled; 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;
}
/**
* 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;
}
return false;
}
/**
* Returns the current state.
*/
public State getCurrentState() {
return mState;
}
/**
* 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;
}
/**
* 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;
}
/**
* 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(); updateFilter();
} }
} }
public void setPressed(boolean pressed) { public float getDesaturation() {
if (mPressed != pressed) { return (float) mDesaturation / REDUCED_FILTER_VALUE_SPACE;
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);
}
}
invalidateSelf();
} }
public boolean isGhostModeEnabled() { /**
return mGhostModeEnabled; * Sets the brightness of this icon, 0 [no add. brightness] -> 1 [2bright2furious]
} */
public void setBrightness(float brightness) {
public int getBrightness() { int newBrightness = (int) Math.floor(brightness * REDUCED_FILTER_VALUE_SPACE);
return mBrightness; if (mBrightness != newBrightness) {
} mBrightness = newBrightness;
public void setBrightness(int brightness) {
if (mBrightness != brightness) {
mBrightness = brightness;
updateFilter(); 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() { private void updateFilter() {
if (mGhostModeEnabled) { boolean usePorterDuffFilter = false;
if (sGhostModeMatrix == null) { int key = -1;
sGhostModeMatrix = new ColorMatrix(); if (mDesaturation > 0) {
sGhostModeMatrix.setSaturation(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;
// For ghost mode, set the color range to [GHOST_MODE_MIN_COLOR_RANGE, 255] // We found that in L, ColorFilters cause drawing artifacts with shadows baked into
float range = (255 - GHOST_MODE_MIN_COLOR_RANGE) / 255.0f; // icons, so just use a PorterDuff filter when we aren't animating saturation
sTempMatrix.set(new float[] { usePorterDuffFilter = true;
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);
}
if (mBrightness == 0) { // Debounce multiple updates on the same frame
mPaint.setColorFilter(new ColorMatrixColorFilter(sGhostModeMatrix)); if (key == mPrevUpdateKey) {
} else { return;
setBrightnessMatrix(sTempMatrix, mBrightness); }
sTempMatrix.postConcat(sGhostModeMatrix); mPrevUpdateKey = key;
mPaint.setColorFilter(new ColorMatrixColorFilter(sTempMatrix));
} if (key != -1) {
} else if (mBrightness != 0) { ColorFilter filter = sCachedFilter.get(key);
ColorFilter filter = sCachedBrightnessFilter.get(mBrightness);
if (filter == null) { if (filter == null) {
filter = new PorterDuffColorFilter(Color.argb(mBrightness, 255, 255, 255), float brightnessF = getBrightness();
PorterDuff.Mode.SRC_ATOP); int brightnessI = (int) (255 * brightnessF);
sCachedBrightnessFilter.put(mBrightness, filter); 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); mPaint.setColorFilter(filter);
} else { } else {
mPaint.setColorFilter(null); mPaint.setColorFilter(null);
} }
invalidateSelf();
} }
private static void setBrightnessMatrix(ColorMatrix matrix, int brightness) { private AnimatorSet cancelAnimator(AnimatorSet animator) {
// Brightness: C-new = C-old*(1-amount) + amount if (animator != null) {
float scale = 1 - brightness / 255.0f; animator.removeAllListeners();
matrix.setScale(scale, scale, scale, 1); animator.cancel();
float[] array = matrix.getArray(); }
return null;
// 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;
} }
} }

View File

@@ -521,7 +521,7 @@ public class FolderIcon extends FrameLayout implements FolderListener {
} }
class PreviewItemDrawingParams { class PreviewItemDrawingParams {
PreviewItemDrawingParams(float transX, float transY, float scale, int overlayAlpha) { PreviewItemDrawingParams(float transX, float transY, float scale, float overlayAlpha) {
this.transX = transX; this.transX = transX;
this.transY = transY; this.transY = transY;
this.scale = scale; this.scale = scale;
@@ -530,7 +530,7 @@ public class FolderIcon extends FrameLayout implements FolderListener {
float transX; float transX;
float transY; float transY;
float scale; float scale;
int overlayAlpha; float overlayAlpha;
Drawable drawable; Drawable drawable;
} }
@@ -562,7 +562,7 @@ public class FolderIcon extends FrameLayout implements FolderListener {
float transY = mAvailableSpaceInPreview - (offset + scaledSize + scaleOffsetCorrection) + getPaddingTop(); float transY = mAvailableSpaceInPreview - (offset + scaledSize + scaleOffsetCorrection) + getPaddingTop();
float transX = (mAvailableSpaceInPreview - scaledSize) / 2; float transX = (mAvailableSpaceInPreview - scaledSize) / 2;
float totalScale = mBaselineIconScale * scale; float totalScale = mBaselineIconScale * scale;
final int overlayAlpha = (int) (80 * (1 - r)); final float overlayAlpha = (80 * (1 - r)) / 255f;
if (params == null) { if (params == null) {
params = new PreviewItemDrawingParams(transX, transY, totalScale, overlayAlpha); params = new PreviewItemDrawingParams(transX, transY, totalScale, overlayAlpha);
@@ -586,12 +586,12 @@ public class FolderIcon extends FrameLayout implements FolderListener {
d.setBounds(0, 0, mIntrinsicIconSize, mIntrinsicIconSize); d.setBounds(0, 0, mIntrinsicIconSize, mIntrinsicIconSize);
if (d instanceof FastBitmapDrawable) { if (d instanceof FastBitmapDrawable) {
FastBitmapDrawable fd = (FastBitmapDrawable) d; FastBitmapDrawable fd = (FastBitmapDrawable) d;
int oldBrightness = fd.getBrightness(); float oldBrightness = fd.getBrightness();
fd.setBrightness(params.overlayAlpha); fd.setBrightness(params.overlayAlpha);
d.draw(canvas); d.draw(canvas);
fd.setBrightness(oldBrightness); fd.setBrightness(oldBrightness);
} else { } 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); PorterDuff.Mode.SRC_ATOP);
d.draw(canvas); d.draw(canvas);
d.clearColorFilter(); d.clearColorFilter();

View File

@@ -46,6 +46,7 @@ import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap; import android.graphics.Bitmap;
@@ -4573,6 +4574,18 @@ public class Launcher extends Activity
UserHandleCompat.myUserHandle()); 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 // TODO: This method should be a part of LauncherSearchCallback
public void startDrag(View dragView, ItemInfo dragInfo, DragSource source) { public void startDrag(View dragView, ItemInfo dragInfo, DragSource source) {
dragView.setTag(dragInfo); dragView.setTag(dragInfo);

View File

@@ -133,7 +133,7 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen
// 3) Setup icon in the center and app icon in the top right corner. // 3) Setup icon in the center and app icon in the top right corner.
if (mDisabledForSafeMode) { if (mDisabledForSafeMode) {
FastBitmapDrawable disabledIcon = mLauncher.createIconDrawable(mIcon); FastBitmapDrawable disabledIcon = mLauncher.createIconDrawable(mIcon);
disabledIcon.setGhostModeEnabled(true); disabledIcon.setState(FastBitmapDrawable.State.DISABLED);
mCenterDrawable = disabledIcon; mCenterDrawable = disabledIcon;
mSettingIconDrawable = null; mSettingIconDrawable = null;
} else if (isReadyForClickSetup()) { } else if (isReadyForClickSetup()) {

View File

@@ -179,7 +179,8 @@ class PreloadIconDrawable extends Drawable {
mPaint.setColor(getIndicatorColor()); mPaint.setColor(getIndicatorColor());
} }
if (mIcon instanceof FastBitmapDrawable) { if (mIcon instanceof FastBitmapDrawable) {
((FastBitmapDrawable) mIcon).setGhostModeEnabled(level <= 0); ((FastBitmapDrawable) mIcon).setState(level <= 0 ?
FastBitmapDrawable.State.DISABLED : FastBitmapDrawable.State.NORMAL);
} }
invalidateSelf(); invalidateSelf();

View File

@@ -28,6 +28,7 @@ import android.text.SpannableStringBuilder;
import android.text.method.TextKeyListener; import android.text.method.TextKeyListener;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewConfiguration; import android.view.ViewConfiguration;
@@ -35,6 +36,7 @@ import android.view.ViewGroup;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import com.android.launcher3.AppInfo; import com.android.launcher3.AppInfo;
import com.android.launcher3.BaseContainerView; import com.android.launcher3.BaseContainerView;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.CellLayout; import com.android.launcher3.CellLayout;
import com.android.launcher3.DeleteDropTarget; import com.android.launcher3.DeleteDropTarget;
import com.android.launcher3.DeviceProfile; import com.android.launcher3.DeviceProfile;
@@ -332,6 +334,18 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
mAppsRecyclerView.addItemDecoration(mItemDecoration); 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(); updateBackgroundAndPaddings();
} }

View File

@@ -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);
}
}

View File

@@ -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 // The message to continue to a market search when there are no filtered results
public static final int SEARCH_MARKET_VIEW_TYPE = 5; public static final int SEARCH_MARKET_VIEW_TYPE = 5;
public interface BindViewCallback {
public void onBindView(ViewHolder holder);
}
/** /**
* ViewHolder for each icon. * ViewHolder for each icon.
*/ */
@@ -328,6 +332,7 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
private View.OnTouchListener mTouchListener; private View.OnTouchListener mTouchListener;
private View.OnClickListener mIconClickListener; private View.OnClickListener mIconClickListener;
private View.OnLongClickListener mIconLongClickListener; private View.OnLongClickListener mIconLongClickListener;
private BindViewCallback mBindViewCallback;
@Thunk final Rect mBackgroundPadding = new Rect(); @Thunk final Rect mBackgroundPadding = new Rect();
@Thunk int mPredictionBarDividerOffset; @Thunk int mPredictionBarDividerOffset;
@Thunk int mAppsPerRow; @Thunk int mAppsPerRow;
@@ -423,6 +428,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 * Notifies the adapter of the background padding so that it can draw things correctly in the
* item decorator. * item decorator.
@@ -528,6 +540,15 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
} }
break; 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 @Override

View File

@@ -15,24 +15,20 @@
*/ */
package com.android.launcher3.allapps; package com.android.launcher3.allapps;
import android.animation.ObjectAnimator;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
import com.android.launcher3.BaseRecyclerView; import com.android.launcher3.BaseRecyclerView;
import com.android.launcher3.BaseRecyclerViewFastScrollBar;
import com.android.launcher3.DeviceProfile; import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R; import com.android.launcher3.R;
import com.android.launcher3.Stats; import com.android.launcher3.Stats;
import com.android.launcher3.Utilities; import com.android.launcher3.Utilities;
import com.android.launcher3.util.Thunk;
import java.util.List; import java.util.List;
@@ -42,25 +38,17 @@ import java.util.List;
public class AllAppsRecyclerView extends BaseRecyclerView public class AllAppsRecyclerView extends BaseRecyclerView
implements Stats.LaunchSourceProvider { 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 AlphabeticalAppsList mApps;
private AllAppsFastScrollHelper mFastScrollHelper;
private BaseRecyclerView.ScrollPositionState mScrollPosState =
new BaseRecyclerView.ScrollPositionState();
private int mNumAppsPerRow; private int mNumAppsPerRow;
@Thunk BaseRecyclerViewFastScrollBar.FastScrollFocusableView mLastFastScrollFocusedView; // The specific icon heights that we use to calculate scroll
@Thunk int mPrevFastScrollFocusedPosition; private int mPredictionIconHeight;
@Thunk int mFastScrollFrameIndex; private int mIconHeight;
@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 empty-search result background
private AllAppsBackgroundDrawable mEmptySearchBackground; private AllAppsBackgroundDrawable mEmptySearchBackground;
private int mEmptySearchBackgroundTopOffset; private int mEmptySearchBackgroundTopOffset;
@@ -79,8 +67,8 @@ public class AllAppsRecyclerView extends BaseRecyclerView
public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) { int defStyleRes) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
Resources res = getResources(); Resources res = getResources();
addOnItemTouchListener(this);
mScrollbar.setDetachThumbOnFastScroll(); mScrollbar.setDetachThumbOnFastScroll();
mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize( mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize(
R.dimen.all_apps_empty_search_bg_top_offset); R.dimen.all_apps_empty_search_bg_top_offset);
@@ -91,6 +79,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView
*/ */
public void setApps(AlphabeticalAppsList apps) { public void setApps(AlphabeticalAppsList apps) {
mApps = apps; mApps = apps;
mFastScrollHelper = new AllAppsFastScrollHelper(this, apps);
} }
/** /**
@@ -109,6 +98,14 @@ public class AllAppsRecyclerView extends BaseRecyclerView
pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows); pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows);
} }
/**
* 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. * Scrolls this recycler view to the top.
*/ */
@@ -126,6 +123,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView
*/ */
@Override @Override
protected void dispatchDraw(Canvas canvas) { 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, canvas.clipRect(mBackgroundPadding.left, mBackgroundPadding.top,
getWidth() - mBackgroundPadding.right, getWidth() - mBackgroundPadding.right,
getHeight() - mBackgroundPadding.bottom); getHeight() - mBackgroundPadding.bottom);
@@ -156,14 +154,6 @@ public class AllAppsRecyclerView extends BaseRecyclerView
updateEmptySearchBackgroundBounds(); updateEmptySearchBackgroundBounds();
} }
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// Bind event handlers
addOnItemTouchListener(this);
}
@Override @Override
public void fillInLaunchSourceData(Bundle sourceData) { public void fillInLaunchSourceData(Bundle sourceData) {
sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_ALL_APPS); sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_ALL_APPS);
@@ -212,63 +202,31 @@ public class AllAppsRecyclerView extends BaseRecyclerView
List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections = List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
mApps.getFastScrollerSections(); mApps.getFastScrollerSections();
AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0); AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0);
if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW) { for (int i = 1; i < fastScrollSections.size(); i++) {
for (int i = 1; i < fastScrollSections.size(); i++) { AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i);
AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i); if (info.touchFraction > touchFraction) {
if (info.touchFraction > touchFraction) { break;
break;
}
lastInfo = info;
} }
} else if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS){ lastInfo = info;
lastInfo = fastScrollSections.get((int) (touchFraction * (fastScrollSections.size() - 1)));
} else {
throw new RuntimeException("Unexpected scroll bar mode");
} }
// Map the touch position back to the scroll of the recycler view // Update the fast scroll
getCurScrollState(mScrollPosState); int scrollY = getScrollTop(mScrollPosState);
int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight); int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows());
LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager(); mFastScrollHelper.smoothScrollToSection(scrollY, availableScrollHeight, lastInfo);
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");
}
}
return lastInfo.sectionName; return lastInfo.sectionName;
} }
@Override @Override
public void onFastScrollCompleted() { public void onFastScrollCompleted() {
super.onFastScrollCompleted(); super.onFastScrollCompleted();
// Reset and clean up the last focused view mFastScrollHelper.onFastScrollCompleted();
if (mLastFastScrollFocusedView != null) { }
mLastFastScrollFocusedView.setFastScrollFocused(false, true);
mLastFastScrollFocusedView = null; @Override
} public void setAdapter(Adapter adapter) {
mPrevFastScrollFocusedPosition = -1; 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) // Find the index and height of the first visible row (all rows have the same height)
int rowCount = mApps.getNumAppRows(); int rowCount = mApps.getNumAppRows();
getCurScrollState(mScrollPosState); getCurScrollState(mScrollPosState, -1);
if (mScrollPosState.rowIndex < 0) { if (mScrollPosState.rowIndex < 0) {
mScrollbar.setThumbOffset(-1, -1); mScrollbar.setThumbOffset(-1, -1);
return; return;
@@ -294,7 +252,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView
// Only show the scrollbar if there is height to be scrolled // Only show the scrollbar if there is height to be scrolled
int availableScrollBarHeight = getAvailableScrollBarHeight(); int availableScrollBarHeight = getAvailableScrollBarHeight();
int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows(), mScrollPosState.rowHeight); int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows());
if (availableScrollHeight <= 0) { if (availableScrollHeight <= 0) {
mScrollbar.setThumbOffset(-1, -1); mScrollbar.setThumbOffset(-1, -1);
return; return;
@@ -303,8 +261,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView
// Calculate the current scroll position, the scrollY of the recycler view accounts for the // 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 // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
// padding) // padding)
int scrollY = getPaddingTop() + int scrollY = getScrollTop(mScrollPosState);
(mScrollPosState.rowIndex * mScrollPosState.rowHeight) - mScrollPosState.rowTopOffset;
int scrollBarY = mBackgroundPadding.top + int scrollBarY = mBackgroundPadding.top +
(int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
@@ -354,59 +311,13 @@ 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. * 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.rowIndex = -1;
stateOut.rowTopOffset = -1; stateOut.rowTopOffset = -1;
stateOut.rowHeight = -1; stateOut.itemPos = -1;
// Return early if there are no items or we haven't been measured // Return early if there are no items or we haven't been measured
List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
@@ -420,15 +331,15 @@ public class AllAppsRecyclerView extends BaseRecyclerView
int position = getChildPosition(child); int position = getChildPosition(child);
if (position != NO_POSITION) { if (position != NO_POSITION) {
AlphabeticalAppsList.AdapterItem item = items.get(position); AlphabeticalAppsList.AdapterItem item = items.get(position);
if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE || if ((item.viewType & viewTypeMask) != 0) {
item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
stateOut.rowIndex = item.rowIndex; stateOut.rowIndex = item.rowIndex;
stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child); stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
stateOut.rowHeight = child.getHeight(); stateOut.itemPos = position;
break; return;
} }
} }
} }
return;
} }
@Override @Override
@@ -438,18 +349,13 @@ public class AllAppsRecyclerView extends BaseRecyclerView
return !mApps.hasFilter(); return !mApps.hasFilter();
} }
/** protected int getTop(int rowIndex) {
* Returns the scrollY for the given position in the adapter. if (getChildCount() == 0 || rowIndex <= 0) {
*/
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 {
return 0; return 0;
} }
// The prediction bar icons have more padding, so account for that in the row offset
return mPredictionIconHeight + (rowIndex - 1) * mIconHeight;
} }
/** /**

View File

@@ -102,9 +102,9 @@ public class WidgetsRecyclerView extends BaseRecyclerView {
// Stop the scroller if it is scrolling // Stop the scroller if it is scrolling
stopScroll(); stopScroll();
getCurScrollState(mScrollPosState); getCurScrollState(mScrollPosState, -1);
float pos = rowCount * touchFraction; float pos = rowCount * touchFraction;
int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight); int availableScrollHeight = getAvailableScrollHeight(rowCount);
LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager()); LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager());
layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); 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. // Skip early if, there no child laid out in the container.
getCurScrollState(mScrollPosState); getCurScrollState(mScrollPosState, -1);
if (mScrollPosState.rowIndex < 0) { if (mScrollPosState.rowIndex < 0) {
mScrollbar.setThumbOffset(-1, -1); mScrollbar.setThumbOffset(-1, -1);
return; return;
@@ -143,10 +143,10 @@ public class WidgetsRecyclerView extends BaseRecyclerView {
/** /**
* Returns the current scroll state. * Returns the current scroll state.
*/ */
protected void getCurScrollState(ScrollPositionState stateOut) { protected void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask) {
stateOut.rowIndex = -1; stateOut.rowIndex = -1;
stateOut.rowTopOffset = -1; stateOut.rowTopOffset = -1;
stateOut.rowHeight = -1; stateOut.itemPos = -1;
// Skip early if widgets are not bound. // Skip early if widgets are not bound.
if (mWidgets == null) { if (mWidgets == null) {
@@ -163,6 +163,17 @@ public class WidgetsRecyclerView extends BaseRecyclerView {
stateOut.rowIndex = position; stateOut.rowIndex = position;
stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child); 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;
} }
} }