Synchronizing views dependent on scroll during pre-draw.

Instead of tracking the absolute scroll, which can be inconsistent,
simply synchronize any views that depend on the RecyclerView scroll
each time in onPreDraw.

Bug: 21169021
This commit is contained in:
Winson Chung
2015-05-15 12:03:37 -07:00
parent 66a90e701f
commit 32f14071cc
3 changed files with 127 additions and 163 deletions
@@ -122,24 +122,6 @@ public class AppsContainerRecyclerView extends BaseContainerRecyclerView {
mApps = apps;
}
@Override
public void setAdapter(Adapter adapter) {
// Register a change listener to update the scroll position state whenever the data set
// changes.
adapter.registerAdapterDataObserver(new AdapterDataObserver() {
@Override
public void onChanged() {
post(new Runnable() {
@Override
public void run() {
refreshCurScrollPosition();
}
});
}
});
super.setAdapter(adapter);
}
/**
* Sets the number of apps per row in this recycler view.
*/
@@ -186,7 +168,20 @@ public class AppsContainerRecyclerView extends BaseContainerRecyclerView {
*/
public void scrollToTop() {
scrollToPosition(0);
updateScrollY(0);
}
/**
* Returns the current scroll position.
*/
public int getScrollPosition() {
List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
getCurScrollState(mScrollPosState, items);
if (mScrollPosState.rowIndex != -1) {
int predictionBarHeight = mApps.getPredictedApps().isEmpty() ? 0 : mPredictionBarHeight;
return getPaddingTop() + (mScrollPosState.rowIndex * mScrollPosState.rowHeight) +
predictionBarHeight - mScrollPosState.rowTopOffset;
}
return 0;
}
@Override
@@ -357,7 +352,6 @@ public class AppsContainerRecyclerView extends BaseContainerRecyclerView {
if (touchFraction <= predictionBarFraction) {
// Scroll to the top of the view, where the prediction bar is
layoutManager.scrollToPositionWithOffset(0, 0);
updateScrollY(0);
return "";
}
}
@@ -376,9 +370,6 @@ public class AppsContainerRecyclerView extends BaseContainerRecyclerView {
lastScrollSection = scrollSection;
}
// We need to workaround the RecyclerView to get the right scroll position
refreshCurScrollPosition();
// Scroll to the view at the position, anchored at the top of the screen. We call the scroll
// method on the LayoutManager directly since it is not exposed by RecyclerView.
layoutManager.scrollToPositionWithOffset(lastScrollSection.appItem.position, 0);
@@ -489,20 +480,6 @@ public class AppsContainerRecyclerView extends BaseContainerRecyclerView {
return rowCount;
}
/**
* Forces a refresh of the scroll position to any scroll listener.
*/
private void refreshCurScrollPosition() {
List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
getCurScrollState(mScrollPosState, items);
if (mScrollPosState.rowIndex != -1) {
int predictionBarHeight = mApps.getPredictedApps().isEmpty() ? 0 : mPredictionBarHeight;
int scrollY = getPaddingTop() + (mScrollPosState.rowIndex * mScrollPosState.rowHeight) +
predictionBarHeight - mScrollPosState.rowTopOffset;
updateScrollY(scrollY);
}
}
/**
* Returns the current scroll state.
*/
+113 -95
View File
@@ -35,6 +35,7 @@ import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;
@@ -46,13 +47,98 @@ import java.util.List;
import java.util.regex.Pattern;
/**
* Interface for controlling the header elevation in response to RecyclerView scroll.
*/
interface HeaderElevationController {
void onScroll(int scrollY);
void disable();
}
/**
* Implementation of the header elevation mechanism for pre-L devices. It simulates elevation
* by drawing a gradient under the header bar.
*/
final class HeaderElevationControllerV16 implements HeaderElevationController {
private final View mShadow;
private final float mScrollToElevation;
public HeaderElevationControllerV16(View header) {
Resources res = header.getContext().getResources();
mScrollToElevation = res.getDimension(R.dimen.all_apps_header_scroll_to_elevation);
mShadow = new View(header.getContext());
mShadow.setBackground(new GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM, new int[] {0x44000000, 0x00000000}));
mShadow.setAlpha(0);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
res.getDimensionPixelSize(R.dimen.all_apps_header_shadow_height));
lp.topMargin = ((FrameLayout.LayoutParams) header.getLayoutParams()).height;
((ViewGroup) header.getParent()).addView(mShadow, lp);
}
@Override
public void onScroll(int scrollY) {
float elevationPct = (float) Math.min(scrollY, mScrollToElevation) /
mScrollToElevation;
mShadow.setAlpha(elevationPct);
}
@Override
public void disable() {
ViewGroup parent = (ViewGroup) mShadow.getParent();
if (parent != null) {
parent.removeView(mShadow);
}
}
}
/**
* Implementation of the header elevation mechanism for L+ devices, which makes use of the native
* view elevation.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
final class HeaderElevationControllerVL implements HeaderElevationController {
private final View mHeader;
private final float mMaxElevation;
private final float mScrollToElevation;
public HeaderElevationControllerVL(View header) {
mHeader = header;
Resources res = header.getContext().getResources();
mMaxElevation = res.getDimension(R.dimen.all_apps_header_max_elevation);
mScrollToElevation = res.getDimension(R.dimen.all_apps_header_scroll_to_elevation);
}
@Override
public void onScroll(int scrollY) {
float elevationPct = (float) Math.min(scrollY, mScrollToElevation) /
mScrollToElevation;
float newElevation = mMaxElevation * elevationPct;
if (Float.compare(mHeader.getElevation(), newElevation) != 0) {
mHeader.setElevation(newElevation);
}
}
@Override
public void disable() { }
}
/**
* The all apps view container.
*/
public class AppsContainerView extends BaseContainerView implements DragSource, Insettable,
TextWatcher, TextView.OnEditorActionListener, LauncherTransitionable,
AlphabeticalAppsList.FilterChangedCallback, AppsGridAdapter.PredictionBarSpacerCallbacks,
View.OnTouchListener, View.OnClickListener, View.OnLongClickListener {
View.OnTouchListener, View.OnClickListener, View.OnLongClickListener,
ViewTreeObserver.OnPreDrawListener {
public static final boolean GRID_MERGE_SECTIONS = true;
@@ -96,8 +182,7 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
// Normal container insets
private int mContainerInset;
private int mPredictionBarHeight;
// RecyclerView scroll position
@Thunk int mRecyclerViewScrollY;
private int mLastRecyclerViewScrollPos = -1;
private CheckLongPressHelper mPredictionIconCheckForLongPress;
private View mPredictionIconUnderTouch;
@@ -266,14 +351,6 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
mAppsRecyclerView.setLayoutManager(mLayoutManager);
mAppsRecyclerView.setAdapter(mAdapter);
mAppsRecyclerView.setHasFixedSize(true);
mAppsRecyclerView.setOnScrollListenerProxy(
new BaseContainerRecyclerView.OnScrollToListener() {
@Override
public void onScrolledTo(int x, int y) {
mRecyclerViewScrollY = y;
onRecyclerViewScrolled();
}
});
if (mItemDecoration != null) {
mAppsRecyclerView.addItemDecoration(mItemDecoration);
}
@@ -283,9 +360,7 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
@Override
public void onBindPredictionBar() {
if (!updatePredictionBarVisibility()) {
return;
}
updatePredictionBarVisibility();
List<AppInfo> predictedApps = mApps.getPredictedApps();
int childCount = mPredictionBarView.getChildCount();
@@ -400,6 +475,12 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
getRevealView().setBackground(background.getConstantState().newDrawable());
}
@Override
public boolean onPreDraw() {
synchronizeToRecyclerViewScrollPosition(mAppsRecyclerView.getScrollPosition());
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return handleTouchEvent(ev);
@@ -600,7 +681,11 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
@Override
public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) {
// Do nothing
// Register for a pre-draw listener to synchronize the recycler view scroll to other views
// in this container
if (!toWorkspace) {
getViewTreeObserver().addOnPreDrawListener(this);
}
}
@Override
@@ -620,18 +705,26 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
hideSearchField(false, false);
}
}
if (toWorkspace) {
getViewTreeObserver().removeOnPreDrawListener(this);
mLastRecyclerViewScrollPos = -1;
}
}
/**
* Updates the container when the recycler view is scrolled.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void onRecyclerViewScrolled() {
if (DYNAMIC_HEADER_ELEVATION) {
mElevationController.onScroll(mRecyclerViewScrollY);
}
private void synchronizeToRecyclerViewScrollPosition(int scrollY) {
if (mLastRecyclerViewScrollPos != scrollY) {
mLastRecyclerViewScrollPos = scrollY;
if (DYNAMIC_HEADER_ELEVATION) {
mElevationController.onScroll(scrollY);
}
mPredictionBarView.setTranslationY(-mRecyclerViewScrollY + mAppsRecyclerView.getPaddingTop());
// Scroll the prediction bar with the contents of the recycler view
mPredictionBarView.setTranslationY(-scrollY + mAppsRecyclerView.getPaddingTop());
}
}
@Override
@@ -867,79 +960,4 @@ public class AppsContainerView extends BaseContainerView implements DragSource,
private InputMethodManager getInputMethodManager() {
return (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
}
private static interface HeaderElevationController {
public void onScroll(int scrollY);
public void disable();
}
private static final class HeaderElevationControllerV16 implements HeaderElevationController {
private final View mShadow;
private final float mScrollToElevation;
public HeaderElevationControllerV16(View header) {
Resources res = header.getContext().getResources();
mScrollToElevation = res.getDimension(R.dimen.all_apps_header_scroll_to_elevation);
mShadow = new View(header.getContext());
mShadow.setBackground(new GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM, new int[] {0x44000000, 0x00000000}));
mShadow.setAlpha(0);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
LayoutParams.MATCH_PARENT,
res.getDimensionPixelSize(R.dimen.all_apps_header_shadow_height));
lp.topMargin = ((FrameLayout.LayoutParams) header.getLayoutParams()).height;
((ViewGroup) header.getParent()).addView(mShadow, lp);
}
@Override
public void onScroll(int scrollY) {
float elevationPct = (float) Math.min(scrollY, mScrollToElevation) /
mScrollToElevation;
mShadow.setAlpha(elevationPct);
}
@Override
public void disable() {
ViewGroup parent = (ViewGroup) mShadow.getParent();
if (parent != null) {
parent.removeView(mShadow);
}
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static final class HeaderElevationControllerVL implements HeaderElevationController {
private final View mHeader;
private final float mMaxElevation;
private final float mScrollToElevation;
public HeaderElevationControllerVL(View header) {
mHeader = header;
Resources res = header.getContext().getResources();
mMaxElevation = res.getDimension(R.dimen.all_apps_header_max_elevation);
mScrollToElevation = res.getDimension(R.dimen.all_apps_header_scroll_to_elevation);
}
@Override
public void onScroll(int scrollY) {
float elevationPct = (float) Math.min(scrollY, mScrollToElevation) /
mScrollToElevation;
float newElevation = mMaxElevation * elevationPct;
if (Float.compare(mHeader.getElevation(), newElevation) != 0) {
mHeader.setElevation(newElevation);
}
}
@Override
public void disable() { }
}
}
@@ -29,20 +29,11 @@ import com.android.launcher3.util.Thunk;
public class BaseContainerRecyclerView extends RecyclerView
implements RecyclerView.OnItemTouchListener {
/**
* Listener to get notified when the absolute scroll changes.
*/
public interface OnScrollToListener {
void onScrolledTo(int x, int y);
}
private static final int SCROLL_DELTA_THRESHOLD_DP = 4;
/** Keeps the last known scrolling delta/velocity along y-axis. */
@Thunk int mDy = 0;
@Thunk int mScrollY;
private float mDeltaThreshold;
private OnScrollToListener mScrollToListener;
public BaseContainerRecyclerView(Context context) {
this(context, null);
@@ -68,20 +59,9 @@ public class BaseContainerRecyclerView extends RecyclerView
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
mDy = dy;
mScrollY += dy;
if (mScrollToListener != null) {
mScrollToListener.onScrolledTo(0, mScrollY);
}
}
}
/**
* Sets an additional scroll listener, only needed for LMR1 version of the support lib.
*/
public void setOnScrollListenerProxy(OnScrollToListener listener) {
mScrollToListener = listener;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
@@ -105,17 +85,6 @@ public class BaseContainerRecyclerView extends RecyclerView
// DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS
}
/**
* Updates the scroll position, used to workaround a RecyclerView issue with scrolling to
* position.
*/
protected void updateScrollY(int scrollY) {
mScrollY = scrollY;
if (mScrollToListener != null) {
mScrollToListener.onScrolledTo(0, mScrollY);
}
}
/**
* Returns whether this {@link MotionEvent} should trigger the scroll to be stopped.
*/