Tweaking fast scroller to follow touch closer.

- Issue: The fast scroller currently does not follow the touch input
  because fundamentally, the fixed scrollbar height and the mapping
  of the scroll space to the scrollbar space is fundamentally incompatible.
- This CL changes the fast scroller to allow it to detach when the user
  fast-scrolls, then re-attaches after the user scrolls the screen and
  the current scroll position for the scrollbar picks up the thumb position.
- Since the scroll position and the fast scroller thumb is now detached,
  we can change the distribution of the fast scroll letters to make it
  independent of the rows for each section and instead uniformly distribute
  it along the scrollbar, which allows for more stability.
- There are edge cases where this fails, especially when there are few
  apps, which we can investigate further.
  
Bug: 20035978

Change-Id: I8322f862107e6f330deff692885233706564bffd
This commit is contained in:
Winson
2015-08-18 17:43:02 -07:00
committed by Winson Chung
parent 97b0d08d92
commit d2eb49e4c3
7 changed files with 161 additions and 61 deletions
@@ -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;
}