Merge "Fix TabLayout when used in a HorizontalScrollView" into lmp-mr1-ub-dev
diff --git a/v7/appcompat/src/android/support/v7/app/AppCompatDelegateImplBase.java b/v7/appcompat/src/android/support/v7/app/AppCompatDelegateImplBase.java
index eb58dc5..411f17c 100644
--- a/v7/appcompat/src/android/support/v7/app/AppCompatDelegateImplBase.java
+++ b/v7/appcompat/src/android/support/v7/app/AppCompatDelegateImplBase.java
@@ -39,6 +39,7 @@
final Context mContext;
final Window mWindow;
final Window.Callback mOriginalWindowCallback;
+ final Window.Callback mAppCompatWindowCallback;
final AppCompatCallback mAppCompatCallback;
ActionBar mActionBar;
@@ -69,8 +70,9 @@
throw new IllegalStateException(
"AppCompat has already installed itself into the Window");
}
+ mAppCompatWindowCallback = wrapWindowCallback(mOriginalWindowCallback);
// Now install the new callback
- mWindow.setCallback(wrapWindowCallback(mOriginalWindowCallback));
+ mWindow.setCallback(mAppCompatWindowCallback);
}
abstract void initWindowDecorActionBar();
diff --git a/v7/appcompat/src/android/support/v7/app/AppCompatDelegateImplV7.java b/v7/appcompat/src/android/support/v7/app/AppCompatDelegateImplV7.java
index c777f57..2f0d5af 100644
--- a/v7/appcompat/src/android/support/v7/app/AppCompatDelegateImplV7.java
+++ b/v7/appcompat/src/android/support/v7/app/AppCompatDelegateImplV7.java
@@ -200,7 +200,7 @@
}
ToolbarActionBar tbab = new ToolbarActionBar(toolbar, ((Activity) mContext).getTitle(),
- mWindow);
+ mAppCompatWindowCallback);
setSupportActionBar(tbab);
mWindow.setCallback(tbab.getWrappedWindowCallback());
tbab.invalidateOptionsMenu();
@@ -1835,7 +1835,8 @@
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
- return AppCompatDelegateImplV7.this.dispatchKeyEvent(event);
+ return AppCompatDelegateImplV7.this.dispatchKeyEvent(event)
+ || super.dispatchKeyEvent(event);
}
@Override
diff --git a/v7/appcompat/src/android/support/v7/internal/app/ToolbarActionBar.java b/v7/appcompat/src/android/support/v7/internal/app/ToolbarActionBar.java
index 46bebe6..1781015 100644
--- a/v7/appcompat/src/android/support/v7/internal/app/ToolbarActionBar.java
+++ b/v7/appcompat/src/android/support/v7/internal/app/ToolbarActionBar.java
@@ -24,8 +24,8 @@
import android.support.v4.view.ViewCompat;
import android.support.v4.view.WindowCompat;
import android.support.v7.app.ActionBar;
-import android.support.v7.internal.view.WindowCallbackWrapper;
import android.support.v7.appcompat.R;
+import android.support.v7.internal.view.WindowCallbackWrapper;
import android.support.v7.internal.view.menu.ListMenuPresenter;
import android.support.v7.internal.view.menu.MenuBuilder;
import android.support.v7.internal.view.menu.MenuPresenter;
@@ -54,10 +54,8 @@
private boolean mMenuCallbackSet;
private boolean mLastMenuVisibility;
- private ArrayList<OnMenuVisibilityListener> mMenuVisibilityListeners =
- new ArrayList<OnMenuVisibilityListener>();
+ private ArrayList<OnMenuVisibilityListener> mMenuVisibilityListeners = new ArrayList<>();
- private Window mWindow;
private ListMenuPresenter mListMenuPresenter;
private final Runnable mMenuInvalidator = new Runnable() {
@@ -75,14 +73,12 @@
}
};
- public ToolbarActionBar(Toolbar toolbar, CharSequence title, Window window) {
+ public ToolbarActionBar(Toolbar toolbar, CharSequence title, Window.Callback callback) {
mDecorToolbar = new ToolbarWidgetWrapper(toolbar, false);
- mWindowCallback = new ToolbarCallbackWrapper(window.getCallback());
+ mWindowCallback = new ToolbarCallbackWrapper(callback);
mDecorToolbar.setWindowCallback(mWindowCallback);
toolbar.setOnMenuItemClickListener(mMenuClicker);
mDecorToolbar.setWindowTitle(title);
-
- mWindow = window;
}
public Window.Callback getWrappedWindowCallback() {
diff --git a/v7/recyclerview/api/current.txt b/v7/recyclerview/api/current.txt
index f48e8fe..a7becb4 100644
--- a/v7/recyclerview/api/current.txt
+++ b/v7/recyclerview/api/current.txt
@@ -625,7 +625,7 @@
method public int getChildPosition(android.view.View);
method public android.support.v7.widget.RecyclerView.LayoutManager getLayoutManager();
method public int getTargetPosition();
- method public void instantScrollToPosition(int);
+ method public deprecated void instantScrollToPosition(int);
method public boolean isPendingInitialRun();
method public boolean isRunning();
method protected void normalize(android.graphics.PointF);
@@ -646,6 +646,7 @@
method public int getDx();
method public int getDy();
method public android.view.animation.Interpolator getInterpolator();
+ method public void jumpTo(int);
method public void setDuration(int);
method public void setDx(int);
method public void setDy(int);
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
index c1e312c..f842ed5 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
@@ -29,6 +29,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
+import android.support.v7.widget.RecyclerView.LayoutParams;
import java.util.List;
@@ -175,8 +176,8 @@
* {@inheritDoc}
*/
@Override
- public RecyclerView.LayoutParams generateDefaultLayoutParams() {
- return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ public LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
@@ -1377,7 +1378,7 @@
result.mFinished = true;
return;
}
- RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
+ LayoutParams params = (LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
@@ -1587,7 +1588,7 @@
final View view = getChildAt(i);
final int position = getPosition(view);
if (position >= 0 && position < itemCount) {
- if (((RecyclerView.LayoutParams) view.getLayoutParams()).isItemRemoved()) {
+ if (((LayoutParams) view.getLayoutParams()).isItemRemoved()) {
if (invalidMatch == null) {
invalidMatch = view; // removed item, least preferred
}
@@ -1973,13 +1974,14 @@
private View nextViewFromScrapList() {
final int size = mScrapList.size();
for (int i = 0; i < size; i++) {
- final RecyclerView.ViewHolder viewHolder = mScrapList.get(i);
- if (viewHolder.isRemoved()) {
+ final View view = mScrapList.get(i).itemView;
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (lp.isItemRemoved()) {
continue;
}
- if (mCurrentPosition == viewHolder.getLayoutPosition()) {
- assignPositionFromScrapList(viewHolder);
- return viewHolder.itemView;
+ if (mCurrentPosition == lp.getViewLayoutPosition()) {
+ assignPositionFromScrapList(view);
+ return view;
}
}
return null;
@@ -1989,31 +1991,36 @@
assignPositionFromScrapList(null);
}
- public void assignPositionFromScrapList(RecyclerView.ViewHolder ignore) {
- RecyclerView.ViewHolder closest = nextViewHolderInLimitedList(ignore);
- mCurrentPosition = closest == null ? RecyclerView.NO_POSITION :
- closest.getLayoutPosition();
+ public void assignPositionFromScrapList(View ignore) {
+ final View closest = nextViewInLimitedList(ignore);
+ if (closest == null) {
+ mCurrentPosition = NO_POSITION;
+ } else {
+ mCurrentPosition = ((LayoutParams) closest.getLayoutParams())
+ .getViewLayoutPosition();
+ }
}
- public RecyclerView.ViewHolder nextViewHolderInLimitedList(RecyclerView.ViewHolder ignore) {
+ public View nextViewInLimitedList(View ignore) {
int size = mScrapList.size();
- RecyclerView.ViewHolder closest = null;
+ View closest = null;
int closestDistance = Integer.MAX_VALUE;
if (DEBUG && mIsPreLayout) {
throw new IllegalStateException("Scrap list cannot be used in pre layout");
}
for (int i = 0; i < size; i++) {
- RecyclerView.ViewHolder viewHolder = mScrapList.get(i);
- if (viewHolder == ignore || viewHolder.isRemoved()) {
+ View view = mScrapList.get(i).itemView;
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (view == ignore || lp.isItemRemoved()) {
continue;
}
- final int distance = (viewHolder.getLayoutPosition() - mCurrentPosition) *
+ final int distance = (lp.getViewLayoutPosition() - mCurrentPosition) *
mItemDirection;
if (distance < 0) {
continue; // item is not in current direction
}
if (distance < closestDistance) {
- closest = viewHolder;
+ closest = view;
closestDistance = distance;
if (distance == 0) {
break;
@@ -2120,7 +2127,7 @@
}
private boolean isViewValidAsAnchor(View child, RecyclerView.State state) {
- RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
return !lp.isItemRemoved() && lp.getViewLayoutPosition() >= 0
&& lp.getViewLayoutPosition() < state.getItemCount();
}
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearSmoothScroller.java b/v7/recyclerview/src/android/support/v7/widget/LinearSmoothScroller.java
index ed4c950..cda5cd1 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LinearSmoothScroller.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearSmoothScroller.java
@@ -229,8 +229,8 @@
+ "LayoutManager#computeScrollVectorForPosition.\n"
+ "Falling back to instant scroll");
final int target = getTargetPosition();
+ action.jumpTo(target);
stop();
- instantScrollToPosition(target);
return;
}
normalize(scrollVector);
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index 108e68e..d884caf 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -1271,6 +1271,14 @@
awakenScrollBars();
}
+ private void jumpToPositionForSmoothScroller(int position) {
+ if (mLayout == null) {
+ return;
+ }
+ mLayout.scrollToPosition(position);
+ awakenScrollBars();
+ }
+
/**
* Starts a smooth scroll to an adapter position.
* <p>
@@ -3797,6 +3805,8 @@
}
}
}
+ onExitLayoutOrScroll();
+ resumeRequestLayout(false);
if (smoothScroller != null && !smoothScroller.isPendingInitialRun() &&
smoothScroller.isRunning()) {
@@ -3810,8 +3820,6 @@
smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
}
}
- onExitLayoutOrScroll();
- resumeRequestLayout(false);
}
if (!mItemDecorations.isEmpty()) {
invalidate();
@@ -3864,8 +3872,13 @@
}
}
// call this after the onAnimation is complete not to have inconsistent callbacks etc.
- if (smoothScroller != null && smoothScroller.isPendingInitialRun()) {
- smoothScroller.onAnimation(0, 0);
+ if (smoothScroller != null) {
+ if (smoothScroller.isPendingInitialRun()) {
+ smoothScroller.onAnimation(0, 0);
+ }
+ if (!mReSchedulePostAnimationCallback) {
+ smoothScroller.stop(); //stop if it does not trigger any scroll
+ }
}
enableRunOnAnimationRequests();
}
@@ -8571,8 +8584,10 @@
}
/**
- * @return The LayoutManager to which this SmoothScroller is attached
+ * @return The LayoutManager to which this SmoothScroller is attached. Will return
+ * <code>null</code> after the SmoothScroller is stopped.
*/
+ @Nullable
public LayoutManager getLayoutManager() {
return mLayoutManager;
}
@@ -8630,15 +8645,16 @@
}
private void onAnimation(int dx, int dy) {
- if (!mRunning || mTargetPosition == RecyclerView.NO_POSITION) {
+ final RecyclerView recyclerView = mRecyclerView;
+ if (!mRunning || mTargetPosition == RecyclerView.NO_POSITION || recyclerView == null) {
stop();
}
mPendingInitialRun = false;
if (mTargetView != null) {
// verify target position
if (getChildPosition(mTargetView) == mTargetPosition) {
- onTargetFound(mTargetView, mRecyclerView.mState, mRecyclingAction);
- mRecyclingAction.runIfNecessary(mRecyclerView);
+ onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction);
+ mRecyclingAction.runIfNecessary(recyclerView);
stop();
} else {
Log.e(TAG, "Passed over target position while smooth scrolling.");
@@ -8646,8 +8662,18 @@
}
}
if (mRunning) {
- onSeekTargetStep(dx, dy, mRecyclerView.mState, mRecyclingAction);
- mRecyclingAction.runIfNecessary(mRecyclerView);
+ onSeekTargetStep(dx, dy, recyclerView.mState, mRecyclingAction);
+ boolean hadJumpTarget = mRecyclingAction.hasJumpTarget();
+ mRecyclingAction.runIfNecessary(recyclerView);
+ if (hadJumpTarget) {
+ // It is not stopped so needs to be restarted
+ if (mRunning) {
+ mPendingInitialRun = true;
+ recyclerView.mViewFlinger.postOnAnimation();
+ } else {
+ stop(); // done
+ }
+ }
}
}
@@ -8674,7 +8700,9 @@
/**
* @see RecyclerView#scrollToPosition(int)
+ * @deprecated Use {@link Action#jumpTo(int)}.
*/
+ @Deprecated
public void instantScrollToPosition(int position) {
mRecyclerView.scrollToPosition(position);
}
@@ -8748,6 +8776,8 @@
private int mDuration;
+ private int mJumpToPosition = NO_POSITION;
+
private Interpolator mInterpolator;
private boolean changed = false;
@@ -8786,7 +8816,38 @@
mDuration = duration;
mInterpolator = interpolator;
}
+
+ /**
+ * Instead of specifying pixels to scroll, use the target position to jump using
+ * {@link RecyclerView#scrollToPosition(int)}.
+ * <p>
+ * You may prefer using this method if scroll target is really far away and you prefer
+ * to jump to a location and smooth scroll afterwards.
+ * <p>
+ * Note that calling this method takes priority over other update methods such as
+ * {@link #update(int, int, int, Interpolator)}, {@link #setX(float)},
+ * {@link #setY(float)} and #{@link #setInterpolator(Interpolator)}. If you call
+ * {@link #jumpTo(int)}, the other changes will not be considered for this animation
+ * frame.
+ *
+ * @param targetPosition The target item position to scroll to using instant scrolling.
+ */
+ public void jumpTo(int targetPosition) {
+ mJumpToPosition = targetPosition;
+ }
+
+ boolean hasJumpTarget() {
+ return mJumpToPosition >= 0;
+ }
+
private void runIfNecessary(RecyclerView recyclerView) {
+ if (mJumpToPosition >= 0) {
+ final int position = mJumpToPosition;
+ mJumpToPosition = NO_POSITION;
+ recyclerView.jumpToPositionForSmoothScroller(position);
+ changed = false;
+ return;
+ }
if (changed) {
validate();
if (mInterpolator == null) {
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
index 630c12d..519520a 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
@@ -48,9 +48,11 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import static android.support.v7.widget.RecyclerView.NO_POSITION;
import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;
import static android.support.v7.widget.RecyclerView.SCROLL_STATE_DRAGGING;
import static android.support.v7.widget.RecyclerView.SCROLL_STATE_SETTLING;
+import static android.support.v7.widget.RecyclerView.getChildViewHolderInt;
import android.support.test.runner.AndroidJUnit4;
@@ -291,7 +293,7 @@
private void scrollInOtherOrientationTest(int flags)
throws Throwable {
- scrollInOtherOrientationTest(flags);
+ scrollInOtherOrientationTest(flags, flags);
}
private void scrollInOtherOrientationTest(final int flags, int expectedFlags) throws Throwable {
@@ -2792,6 +2794,152 @@
assertEquals(addItemDecors ? -30 : -20, scrollDist.get());
}
+ @Test
+ public void testUnimplementedSmoothScroll() throws Throwable {
+ final AtomicInteger receivedScrollToPosition = new AtomicInteger(-1);
+ final AtomicInteger receivedSmoothScrollToPosition = new AtomicInteger(-1);
+ final CountDownLatch cbLatch = new CountDownLatch(2);
+ TestLayoutManager tlm = new TestLayoutManager() {
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ detachAndScrapAttachedViews(recycler);
+ layoutRange(recycler, 0, 10);
+ layoutLatch.countDown();
+ }
+
+ @Override
+ public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
+ int position) {
+ assertEquals(-1, receivedSmoothScrollToPosition.get());
+ receivedSmoothScrollToPosition.set(position);
+ RecyclerView.SmoothScroller ss =
+ new LinearSmoothScroller(recyclerView.getContext()) {
+ @Override
+ public PointF computeScrollVectorForPosition(int targetPosition) {
+ return null;
+ }
+ };
+ ss.setTargetPosition(position);
+ startSmoothScroll(ss);
+ cbLatch.countDown();
+ }
+
+ @Override
+ public void scrollToPosition(int position) {
+ assertEquals(-1, receivedScrollToPosition.get());
+ receivedScrollToPosition.set(position);
+ cbLatch.countDown();
+ }
+ };
+ RecyclerView rv = new RecyclerView(getActivity());
+ rv.setAdapter(new TestAdapter(100));
+ rv.setLayoutManager(tlm);
+ tlm.expectLayouts(1);
+ setRecyclerView(rv);
+ tlm.waitForLayout(2);
+ smoothScrollToPosition(35);
+ assertTrue("both scrolls should be called", cbLatch.await(3, TimeUnit.SECONDS));
+ checkForMainThreadException();
+ assertEquals(35, receivedSmoothScrollToPosition.get());
+ assertEquals(35, receivedScrollToPosition.get());
+ }
+
+ @Test
+ public void testJumpingJackSmoothScroller() throws Throwable {
+ jumpingJackSmoothScrollerTest(true);
+ }
+
+ @Test
+ public void testJumpingJackSmoothScrollerGoesIdle() throws Throwable {
+ jumpingJackSmoothScrollerTest(false);
+ }
+
+ private void jumpingJackSmoothScrollerTest(final boolean succeed) throws Throwable {
+ final List<Integer> receivedScrollToPositions = new ArrayList<>();
+ final TestAdapter testAdapter = new TestAdapter(200);
+ final AtomicBoolean mTargetFound = new AtomicBoolean(false);
+ TestLayoutManager tlm = new TestLayoutManager() {
+ int pendingScrollPosition = -1;
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ detachAndScrapAttachedViews(recycler);
+ final int pos = pendingScrollPosition < 0 ? 0: pendingScrollPosition;
+ layoutRange(recycler, pos, pos + 10);
+ if (layoutLatch != null) {
+ layoutLatch.countDown();
+ }
+ }
+
+ @Override
+ public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
+ final int position) {
+ RecyclerView.SmoothScroller ss =
+ new LinearSmoothScroller(recyclerView.getContext()) {
+ @Override
+ public PointF computeScrollVectorForPosition(int targetPosition) {
+ return new PointF(0, 1);
+ }
+
+ @Override
+ protected void onTargetFound(View targetView, RecyclerView.State state,
+ Action action) {
+ super.onTargetFound(targetView, state, action);
+ mTargetFound.set(true);
+ }
+
+ @Override
+ protected void updateActionForInterimTarget(Action action) {
+ int limit = succeed ? getTargetPosition() : 100;
+ if (pendingScrollPosition + 2 < limit) {
+ if (pendingScrollPosition != NO_POSITION) {
+ assertEquals(pendingScrollPosition,
+ getChildViewHolderInt(getChildAt(0))
+ .getAdapterPosition());
+ }
+ action.jumpTo(pendingScrollPosition + 2);
+ }
+ }
+ };
+ ss.setTargetPosition(position);
+ startSmoothScroll(ss);
+ }
+
+ @Override
+ public void scrollToPosition(int position) {
+ receivedScrollToPositions.add(position);
+ pendingScrollPosition = position;
+ requestLayout();
+ }
+ };
+ final RecyclerView rv = new RecyclerView(getActivity());
+ rv.setAdapter(testAdapter);
+ rv.setLayoutManager(tlm);
+
+ tlm.expectLayouts(1);
+ setRecyclerView(rv);
+ tlm.waitForLayout(2);
+
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ rv.smoothScrollToPosition(150);
+ }
+ });
+ int limit = 100;
+ while (rv.getLayoutManager().isSmoothScrolling() && --limit > 0) {
+ Thread.sleep(200);
+ checkForMainThreadException();
+ }
+ checkForMainThreadException();
+ assertTrue(limit > 0);
+ for (int i = 1; i < 100; i+=2) {
+ assertTrue("scroll positions must include " + i, receivedScrollToPositions.contains(i));
+ }
+
+ assertEquals(succeed, mTargetFound.get());
+
+ }
+
private static class TestViewHolder2 extends RecyclerView.ViewHolder {
Object mData;