Merge "Tweaking fast scroller to follow touch closer." into ub-launcher3-burnaby

This commit is contained in:
Winson Chung
2015-08-20 19:26:00 +00:00
committed by Android (Google) Code Review
7 changed files with 161 additions and 61 deletions

View File

@@ -3,8 +3,8 @@
}
-keep class com.android.launcher3.BaseRecyclerViewFastScrollBar {
public void setWidth(int);
public int getWidth();
public void setThumbWidth(int);
public int getThumbWidth();
public void setTrackAlpha(int);
public int getTrackAlpha();
}

View File

@@ -92,9 +92,15 @@ public abstract class BaseRecyclerView extends RecyclerView
// TODO(winsonc): If we want to animate the section heads while scrolling, we can
// initiate that here if the recycler view scroll state is not
// RecyclerView.SCROLL_STATE_IDLE.
onUpdateScrollbar(dy);
}
}
public void reset() {
mScrollbar.reattachThumbToScroll();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
@@ -143,7 +149,7 @@ public abstract class BaseRecyclerView extends RecyclerView
mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
break;
}
return mScrollbar.isDragging();
return mScrollbar.isDraggingThumb();
}
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
@@ -185,12 +191,10 @@ public abstract class BaseRecyclerView extends RecyclerView
* AvailableScrollHeight = Total height of the all items - last page height
*
* This assumes that all rows are the same height.
*
* @param yOffset the offset from the top of the recycler view to start tracking.
*/
protected int getAvailableScrollHeight(int rowCount, int rowHeight, int yOffset) {
protected int getAvailableScrollHeight(int rowCount, int rowHeight) {
int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
int scrollHeight = getPaddingTop() + yOffset + rowCount * rowHeight + getPaddingBottom();
int scrollHeight = getPaddingTop() + rowCount * rowHeight + getPaddingBottom();
int availableScrollHeight = scrollHeight - visibleHeight;
return availableScrollHeight;
}
@@ -222,7 +226,7 @@ public abstract class BaseRecyclerView extends RecyclerView
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
onUpdateScrollbar();
onUpdateScrollbar(0);
mScrollbar.draw(canvas);
}
@@ -234,24 +238,21 @@ public abstract class BaseRecyclerView extends RecyclerView
* @param scrollPosState the current scroll position
* @param rowCount the number of rows, used to calculate the total scroll height (assumes that
* all rows are the same height)
* @param yOffset the offset to start tracking in the recycler view (only used for all apps)
*/
protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState,
int rowCount, int yOffset) {
int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight,
yOffset);
int availableScrollBarHeight = getAvailableScrollBarHeight();
int rowCount) {
// Only show the scrollbar if there is height to be scrolled
int availableScrollBarHeight = getAvailableScrollBarHeight();
int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight);
if (availableScrollHeight <= 0) {
mScrollbar.setScrollbarThumbOffset(-1, -1);
mScrollbar.setThumbOffset(-1, -1);
return;
}
// 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() + yOffset +
int scrollY = getPaddingTop() +
(scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset;
int scrollBarY = mBackgroundPadding.top +
(int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
@@ -261,9 +262,9 @@ public abstract class BaseRecyclerView extends RecyclerView
if (Utilities.isRtl(getResources())) {
scrollBarX = mBackgroundPadding.left;
} else {
scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getWidth();
scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth();
}
mScrollbar.setScrollbarThumbOffset(scrollBarX, scrollBarY);
mScrollbar.setThumbOffset(scrollBarX, scrollBarY);
}
/**
@@ -276,10 +277,15 @@ public abstract class BaseRecyclerView extends RecyclerView
* Updates the bounds for the scrollbar.
* <p>Override in each subclass of this base class.
*/
public abstract void onUpdateScrollbar();
public abstract void onUpdateScrollbar(int dy);
/**
* <p>Override in each subclass of this base class.
*/
public void onFastScrollCompleted() {}
/**
* Returns information about the item that the recycler view is currently scrolled to.
*/
protected abstract void getCurScrollState(ScrollPositionState stateOut);
}

View File

@@ -56,9 +56,12 @@ public class BaseRecyclerViewFastScrollBar {
private int mThumbMaxWidth;
@Thunk int mThumbWidth;
@Thunk int mThumbHeight;
private float mLastTouchY;
// The inset is the buffer around which a point will still register as a click on the scrollbar
private int mTouchInset;
private boolean mIsDragging;
private boolean mIsThumbDetached;
private boolean mCanThumbDetach;
// This is the offset from the top of the scrollbar when the user first starts touching. To
// prevent jumping, this offset is applied as the user scrolls.
@@ -84,7 +87,15 @@ public class BaseRecyclerViewFastScrollBar {
mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset);
}
public void setScrollbarThumbOffset(int x, int y) {
public void setDetachThumbOnFastScroll() {
mCanThumbDetach = true;
}
public void reattachThumbToScroll() {
mIsThumbDetached = false;
}
public void setThumbOffset(int x, int y) {
if (mThumbOffset.x == x && mThumbOffset.y == y) {
return;
}
@@ -95,8 +106,12 @@ public class BaseRecyclerViewFastScrollBar {
mRv.invalidate(mInvalidateRect);
}
public Point getThumbOffset() {
return mThumbOffset;
}
// Setter/getter for the search bar width for animations
public void setWidth(int width) {
public void setThumbWidth(int width) {
mInvalidateRect.set(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight());
mThumbWidth = width;
mInvalidateRect.union(new Rect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth,
@@ -104,7 +119,7 @@ public class BaseRecyclerViewFastScrollBar {
mRv.invalidate(mInvalidateRect);
}
public int getWidth() {
public int getThumbWidth() {
return mThumbWidth;
}
@@ -127,10 +142,18 @@ public class BaseRecyclerViewFastScrollBar {
return mThumbMaxWidth;
}
public boolean isDragging() {
public float getLastTouchY() {
return mLastTouchY;
}
public boolean isDraggingThumb() {
return mIsDragging;
}
public boolean isThumbDetached() {
return mIsThumbDetached;
}
/**
* Handles the touch event and determines whether to show the fast scroller (or updates it if
* it is already showing).
@@ -152,6 +175,9 @@ public class BaseRecyclerViewFastScrollBar {
Math.abs(y - downY) > config.getScaledTouchSlop()) {
mRv.getParent().requestDisallowInterceptTouchEvent(true);
mIsDragging = true;
if (mCanThumbDetach) {
mIsThumbDetached = true;
}
mTouchOffset += (lastY - downY);
mPopup.animateVisibility(true);
animateScrollbar(true);
@@ -166,11 +192,13 @@ public class BaseRecyclerViewFastScrollBar {
mPopup.setSectionName(sectionName);
mPopup.animateVisibility(!sectionName.isEmpty());
mRv.invalidate(mPopup.updateFastScrollerBounds(mRv, lastY));
mLastTouchY = boundedY;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mTouchOffset = 0;
mLastTouchY = 0;
if (mIsDragging) {
mIsDragging = false;
mPopup.animateVisibility(false);
@@ -205,7 +233,7 @@ public class BaseRecyclerViewFastScrollBar {
}
ObjectAnimator trackAlphaAnim = ObjectAnimator.ofInt(this, "trackAlpha",
isScrolling ? MAX_TRACK_ALPHA : 0);
ObjectAnimator thumbWidthAnim = ObjectAnimator.ofInt(this, "width",
ObjectAnimator thumbWidthAnim = ObjectAnimator.ofInt(this, "thumbWidth",
isScrolling ? mThumbMaxWidth : mThumbMinWidth);
ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(),
mThumbPaint.getColor(), isScrolling ? mThumbActiveColor : mThumbInactiveColor);

View File

@@ -555,8 +555,9 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
@Override
public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) {
if (toWorkspace) {
// Reset the search bar after transitioning home
// Reset the search bar and base recycler view after transitioning home
mSearchBarController.reset();
mAppsRecyclerView.reset();
}
}

View File

@@ -72,6 +72,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView
public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr);
mScrollbar.setDetachThumbOnFastScroll();
}
/**
@@ -168,8 +169,8 @@ public class AllAppsRecyclerView extends BaseRecyclerView
}
// Map the touch position back to the scroll of the recycler view
getCurScrollState(mScrollPosState, mApps.getAdapterItems());
int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight, 0);
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));
@@ -216,24 +217,73 @@ public class AllAppsRecyclerView extends BaseRecyclerView
* Updates the bounds for the scrollbar.
*/
@Override
public void onUpdateScrollbar() {
public void onUpdateScrollbar(int dy) {
List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
// Skip early if there are no items or we haven't been measured
if (items.isEmpty() || mNumAppsPerRow == 0) {
mScrollbar.setScrollbarThumbOffset(-1, -1);
mScrollbar.setThumbOffset(-1, -1);
return;
}
// Find the index and height of the first visible row (all rows have the same height)
int rowCount = mApps.getNumAppRows();
getCurScrollState(mScrollPosState, items);
getCurScrollState(mScrollPosState);
if (mScrollPosState.rowIndex < 0) {
mScrollbar.setScrollbarThumbOffset(-1, -1);
mScrollbar.setThumbOffset(-1, -1);
return;
}
synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, 0);
// Only show the scrollbar if there is height to be scrolled
int availableScrollBarHeight = getAvailableScrollBarHeight();
int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows(), mScrollPosState.rowHeight);
if (availableScrollHeight <= 0) {
mScrollbar.setThumbOffset(-1, -1);
return;
}
// 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 scrollBarY = mBackgroundPadding.top +
(int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
if (mScrollbar.isThumbDetached()) {
int scrollBarX;
if (Utilities.isRtl(getResources())) {
scrollBarX = mBackgroundPadding.left;
} else {
scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth();
}
if (mScrollbar.isDraggingThumb()) {
// If the thumb is detached, then just update the thumb to the current
// touch position
mScrollbar.setThumbOffset(scrollBarX, (int) mScrollbar.getLastTouchY());
} else {
int thumbScrollY = mScrollbar.getThumbOffset().y;
int diffScrollY = scrollBarY - thumbScrollY;
if (diffScrollY * dy > 0f) {
// User is scrolling in the same direction the thumb needs to catch up to the
// current scroll position.
thumbScrollY += dy < 0 ? Math.max(dy, diffScrollY) : Math.min(dy, diffScrollY);
thumbScrollY = Math.max(0, Math.min(availableScrollBarHeight, thumbScrollY));
mScrollbar.setThumbOffset(scrollBarX, thumbScrollY);
if (scrollBarY == thumbScrollY) {
mScrollbar.reattachThumbToScroll();
}
} else {
// User is scrolling in an opposite direction to the direction that the thumb
// needs to catch up to the scroll position. Do nothing except for updating
// the scroll bar x to match the thumb width.
mScrollbar.setThumbOffset(scrollBarX, thumbScrollY);
}
}
} else {
synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount);
}
}
/**
@@ -285,13 +335,13 @@ public class AllAppsRecyclerView extends BaseRecyclerView
/**
* Returns the current scroll state of the apps rows.
*/
private void getCurScrollState(ScrollPositionState stateOut,
List<AlphabeticalAppsList.AdapterItem> items) {
protected void getCurScrollState(ScrollPositionState stateOut) {
stateOut.rowIndex = -1;
stateOut.rowTopOffset = -1;
stateOut.rowHeight = -1;
// Return early if there are no items or we haven't been measured
List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
if (items.isEmpty() || mNumAppsPerRow == 0) {
return;
}

View File

@@ -43,6 +43,11 @@ public class AlphabeticalAppsList {
private static final boolean DEBUG = false;
private static final boolean DEBUG_PREDICTIONS = false;
private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION = 0;
private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS = 1;
private final int mFastScrollDistributionMode = FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS;
/**
* Info about a section in the alphabetic list
*/
@@ -85,8 +90,6 @@ public class AlphabeticalAppsList {
/** Section & App properties */
// The section for this item
public SectionInfo sectionInfo;
// The row that this item shows up on
public int rowIndex;
/** App-only properties */
// The section name of this app. Note that there can be multiple items with different
@@ -94,6 +97,8 @@ public class AlphabeticalAppsList {
public String sectionName = null;
// The index of this app in the section
public int sectionAppIndex = -1;
// The row that this item shows up on
public int rowIndex;
// The index of this app in the row
public int rowAppIndex;
// The associated AppInfo for the app
@@ -188,7 +193,6 @@ public class AlphabeticalAppsList {
private int mNumAppsPerRow;
private int mNumPredictedAppsPerRow;
private int mNumAppRowsInAdapter;
private boolean mDisableEmptyText;
public AlphabeticalAppsList(Context context) {
mLauncher = (Launcher) context;
@@ -215,13 +219,6 @@ public class AlphabeticalAppsList {
mAdapter = adapter;
}
/**
* Disables the empty text message when there are no search results.
*/
public void disableEmptyText() {
mDisableEmptyText = true;
}
/**
* Returns all the apps.
*/
@@ -523,18 +520,36 @@ public class AlphabeticalAppsList {
}
mNumAppRowsInAdapter = rowIndex + 1;
// Pre-calculate all the fast scroller fractions based on the number of rows
float rowFraction = 1f / mNumAppRowsInAdapter;
for (FastScrollSectionInfo info : mFastScrollerSections) {
AdapterItem item = info.fastScrollToItem;
if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE &&
item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
info.touchFraction = 0f;
continue;
}
// Pre-calculate all the fast scroller fractions
switch (mFastScrollDistributionMode) {
case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION:
float rowFraction = 1f / mNumAppRowsInAdapter;
for (FastScrollSectionInfo info : mFastScrollerSections) {
AdapterItem item = info.fastScrollToItem;
if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE &&
item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
info.touchFraction = 0f;
continue;
}
float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow);
info.touchFraction = item.rowIndex * rowFraction + subRowFraction;
float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow);
info.touchFraction = item.rowIndex * rowFraction + subRowFraction;
}
break;
case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS:
float perSectionTouchFraction = 1f / mFastScrollerSections.size();
float cumulativeTouchFraction = 0f;
for (FastScrollSectionInfo info : mFastScrollerSections) {
AdapterItem item = info.fastScrollToItem;
if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE &&
item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
info.touchFraction = 0f;
continue;
}
info.touchFraction = cumulativeTouchFraction;
cumulativeTouchFraction += perSectionTouchFraction;
}
break;
}
}

View File

@@ -102,7 +102,7 @@ public class WidgetsRecyclerView extends BaseRecyclerView {
getCurScrollState(mScrollPosState);
float pos = rowCount * touchFraction;
int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight, 0);
int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight);
LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager());
layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction));
@@ -115,29 +115,29 @@ public class WidgetsRecyclerView extends BaseRecyclerView {
* Updates the bounds for the scrollbar.
*/
@Override
public void onUpdateScrollbar() {
public void onUpdateScrollbar(int dy) {
int rowCount = mWidgets.getPackageSize();
// Skip early if, there are no items.
if (rowCount == 0) {
mScrollbar.setScrollbarThumbOffset(-1, -1);
mScrollbar.setThumbOffset(-1, -1);
return;
}
// Skip early if, there no child laid out in the container.
getCurScrollState(mScrollPosState);
if (mScrollPosState.rowIndex < 0) {
mScrollbar.setScrollbarThumbOffset(-1, -1);
mScrollbar.setThumbOffset(-1, -1);
return;
}
synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, 0);
synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount);
}
/**
* Returns the current scroll state.
*/
private void getCurScrollState(ScrollPositionState stateOut) {
protected void getCurScrollState(ScrollPositionState stateOut) {
stateOut.rowIndex = -1;
stateOut.rowTopOffset = -1;
stateOut.rowHeight = -1;