blob: 3bfaf3ddc55d8382cb3fc380433d16b8f66892db [file] [log] [blame]
/*
* Copyright 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.viewpager2.widget;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import androidx.core.view.accessibility.AccessibilityViewCommand;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.PagerSnapHelper;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.Adapter;
import androidx.recyclerview.widget.RecyclerView.ItemDecoration;
import androidx.viewpager2.R;
import androidx.viewpager2.adapter.StatefulAdapter;
import java.lang.annotation.Retention;
/**
* ViewPager2 replaces {@link androidx.viewpager.widget.ViewPager}, addressing most of its
* predecessor’s pain-points, including right-to-left layout support, vertical orientation,
* modifiable Fragment collections, etc.
*
* @see androidx.viewpager.widget.ViewPager
*/
public final class ViewPager2 extends ViewGroup {
/** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Retention(SOURCE)
@IntDef({ORIENTATION_HORIZONTAL, ORIENTATION_VERTICAL})
public @interface Orientation {
}
public static final int ORIENTATION_HORIZONTAL = RecyclerView.HORIZONTAL;
public static final int ORIENTATION_VERTICAL = RecyclerView.VERTICAL;
/** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Retention(SOURCE)
@IntDef({SCROLL_STATE_IDLE, SCROLL_STATE_DRAGGING, SCROLL_STATE_SETTLING})
public @interface ScrollState {
}
/** @hide */
@SuppressWarnings("WeakerAccess")
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Retention(SOURCE)
@IntDef({OFFSCREEN_PAGE_LIMIT_DEFAULT})
@IntRange(from = 1)
public @interface OffscreenPageLimit {
}
/**
* Indicates that the ViewPager2 is in an idle, settled state. The current page
* is fully in view and no animation is in progress.
*/
public static final int SCROLL_STATE_IDLE = 0;
/**
* Indicates that the ViewPager2 is currently being dragged by the user, or programmatically
* via fake drag functionality.
*/
public static final int SCROLL_STATE_DRAGGING = 1;
/**
* Indicates that the ViewPager2 is in the process of settling to a final position.
*/
public static final int SCROLL_STATE_SETTLING = 2;
/**
* Value to indicate that the default caching mechanism of RecyclerView should be used instead
* of explicitly prefetch and retain pages to either side of the current page.
* @see #setOffscreenPageLimit(int)
*/
public static final int OFFSCREEN_PAGE_LIMIT_DEFAULT = -1;
/** Feature flag while stabilizing enhanced a11y */
static boolean sFeatureEnhancedA11yEnabled = true;
// reused in layout(...)
private final Rect mTmpContainerRect = new Rect();
private final Rect mTmpChildRect = new Rect();
private CompositeOnPageChangeCallback mExternalPageChangeCallbacks =
new CompositeOnPageChangeCallback(3);
int mCurrentItem;
boolean mCurrentItemDirty = false;
private RecyclerView.AdapterDataObserver mCurrentItemDataSetChangeObserver =
new DataSetChangeObserver() {
@Override
public void onChanged() {
mCurrentItemDirty = true;
mScrollEventAdapter.notifyDataSetChangeHappened();
}
};
private LinearLayoutManager mLayoutManager;
private int mPendingCurrentItem = NO_POSITION;
private Parcelable mPendingAdapterState;
RecyclerView mRecyclerView;
private PagerSnapHelper mPagerSnapHelper;
ScrollEventAdapter mScrollEventAdapter;
private CompositeOnPageChangeCallback mPageChangeEventDispatcher;
private FakeDrag mFakeDragger;
private PageTransformerAdapter mPageTransformerAdapter;
private RecyclerView.ItemAnimator mSavedItemAnimator = null;
private boolean mSavedItemAnimatorPresent = false;
private boolean mUserInputEnabled = true;
private @OffscreenPageLimit int mOffscreenPageLimit = OFFSCREEN_PAGE_LIMIT_DEFAULT;
AccessibilityProvider mAccessibilityProvider; // to avoid creation of a synthetic accessor
public ViewPager2(@NonNull Context context) {
super(context);
initialize(context, null);
}
public ViewPager2(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs);
}
public ViewPager2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize(context, attrs);
}
@RequiresApi(21)
public ViewPager2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize(context, attrs);
}
private void initialize(Context context, AttributeSet attrs) {
mAccessibilityProvider = sFeatureEnhancedA11yEnabled
? new PageAwareAccessibilityProvider()
: new BasicAccessibilityProvider();
mRecyclerView = new RecyclerViewImpl(context);
mRecyclerView.setId(ViewCompat.generateViewId());
mLayoutManager = new LinearLayoutManagerImpl(context);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);
setOrientation(context, attrs);
mRecyclerView.setLayoutParams(
new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
mRecyclerView.addOnChildAttachStateChangeListener(enforceChildFillListener());
// Create ScrollEventAdapter before attaching PagerSnapHelper to RecyclerView, because the
// attach process calls PagerSnapHelperImpl.findSnapView, which uses the mScrollEventAdapter
mScrollEventAdapter = new ScrollEventAdapter(this);
// Create FakeDrag before attaching PagerSnapHelper, same reason as above
mFakeDragger = new FakeDrag(this, mScrollEventAdapter, mRecyclerView);
mPagerSnapHelper = new PagerSnapHelperImpl();
mPagerSnapHelper.attachToRecyclerView(mRecyclerView);
// Add mScrollEventAdapter after attaching mPagerSnapHelper to mRecyclerView, because we
// don't want to respond on the events sent out during the attach process
mRecyclerView.addOnScrollListener(mScrollEventAdapter);
mPageChangeEventDispatcher = new CompositeOnPageChangeCallback(3);
mScrollEventAdapter.setOnPageChangeCallback(mPageChangeEventDispatcher);
// Callback that updates mCurrentItem after swipes. Also triggered in other cases, but in
// all those cases mCurrentItem will only be overwritten with the same value.
final OnPageChangeCallback currentItemUpdater = new OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
if (mCurrentItem != position) {
mCurrentItem = position;
mAccessibilityProvider.onSetNewCurrentItem();
}
}
@Override
public void onPageScrollStateChanged(int newState) {
if (newState == SCROLL_STATE_IDLE) {
updateCurrentItem();
}
}
};
// Add currentItemUpdater before mExternalPageChangeCallbacks, because we need to update
// internal state first
mPageChangeEventDispatcher.addOnPageChangeCallback(currentItemUpdater);
// Allow a11y to register its listeners just after currentItemUpdater (so it has the
// right data). TODO: replace ordering comments with a test.
mAccessibilityProvider.onInitialize(mPageChangeEventDispatcher, mRecyclerView);
mPageChangeEventDispatcher.addOnPageChangeCallback(mExternalPageChangeCallbacks);
// Add mPageTransformerAdapter after mExternalPageChangeCallbacks, because page transform
// events must be fired after scroll events
mPageTransformerAdapter = new PageTransformerAdapter(mLayoutManager);
mPageChangeEventDispatcher.addOnPageChangeCallback(mPageTransformerAdapter);
attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams());
}
/**
* A lot of places in code rely on an assumption that the page fills the whole ViewPager2.
*
* TODO(b/70666617) Allow page width different than width/height 100%/100%
*/
private RecyclerView.OnChildAttachStateChangeListener enforceChildFillListener() {
return new RecyclerView.OnChildAttachStateChangeListener() {
@Override
public void onChildViewAttachedToWindow(@NonNull View view) {
RecyclerView.LayoutParams layoutParams =
(RecyclerView.LayoutParams) view.getLayoutParams();
if (layoutParams.width != LayoutParams.MATCH_PARENT
|| layoutParams.height != LayoutParams.MATCH_PARENT) {
throw new IllegalStateException(
"Pages must fill the whole ViewPager2 (use match_parent)");
}
}
@Override
public void onChildViewDetachedFromWindow(@NonNull View view) {
// nothing
}
};
}
@RequiresApi(23)
@Override
public CharSequence getAccessibilityClassName() {
if (mAccessibilityProvider.handlesGetAccessibilityClassName()) {
return mAccessibilityProvider.onGetAccessibilityClassName();
}
return super.getAccessibilityClassName();
}
private void setOrientation(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewPager2);
if (Build.VERSION.SDK_INT >= 29) {
saveAttributeDataForStyleable(context, R.styleable.ViewPager2, attrs, a, 0, 0);
}
try {
setOrientation(
a.getInt(R.styleable.ViewPager2_android_orientation, ORIENTATION_HORIZONTAL));
} finally {
a.recycle();
}
}
@SuppressWarnings("ConstantConditions")
@Nullable
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.mRecyclerViewId = mRecyclerView.getId();
ss.mCurrentItem = mPendingCurrentItem == NO_POSITION ? mCurrentItem : mPendingCurrentItem;
if (mPendingAdapterState != null) {
ss.mAdapterState = mPendingAdapterState;
} else {
Adapter<?> adapter = mRecyclerView.getAdapter();
if (adapter instanceof StatefulAdapter) {
ss.mAdapterState = ((StatefulAdapter) adapter).saveState();
}
}
return ss;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
mPendingCurrentItem = ss.mCurrentItem;
mPendingAdapterState = ss.mAdapterState;
}
private void restorePendingState() {
if (mPendingCurrentItem == NO_POSITION) {
// No state to restore, or state is already restored
return;
}
Adapter<?> adapter = getAdapter();
if (adapter == null) {
return;
}
if (mPendingAdapterState != null) {
if (adapter instanceof StatefulAdapter) {
((StatefulAdapter) adapter).restoreState(mPendingAdapterState);
}
mPendingAdapterState = null;
}
// Now we have an adapter, we can clamp the pending current item and set it
mCurrentItem = Math.max(0, Math.min(mPendingCurrentItem, adapter.getItemCount() - 1));
mPendingCurrentItem = NO_POSITION;
mRecyclerView.scrollToPosition(mCurrentItem);
mAccessibilityProvider.onRestorePendingState();
}
@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
// RecyclerView changed an id, so we need to reflect that in the saved state
Parcelable state = container.get(getId());
if (state instanceof SavedState) {
final int previousRvId = ((SavedState) state).mRecyclerViewId;
final int currentRvId = mRecyclerView.getId();
container.put(currentRvId, container.get(previousRvId));
container.remove(previousRvId);
}
super.dispatchRestoreInstanceState(container);
// State of ViewPager2 and its child (RecyclerView) has been restored now
restorePendingState();
}
static class SavedState extends BaseSavedState {
int mRecyclerViewId;
int mCurrentItem;
Parcelable mAdapterState;
@RequiresApi(24)
SavedState(Parcel source, ClassLoader loader) {
super(source, loader);
readValues(source, loader);
}
SavedState(Parcel source) {
super(source);
readValues(source, null);
}
SavedState(Parcelable superState) {
super(superState);
}
private void readValues(Parcel source, ClassLoader loader) {
mRecyclerViewId = source.readInt();
mCurrentItem = source.readInt();
mAdapterState = source.readParcelable(loader);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(mRecyclerViewId);
out.writeInt(mCurrentItem);
out.writeParcelable(mAdapterState, flags);
}
public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel source, ClassLoader loader) {
return Build.VERSION.SDK_INT >= 24
? new SavedState(source, loader)
: new SavedState(source);
}
@Override
public SavedState createFromParcel(Parcel source) {
return createFromParcel(source, null);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
/**
* <p>Set a new adapter to provide page views on demand.</p>
*
* <p>If you're planning to use {@link androidx.fragment.app.Fragment Fragments} as pages,
* implement {@link androidx.viewpager2.adapter.FragmentStateAdapter FragmentStateAdapter}. If
* your pages are Views, implement {@link RecyclerView.Adapter} as usual.</p>
*
* <p>If your pages contain LayoutTransitions, then those LayoutTransitions <em>must</em> have
* {@code animateParentHierarchy} set to {@code false}. Note that if you have a ViewGroup with
* {@code animateLayoutChanges="true"} in your layout xml file, a LayoutTransition is added
* automatically to that ViewGroup. You will need to manually call {@link
* android.animation.LayoutTransition#setAnimateParentHierarchy(boolean)
* getLayoutTransition().setAnimateParentHierarchy(false)} on that ViewGroup after you inflated
* the xml layout, like this:</p>
*
* <pre>
* View view = layoutInflater.inflate(R.layout.page, parent, false);
* ViewGroup viewGroup = view.findViewById(R.id.animated_viewgroup);
* viewGroup.getLayoutTransition().setAnimateParentHierarchy(false);
* </pre>
*
* @param adapter The adapter to use, or {@code null} to remove the current adapter
* @see androidx.viewpager2.adapter.FragmentStateAdapter
* @see RecyclerView#setAdapter(Adapter)
*/
public void setAdapter(@Nullable @SuppressWarnings("rawtypes") Adapter adapter) {
final Adapter<?> currentAdapter = mRecyclerView.getAdapter();
mAccessibilityProvider.onDetachAdapter(currentAdapter);
unregisterCurrentItemDataSetTracker(currentAdapter);
mRecyclerView.setAdapter(adapter);
mCurrentItem = 0;
restorePendingState();
mAccessibilityProvider.onAttachAdapter(adapter);
registerCurrentItemDataSetTracker(adapter);
}
private void registerCurrentItemDataSetTracker(@Nullable Adapter<?> adapter) {
if (adapter != null) {
adapter.registerAdapterDataObserver(mCurrentItemDataSetChangeObserver);
}
}
private void unregisterCurrentItemDataSetTracker(@Nullable Adapter<?> adapter) {
if (adapter != null) {
adapter.unregisterAdapterDataObserver(mCurrentItemDataSetChangeObserver);
}
}
@SuppressWarnings("rawtypes")
public @Nullable Adapter getAdapter() {
return mRecyclerView.getAdapter();
}
@Override
public void onViewAdded(View child) {
// TODO(b/70666620): consider adding a support for Decor views
throw new IllegalStateException(
getClass().getSimpleName() + " does not support direct child views");
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO(b/70666622): consider margin support
// TODO(b/70666626): consider delegating all this to RecyclerView
// TODO(b/70666625): write automated tests for this
measureChild(mRecyclerView, widthMeasureSpec, heightMeasureSpec);
int width = mRecyclerView.getMeasuredWidth();
int height = mRecyclerView.getMeasuredHeight();
int childState = mRecyclerView.getMeasuredState();
width += getPaddingLeft() + getPaddingRight();
height += getPaddingTop() + getPaddingBottom();
width = Math.max(width, getSuggestedMinimumWidth());
height = Math.max(height, getSuggestedMinimumHeight());
setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState),
resolveSizeAndState(height, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int width = mRecyclerView.getMeasuredWidth();
int height = mRecyclerView.getMeasuredHeight();
// TODO(b/70666626): consider delegating padding handling to the RecyclerView to avoid
// an unnatural page transition effect: http://shortn/_Vnug3yZpQT
mTmpContainerRect.left = getPaddingLeft();
mTmpContainerRect.right = r - l - getPaddingRight();
mTmpContainerRect.top = getPaddingTop();
mTmpContainerRect.bottom = b - t - getPaddingBottom();
Gravity.apply(Gravity.TOP | Gravity.START, width, height, mTmpContainerRect, mTmpChildRect);
mRecyclerView.layout(mTmpChildRect.left, mTmpChildRect.top, mTmpChildRect.right,
mTmpChildRect.bottom);
if (mCurrentItemDirty) {
updateCurrentItem();
}
}
/** Updates {@link #mCurrentItem} based on what is currently visible in the viewport. */
void updateCurrentItem() {
if (mPagerSnapHelper == null) {
throw new IllegalStateException("Design assumption violated.");
}
View snapView = mPagerSnapHelper.findSnapView(mLayoutManager);
if (snapView == null) {
return; // nothing we can do
}
int snapPosition = mLayoutManager.getPosition(snapView);
if (snapPosition != mCurrentItem && getScrollState() == SCROLL_STATE_IDLE) {
/** TODO: revisit if push to {@link ScrollEventAdapter} / separate component */
mPageChangeEventDispatcher.onPageSelected(snapPosition);
}
mCurrentItemDirty = false;
}
int getPageSize() {
final RecyclerView rv = mRecyclerView;
return getOrientation() == ORIENTATION_HORIZONTAL
? rv.getWidth() - rv.getPaddingLeft() - rv.getPaddingRight()
: rv.getHeight() - rv.getPaddingTop() - rv.getPaddingBottom();
}
/**
* Sets the orientation of the ViewPager2.
*
* @param orientation {@link #ORIENTATION_HORIZONTAL} or {@link #ORIENTATION_VERTICAL}
*/
public void setOrientation(@Orientation int orientation) {
mLayoutManager.setOrientation(orientation);
mAccessibilityProvider.onSetOrientation();
}
public @Orientation int getOrientation() {
return mLayoutManager.getOrientation();
}
boolean isRtl() {
return mLayoutManager.getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL;
}
/**
* Set the currently selected page. If the ViewPager has already been through its first
* layout with its current adapter there will be a smooth animated transition between
* the current item and the specified item. Silently ignored if the adapter is not set or
* empty. Clamps item to the bounds of the adapter.
*
* TODO(b/123069219): verify first layout behavior
*
* @param item Item index to select
*/
public void setCurrentItem(int item) {
setCurrentItem(item, true);
}
/**
* Set the currently selected page. If {@code smoothScroll = true}, will perform a smooth
* animation from the current item to the new item. Silently ignored if the adapter is not set
* or empty. Clamps item to the bounds of the adapter.
*
* @param item Item index to select
* @param smoothScroll True to smoothly scroll to the new item, false to transition immediately
*/
public void setCurrentItem(int item, boolean smoothScroll) {
if (isFakeDragging()) {
throw new IllegalStateException("Cannot change current item when ViewPager2 is fake "
+ "dragging");
}
setCurrentItemInternal(item, smoothScroll);
}
void setCurrentItemInternal(int item, boolean smoothScroll) {
// 1. Preprocessing (check state, validate item, decide if update is necessary, etc)
Adapter<?> adapter = getAdapter();
if (adapter == null) {
// Update the pending current item if we're still waiting for the adapter
if (mPendingCurrentItem != NO_POSITION) {
mPendingCurrentItem = Math.max(item, 0);
}
return;
}
if (adapter.getItemCount() <= 0) {
// Adapter is empty
return;
}
item = Math.max(item, 0);
item = Math.min(item, adapter.getItemCount() - 1);
if (item == mCurrentItem && mScrollEventAdapter.isIdle()) {
// Already at the correct page
return;
}
if (item == mCurrentItem && smoothScroll) {
// Already scrolling to the correct page, but not yet there. Only handle instant scrolls
// because then we need to interrupt the current smooth scroll.
return;
}
// 2. Update the item internally
double previousItem = mCurrentItem;
mCurrentItem = item;
mAccessibilityProvider.onSetNewCurrentItem();
if (!mScrollEventAdapter.isIdle()) {
// Scroll in progress, overwrite previousItem with actual current position
previousItem = mScrollEventAdapter.getRelativeScrollPosition();
}
// 3. Perform the necessary scroll actions on RecyclerView
mScrollEventAdapter.notifyProgrammaticScroll(item, smoothScroll);
if (!smoothScroll) {
mRecyclerView.scrollToPosition(item);
return;
}
// For smooth scroll, pre-jump to nearby item for long jumps.
if (Math.abs(item - previousItem) > 3) {
mRecyclerView.scrollToPosition(item > previousItem ? item - 3 : item + 3);
// TODO(b/114361680): call smoothScrollToPosition synchronously (blocked by b/114019007)
mRecyclerView.post(new SmoothScrollToPosition(item, mRecyclerView));
} else {
mRecyclerView.smoothScrollToPosition(item);
}
}
/**
* Returns the currently selected page. If no page can sensibly be selected because there is no
* adapter or the adapter is empty, returns 0.
*
* @return Currently selected page
*/
public int getCurrentItem() {
return mCurrentItem;
}
/**
* Returns the current scroll state of the ViewPager2. Returned value is one of can be one of
* {@link #SCROLL_STATE_IDLE}, {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}.
*
* @return The scroll state that was last dispatched to {@link
* OnPageChangeCallback#onPageScrollStateChanged(int)}
*/
@ScrollState
public int getScrollState() {
return mScrollEventAdapter.getScrollState();
}
/**
* Start a fake drag of the pager.
*
* <p>A fake drag can be useful if you want to synchronize the motion of the ViewPager2 with the
* touch scrolling of another view, while still letting the ViewPager2 control the snapping
* motion and fling behavior. (e.g. parallax-scrolling tabs.) Call {@link #fakeDragBy(float)} to
* simulate the actual drag motion. Call {@link #endFakeDrag()} to complete the fake drag and
* fling as necessary.
*
* <p>A fake drag can be interrupted by a real drag. From that point on, all calls to {@code
* fakeDragBy} and {@code endFakeDrag} will be ignored until the next fake drag is started by
* calling {@code beginFakeDrag}. If you need the ViewPager2 to ignore touch events and other
* user input during a fake drag, use {@link #setUserInputEnabled(boolean)}. If a real or fake
* drag is already in progress, this method will return {@code false}.
*
* @return {@code true} if the fake drag began successfully, {@code false} if it could not be
* started
*
* @see #fakeDragBy(float)
* @see #endFakeDrag()
* @see #isFakeDragging()
*/
public boolean beginFakeDrag() {
return mFakeDragger.beginFakeDrag();
}
/**
* Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first. Drag
* happens in the direction of the orientation. Positive offsets will drag to the previous page,
* negative values to the next page, with one exception: if layout direction is set to RTL and
* the ViewPager2's orientation is horizontal, then the behavior will be inverted. This matches
* the deltas of touch events that would cause the same real drag.
*
* <p>If the pager is not in the fake dragging state anymore, it ignores this call and returns
* {@code false}.
*
* @param offsetPxFloat Offset in pixels to drag by
* @return {@code true} if the fake drag was executed. If {@code false} is returned, it means
* there was no fake drag to end.
*
* @see #beginFakeDrag()
* @see #endFakeDrag()
* @see #isFakeDragging()
*/
public boolean fakeDragBy(@SuppressLint("SupportAnnotationUsage") @Px float offsetPxFloat) {
return mFakeDragger.fakeDragBy(offsetPxFloat);
}
/**
* End a fake drag of the pager.
*
* @return {@code true} if the fake drag was ended. If {@code false} is returned, it means there
* was no fake drag to end.
*
* @see #beginFakeDrag()
* @see #fakeDragBy(float)
* @see #isFakeDragging()
*/
public boolean endFakeDrag() {
return mFakeDragger.endFakeDrag();
}
/**
* Returns {@code true} if a fake drag is in progress.
*
* @return {@code true} if currently in a fake drag, {@code false} otherwise.
* @see #beginFakeDrag()
* @see #fakeDragBy(float)
* @see #endFakeDrag()
*/
public boolean isFakeDragging() {
return mFakeDragger.isFakeDragging();
}
/**
* Snaps the ViewPager2 to the closest page
*/
void snapToPage() {
// Method copied from PagerSnapHelper#snapToTargetExistingView
// When fixing something here, make sure to update that method as well
View view = mPagerSnapHelper.findSnapView(mLayoutManager);
if (view == null) {
return;
}
int[] snapDistance = mPagerSnapHelper.calculateDistanceToFinalSnap(mLayoutManager, view);
//noinspection ConstantConditions
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}
/**
* Enable or disable user initiated scrolling. This includes touch input (scroll and fling
* gestures) and accessibility input. Disabling keyboard input is not yet supported. When user
* initiated scrolling is disabled, programmatic scrolls through {@link #setCurrentItem(int,
* boolean) setCurrentItem} still work. By default, user initiated scrolling is enabled.
*
* @param enabled {@code true} to allow user initiated scrolling, {@code false} to block user
* initiated scrolling
* @see #isUserInputEnabled()
*/
public void setUserInputEnabled(boolean enabled) {
mUserInputEnabled = enabled;
mAccessibilityProvider.onSetUserInputEnabled();
}
/**
* Returns if user initiated scrolling between pages is enabled. Enabled by default.
*
* @return {@code true} if users can scroll the ViewPager2, {@code false} otherwise
* @see #setUserInputEnabled(boolean)
*/
public boolean isUserInputEnabled() {
return mUserInputEnabled;
}
/**
* <p>Set the number of pages that should be retained to either side of the currently visible
* page(s). Pages beyond this limit will be recreated from the adapter when needed. Set this to
* {@link #OFFSCREEN_PAGE_LIMIT_DEFAULT} to use RecyclerView's caching strategy. The given value
* must either be larger than 0, or {@code #OFFSCREEN_PAGE_LIMIT_DEFAULT}.</p>
*
* <p>Pages within {@code limit} pages away from the current page are created and added to the
* view hierarchy, even though they are not visible on the screen. Pages outside this limit will
* be removed from the view hierarchy, but the {@code ViewHolder}s will be recycled as usual by
* {@link RecyclerView}.</p>
*
* <p>This is offered as an optimization. If you know in advance the number of pages you will
* need to support or have lazy-loading mechanisms in place on your pages, tweaking this setting
* can have benefits in perceived smoothness of paging animations and interaction. If you have a
* small number of pages (3-4) that you can keep active all at once, less time will be spent in
* layout for newly created view subtrees as the user pages back and forth.</p>
*
* <p>You should keep this limit low, especially if your pages have complex layouts. By default
* it is set to {@code OFFSCREEN_PAGE_LIMIT_DEFAULT}.</p>
*
* @param limit How many pages will be kept offscreen on either side. Valid values are all
* values {@code >= 1} and {@link #OFFSCREEN_PAGE_LIMIT_DEFAULT}
* @throws IllegalArgumentException If the given limit is invalid
* @see #getOffscreenPageLimit()
*/
public void setOffscreenPageLimit(@OffscreenPageLimit int limit) {
if (limit < 1 && limit != OFFSCREEN_PAGE_LIMIT_DEFAULT) {
throw new IllegalArgumentException(
"Offscreen page limit must be OFFSCREEN_PAGE_LIMIT_DEFAULT or a number > 0");
}
mOffscreenPageLimit = limit;
// Trigger layout so prefetch happens through getExtraLayoutSize()
mRecyclerView.requestLayout();
}
/**
* Returns the number of pages that will be retained to either side of the current page in the
* view hierarchy in an idle state. Defaults to {@link #OFFSCREEN_PAGE_LIMIT_DEFAULT}.
*
* @return How many pages will be kept offscreen on either side
* @see #setOffscreenPageLimit(int)
*/
@OffscreenPageLimit
public int getOffscreenPageLimit() {
return mOffscreenPageLimit;
}
/**
* Add a callback that will be invoked whenever the page changes or is incrementally
* scrolled. See {@link OnPageChangeCallback}.
*
* <p>Components that add a callback should take care to remove it when finished.
*
* @param callback callback to add
*/
public void registerOnPageChangeCallback(@NonNull OnPageChangeCallback callback) {
mExternalPageChangeCallbacks.addOnPageChangeCallback(callback);
}
/**
* Remove a callback that was previously added via
* {@link #registerOnPageChangeCallback(OnPageChangeCallback)}.
*
* @param callback callback to remove
*/
public void unregisterOnPageChangeCallback(@NonNull OnPageChangeCallback callback) {
mExternalPageChangeCallbacks.removeOnPageChangeCallback(callback);
}
/**
* Sets a {@link PageTransformer} that will be called for each attached page whenever the
* scroll position is changed. This allows the application to apply custom property
* transformations to each page, overriding the default sliding behavior.
* <p>
* Note: setting a {@link PageTransformer} disables data-set change animations to prevent
* conflicts between the two animation systems. Setting a {@code null} transformer will restore
* data-set change animations.
*
* @param transformer PageTransformer that will modify each page's animation properties
*
* @see MarginPageTransformer
* @see CompositePageTransformer
*/
public void setPageTransformer(@Nullable PageTransformer transformer) {
if (transformer != null) {
if (!mSavedItemAnimatorPresent) {
mSavedItemAnimator = mRecyclerView.getItemAnimator();
mSavedItemAnimatorPresent = true;
}
mRecyclerView.setItemAnimator(null);
} else {
if (mSavedItemAnimatorPresent) {
mRecyclerView.setItemAnimator(mSavedItemAnimator);
mSavedItemAnimator = null;
mSavedItemAnimatorPresent = false;
}
}
// TODO: add support for reverseDrawingOrder: b/112892792
// TODO: add support for pageLayerType: b/112893074
if (transformer == mPageTransformerAdapter.getPageTransformer()) {
return;
}
mPageTransformerAdapter.setPageTransformer(transformer);
requestTransform();
}
/**
* Trigger a call to the registered {@link PageTransformer PageTransformer}'s {@link
* PageTransformer#transformPage(View, float) transformPage} method. Call this when something
* has changed which has invalidated the transformations defined by the {@code PageTransformer}
* that did not trigger a page scroll.
*/
public void requestTransform() {
if (mPageTransformerAdapter.getPageTransformer() == null) {
return;
}
double relativePosition = mScrollEventAdapter.getRelativeScrollPosition();
int position = (int) relativePosition;
float positionOffset = (float) (relativePosition - position);
int positionOffsetPx = Math.round(getPageSize() * positionOffset);
mPageTransformerAdapter.onPageScrolled(position, positionOffset, positionOffsetPx);
}
@Override
@RequiresApi(17)
public void setLayoutDirection(int layoutDirection) {
super.setLayoutDirection(layoutDirection);
mAccessibilityProvider.onSetLayoutDirection();
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
mAccessibilityProvider.onInitializeAccessibilityNodeInfo(info);
}
@RequiresApi(16)
@Override
public boolean performAccessibilityAction(int action, Bundle arguments) {
if (mAccessibilityProvider.handlesPerformAccessibilityAction(action, arguments)) {
return mAccessibilityProvider.onPerformAccessibilityAction(action, arguments);
}
return super.performAccessibilityAction(action, arguments);
}
/**
* Slightly modified RecyclerView to get ViewPager behavior in accessibility and to
* enable/disable user scrolling.
*/
private class RecyclerViewImpl extends RecyclerView {
RecyclerViewImpl(@NonNull Context context) {
super(context);
}
@RequiresApi(23)
@Override
public CharSequence getAccessibilityClassName() {
if (mAccessibilityProvider.handlesRvGetAccessibilityClassName()) {
return mAccessibilityProvider.onRvGetAccessibilityClassName();
}
return super.getAccessibilityClassName();
}
@Override
public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
event.setFromIndex(mCurrentItem);
event.setToIndex(mCurrentItem);
mAccessibilityProvider.onRvInitializeAccessibilityEvent(event);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
return isUserInputEnabled() && super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return isUserInputEnabled() && super.onInterceptTouchEvent(ev);
}
}
private class LinearLayoutManagerImpl extends LinearLayoutManager {
LinearLayoutManagerImpl(Context context) {
super(context);
}
@Override
public boolean performAccessibilityAction(@NonNull RecyclerView.Recycler recycler,
@NonNull RecyclerView.State state, int action, @Nullable Bundle args) {
if (mAccessibilityProvider.handlesLmPerformAccessibilityAction(action)) {
return mAccessibilityProvider.onLmPerformAccessibilityAction(action);
}
return super.performAccessibilityAction(recycler, state, action, args);
}
@Override
public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler,
@NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(recycler, state, info);
mAccessibilityProvider.onLmInitializeAccessibilityNodeInfo(info);
}
@Override
protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,
@NonNull int[] extraLayoutSpace) {
int pageLimit = getOffscreenPageLimit();
if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) {
// Only do custom prefetching of offscreen pages if requested
super.calculateExtraLayoutSpace(state, extraLayoutSpace);
return;
}
final int offscreenSpace = getPageSize() * pageLimit;
extraLayoutSpace[0] = offscreenSpace;
extraLayoutSpace[1] = offscreenSpace;
}
}
private class PagerSnapHelperImpl extends PagerSnapHelper {
PagerSnapHelperImpl() {
}
@Nullable
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
// When interrupting a smooth scroll with a fake drag, we stop RecyclerView's scroll
// animation, which fires a scroll state change to IDLE. PagerSnapHelper then kicks in
// to snap to a page, which we need to prevent here.
// Simplifying that case: during a fake drag, no snapping should occur.
return isFakeDragging() ? null : super.findSnapView(layoutManager);
}
}
private static class SmoothScrollToPosition implements Runnable {
private final int mPosition;
private final RecyclerView mRecyclerView;
SmoothScrollToPosition(int position, RecyclerView recyclerView) {
mPosition = position;
mRecyclerView = recyclerView; // to avoid a synthetic accessor
}
@Override
public void run() {
mRecyclerView.smoothScrollToPosition(mPosition);
}
}
/**
* Callback interface for responding to changing state of the selected page.
*/
public abstract static class OnPageChangeCallback {
/**
* This method will be invoked when the current page is scrolled, either as part
* of a programmatically initiated smooth scroll or a user initiated touch scroll.
*
* @param position Position index of the first page currently being displayed.
* Page position+1 will be visible if positionOffset is nonzero.
* @param positionOffset Value from [0, 1) indicating the offset from the page at position.
* @param positionOffsetPixels Value in pixels indicating the offset from position.
*/
public void onPageScrolled(int position, float positionOffset,
@Px int positionOffsetPixels) {
}
/**
* This method will be invoked when a new page becomes selected. Animation is not
* necessarily complete.
*
* @param position Position index of the new selected page.
*/
public void onPageSelected(int position) {
}
/**
* Called when the scroll state changes. Useful for discovering when the user begins
* dragging, when a fake drag is started, when the pager is automatically settling to the
* current page, or when it is fully stopped/idle. {@code state} can be one of {@link
* #SCROLL_STATE_IDLE}, {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}.
*/
public void onPageScrollStateChanged(@ScrollState int state) {
}
}
/**
* A PageTransformer is invoked whenever a visible/attached page is scrolled.
* This offers an opportunity for the application to apply a custom transformation
* to the page views using animation properties.
*/
public interface PageTransformer {
/**
* Apply a property transformation to the given page.
*
* @param page Apply the transformation to this page
* @param position Position of page relative to the current front-and-center
* position of the pager. 0 is front and center. 1 is one full
* page position to the right, and -2 is two pages to the left.
* Minimum / maximum observed values depend on how many pages we keep
* attached, which depends on offscreenPageLimit.
*
* @see #setOffscreenPageLimit(int)
*/
void transformPage(@NonNull View page, float position);
}
/**
* Add an {@link ItemDecoration} to this ViewPager2. Item decorations can
* affect both measurement and drawing of individual item views.
*
* <p>Item decorations are ordered. Decorations placed earlier in the list will
* be run/queried/drawn first for their effects on item views. Padding added to views
* will be nested; a padding added by an earlier decoration will mean further
* item decorations in the list will be asked to draw/pad within the previous decoration's
* given area.</p>
*
* @param decor Decoration to add
*/
public void addItemDecoration(@NonNull ItemDecoration decor) {
mRecyclerView.addItemDecoration(decor);
}
/**
* Add an {@link ItemDecoration} to this ViewPager2. Item decorations can
* affect both measurement and drawing of individual item views.
*
* <p>Item decorations are ordered. Decorations placed earlier in the list will
* be run/queried/drawn first for their effects on item views. Padding added to views
* will be nested; a padding added by an earlier decoration will mean further
* item decorations in the list will be asked to draw/pad within the previous decoration's
* given area.</p>
*
* @param decor Decoration to add
* @param index Position in the decoration chain to insert this decoration at. If this value
* is negative the decoration will be added at the end.
* @throws IndexOutOfBoundsException on indexes larger than {@link #getItemDecorationCount}
*/
public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
mRecyclerView.addItemDecoration(decor, index);
}
/**
* Returns an {@link ItemDecoration} previously added to this ViewPager2.
*
* @param index The index position of the desired ItemDecoration.
* @return the ItemDecoration at index position
* @throws IndexOutOfBoundsException on invalid index
*/
@NonNull
public ItemDecoration getItemDecorationAt(int index) {
return mRecyclerView.getItemDecorationAt(index);
}
/**
* Returns the number of {@link ItemDecoration} currently added to this ViewPager2.
*
* @return number of ItemDecorations currently added added to this ViewPager2.
*/
public int getItemDecorationCount() {
return mRecyclerView.getItemDecorationCount();
}
/**
* Invalidates all ItemDecorations. If ViewPager2 has item decorations, calling this method
* will trigger a {@link #requestLayout()} call.
*/
public void invalidateItemDecorations() {
mRecyclerView.invalidateItemDecorations();
}
/**
* Removes the {@link ItemDecoration} associated with the supplied index position.
*
* @param index The index position of the ItemDecoration to be removed.
* @throws IndexOutOfBoundsException on invalid index
*/
public void removeItemDecorationAt(int index) {
mRecyclerView.removeItemDecorationAt(index);
}
/**
* Remove an {@link ItemDecoration} from this ViewPager2.
*
* <p>The given decoration will no longer impact the measurement and drawing of
* item views.</p>
*
* @param decor Decoration to remove
* @see #addItemDecoration(ItemDecoration)
*/
public void removeItemDecoration(@NonNull ItemDecoration decor) {
mRecyclerView.removeItemDecoration(decor);
}
private abstract class AccessibilityProvider {
void onInitialize(@NonNull CompositeOnPageChangeCallback pageChangeEventDispatcher,
@NonNull RecyclerView recyclerView) {
}
boolean handlesGetAccessibilityClassName() {
return false;
}
String onGetAccessibilityClassName() {
throw new IllegalStateException("Not implemented.");
}
void onRestorePendingState() {
}
void onAttachAdapter(@Nullable Adapter<?> newAdapter) {
}
void onDetachAdapter(@Nullable Adapter<?> oldAdapter) {
}
void onSetOrientation() {
}
void onSetNewCurrentItem() {
}
void onSetUserInputEnabled() {
}
void onSetLayoutDirection() {
}
void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
}
boolean handlesPerformAccessibilityAction(int action, Bundle arguments) {
return false;
}
boolean onPerformAccessibilityAction(int action, Bundle arguments) {
throw new IllegalStateException("Not implemented.");
}
void onRvInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) {
}
boolean handlesLmPerformAccessibilityAction(int action) {
return false;
}
boolean onLmPerformAccessibilityAction(int action) {
throw new IllegalStateException("Not implemented.");
}
void onLmInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfoCompat info) {
}
boolean handlesRvGetAccessibilityClassName() {
return false;
}
CharSequence onRvGetAccessibilityClassName() {
throw new IllegalStateException("Not implemented.");
}
}
class BasicAccessibilityProvider extends AccessibilityProvider {
@Override
public boolean handlesLmPerformAccessibilityAction(int action) {
return (action == AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD
|| action == AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD)
&& !isUserInputEnabled();
}
@Override
public boolean onLmPerformAccessibilityAction(int action) {
if (!handlesLmPerformAccessibilityAction(action)) {
throw new IllegalStateException();
}
return false;
}
@Override
public void onLmInitializeAccessibilityNodeInfo(
@NonNull AccessibilityNodeInfoCompat info) {
if (!isUserInputEnabled()) {
info.removeAction(AccessibilityActionCompat.ACTION_SCROLL_BACKWARD);
info.removeAction(AccessibilityActionCompat.ACTION_SCROLL_FORWARD);
info.setScrollable(false);
}
}
@Override
public boolean handlesRvGetAccessibilityClassName() {
return true;
}
@Override
public CharSequence onRvGetAccessibilityClassName() {
if (!handlesRvGetAccessibilityClassName()) {
throw new IllegalStateException();
}
return "androidx.viewpager.widget.ViewPager";
}
}
class PageAwareAccessibilityProvider extends AccessibilityProvider {
private final AccessibilityViewCommand mActionPageForward =
new AccessibilityViewCommand() {
@Override
public boolean perform(@NonNull View view,
@Nullable CommandArguments arguments) {
ViewPager2 viewPager = (ViewPager2) view;
setCurrentItemFromAccessibilityCommand(viewPager.getCurrentItem() + 1);
return true;
}
};
private final AccessibilityViewCommand mActionPageBackward =
new AccessibilityViewCommand() {
@Override
public boolean perform(@NonNull View view,
@Nullable CommandArguments arguments) {
ViewPager2 viewPager = (ViewPager2) view;
setCurrentItemFromAccessibilityCommand(viewPager.getCurrentItem() - 1);
return true;
}
};
private RecyclerView.AdapterDataObserver mAdapterDataObserver;
@Override
public void onInitialize(@NonNull CompositeOnPageChangeCallback pageChangeEventDispatcher,
@NonNull RecyclerView recyclerView) {
ViewCompat.setImportantForAccessibility(recyclerView,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
mAdapterDataObserver = new DataSetChangeObserver() {
@Override
public void onChanged() {
updatePageAccessibilityActions();
}
};
if (ViewCompat.getImportantForAccessibility(ViewPager2.this)
== ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
ViewCompat.setImportantForAccessibility(ViewPager2.this,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
}
@Override
public boolean handlesGetAccessibilityClassName() {
return true;
}
@Override
public String onGetAccessibilityClassName() {
if (!handlesGetAccessibilityClassName()) {
throw new IllegalStateException();
}
return "androidx.viewpager.widget.ViewPager";
}
@Override
public void onRestorePendingState() {
updatePageAccessibilityActions();
}
@Override
public void onAttachAdapter(@Nullable Adapter<?> newAdapter) {
updatePageAccessibilityActions();
if (newAdapter != null) {
newAdapter.registerAdapterDataObserver(mAdapterDataObserver);
}
}
@Override
public void onDetachAdapter(@Nullable Adapter<?> oldAdapter) {
if (oldAdapter != null) {
oldAdapter.unregisterAdapterDataObserver(mAdapterDataObserver);
}
}
@Override
public void onSetOrientation() {
updatePageAccessibilityActions();
}
@Override
public void onSetNewCurrentItem() {
updatePageAccessibilityActions();
}
@Override
public void onSetUserInputEnabled() {
updatePageAccessibilityActions();
if (Build.VERSION.SDK_INT < 21) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
}
}
@Override
public void onSetLayoutDirection() {
updatePageAccessibilityActions();
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
addCollectionInfo(info);
if (Build.VERSION.SDK_INT >= 16) {
addScrollActions(info);
}
}
@Override
public boolean handlesPerformAccessibilityAction(int action, Bundle arguments) {
return action == AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD
|| action == AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD;
}
@Override
public boolean onPerformAccessibilityAction(int action, Bundle arguments) {
if (!handlesPerformAccessibilityAction(action, arguments)) {
throw new IllegalStateException();
}
int nextItem = (action == AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD)
? getCurrentItem() - 1
: getCurrentItem() + 1;
setCurrentItemFromAccessibilityCommand(nextItem);
return true;
}
@Override
public void onRvInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) {
event.setSource(ViewPager2.this);
event.setClassName(onGetAccessibilityClassName());
}
/**
* Sets the current item without checking if a fake drag is ongoing. Only call this method
* from within an accessibility command or other forms of user input. Call is ignored if
* {@link #isUserInputEnabled() user input is disabled}.
*/
void setCurrentItemFromAccessibilityCommand(int item) {
if (isUserInputEnabled()) {
setCurrentItemInternal(item, true);
}
}
/**
* Update the ViewPager2's available page accessibility actions. These are updated in
* response to page, adapter, and orientation changes. Compatible with API >= 21.
*/
void updatePageAccessibilityActions() {
ViewPager2 viewPager = ViewPager2.this;
@SuppressLint("InlinedApi")
final int actionIdPageLeft = android.R.id.accessibilityActionPageLeft;
@SuppressLint("InlinedApi")
final int actionIdPageRight = android.R.id.accessibilityActionPageRight;
@SuppressLint("InlinedApi")
final int actionIdPageUp = android.R.id.accessibilityActionPageUp;
@SuppressLint("InlinedApi")
final int actionIdPageDown = android.R.id.accessibilityActionPageDown;
ViewCompat.removeAccessibilityAction(viewPager, actionIdPageLeft);
ViewCompat.removeAccessibilityAction(viewPager, actionIdPageRight);
ViewCompat.removeAccessibilityAction(viewPager, actionIdPageUp);
ViewCompat.removeAccessibilityAction(viewPager, actionIdPageDown);
if (getAdapter() == null) {
return;
}
int itemCount = getAdapter().getItemCount();
if (itemCount == 0) {
return;
}
if (!isUserInputEnabled()) {
return;
}
if (getOrientation() == ORIENTATION_HORIZONTAL) {
boolean isLayoutRtl = isRtl();
int actionIdPageForward = isLayoutRtl ? actionIdPageLeft : actionIdPageRight;
int actionIdPageBackward = isLayoutRtl ? actionIdPageRight : actionIdPageLeft;
if (mCurrentItem < itemCount - 1) {
ViewCompat.replaceAccessibilityAction(viewPager,
new AccessibilityActionCompat(actionIdPageForward, null), null,
mActionPageForward);
}
if (mCurrentItem > 0) {
ViewCompat.replaceAccessibilityAction(viewPager,
new AccessibilityActionCompat(actionIdPageBackward, null), null,
mActionPageBackward);
}
} else {
if (mCurrentItem < itemCount - 1) {
ViewCompat.replaceAccessibilityAction(viewPager,
new AccessibilityActionCompat(actionIdPageDown, null), null,
mActionPageForward);
}
if (mCurrentItem > 0) {
ViewCompat.replaceAccessibilityAction(viewPager,
new AccessibilityActionCompat(actionIdPageUp, null), null,
mActionPageBackward);
}
}
}
private void addCollectionInfo(AccessibilityNodeInfo info) {
int rowCount = 0;
int colCount = 0;
if (getAdapter() != null) {
if (getOrientation() == ORIENTATION_VERTICAL) {
rowCount = getAdapter().getItemCount();
} else {
colCount = getAdapter().getItemCount();
}
}
AccessibilityNodeInfoCompat nodeInfoCompat = AccessibilityNodeInfoCompat.wrap(info);
AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo =
AccessibilityNodeInfoCompat.CollectionInfoCompat.obtain(rowCount, colCount,
/* hierarchical= */false,
AccessibilityNodeInfoCompat.CollectionInfoCompat.SELECTION_MODE_NONE);
nodeInfoCompat.setCollectionInfo(collectionInfo);
}
private void addScrollActions(AccessibilityNodeInfo info) {
final Adapter<?> adapter = getAdapter();
if (adapter == null) {
return;
}
int itemCount = adapter.getItemCount();
if (itemCount == 0 || !isUserInputEnabled()) {
return;
}
if (mCurrentItem > 0) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
}
if (mCurrentItem < itemCount - 1) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
}
info.setScrollable(true);
}
}
/**
* Simplified {@link RecyclerView.AdapterDataObserver} for clients interested in any data-set
* changes regardless of their nature.
*/
private abstract static class DataSetChangeObserver extends RecyclerView.AdapterDataObserver {
@Override
public abstract void onChanged();
@Override
public final void onItemRangeChanged(int positionStart, int itemCount) {
onChanged();
}
@Override
public final void onItemRangeChanged(int positionStart, int itemCount,
@Nullable Object payload) {
onChanged();
}
@Override
public final void onItemRangeInserted(int positionStart, int itemCount) {
onChanged();
}
@Override
public final void onItemRangeRemoved(int positionStart, int itemCount) {
onChanged();
}
@Override
public final void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
onChanged();
}
}
}