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:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user