2883 lines
108 KiB
Java
2883 lines
108 KiB
Java
/*
|
|
* Copyright (C) 2015 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 com.android.internal.widget;
|
|
|
|
import android.annotation.DrawableRes;
|
|
import android.annotation.NonNull;
|
|
import android.compat.annotation.UnsupportedAppUsage;
|
|
import android.content.Context;
|
|
import android.content.res.Resources;
|
|
import android.content.res.TypedArray;
|
|
import android.database.DataSetObserver;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.Rect;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.os.Bundle;
|
|
import android.os.Parcel;
|
|
import android.os.Parcelable;
|
|
import android.util.AttributeSet;
|
|
import android.util.Log;
|
|
import android.util.MathUtils;
|
|
import android.view.AbsSavedState;
|
|
import android.view.FocusFinder;
|
|
import android.view.Gravity;
|
|
import android.view.KeyEvent;
|
|
import android.view.MotionEvent;
|
|
import android.view.SoundEffectConstants;
|
|
import android.view.VelocityTracker;
|
|
import android.view.View;
|
|
import android.view.ViewConfiguration;
|
|
import android.view.ViewGroup;
|
|
import android.view.ViewParent;
|
|
import android.view.accessibility.AccessibilityEvent;
|
|
import android.view.accessibility.AccessibilityNodeInfo;
|
|
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
|
|
import android.view.animation.Interpolator;
|
|
import android.view.inspector.InspectableProperty;
|
|
import android.widget.EdgeEffect;
|
|
import android.widget.Scroller;
|
|
|
|
import com.android.internal.R;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.Comparator;
|
|
|
|
/**
|
|
* Framework copy of the support-v4 ViewPager class.
|
|
*/
|
|
public class ViewPager extends ViewGroup {
|
|
private static final String TAG = "ViewPager";
|
|
private static final boolean DEBUG = false;
|
|
|
|
private static final int MAX_SCROLL_X = 2 << 23;
|
|
private static final boolean USE_CACHE = false;
|
|
|
|
private static final int DEFAULT_OFFSCREEN_PAGES = 1;
|
|
private static final int MAX_SETTLE_DURATION = 600; // ms
|
|
private static final int MIN_DISTANCE_FOR_FLING = 25; // dips
|
|
|
|
private static final int DEFAULT_GUTTER_SIZE = 16; // dips
|
|
|
|
private static final int MIN_FLING_VELOCITY = 400; // dips
|
|
|
|
private static final int[] LAYOUT_ATTRS = new int[] {
|
|
com.android.internal.R.attr.layout_gravity
|
|
};
|
|
|
|
/**
|
|
* Used to track what the expected number of items in the adapter should be.
|
|
* If the app changes this when we don't expect it, we'll throw a big obnoxious exception.
|
|
*/
|
|
private int mExpectedAdapterCount;
|
|
|
|
static class ItemInfo {
|
|
Object object;
|
|
boolean scrolling;
|
|
float widthFactor;
|
|
|
|
/** Logical position of the item within the pager adapter. */
|
|
int position;
|
|
|
|
/** Offset between the starting edges of the item and its container. */
|
|
float offset;
|
|
}
|
|
|
|
private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>(){
|
|
@Override
|
|
public int compare(ItemInfo lhs, ItemInfo rhs) {
|
|
return lhs.position - rhs.position;
|
|
}
|
|
};
|
|
|
|
private static final Interpolator sInterpolator = new Interpolator() {
|
|
public float getInterpolation(float t) {
|
|
t -= 1.0f;
|
|
return t * t * t * t * t + 1.0f;
|
|
}
|
|
};
|
|
|
|
private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
|
|
private final ItemInfo mTempItem = new ItemInfo();
|
|
|
|
private final Rect mTempRect = new Rect();
|
|
|
|
private PagerAdapter mAdapter;
|
|
private int mCurItem; // Index of currently displayed page.
|
|
private int mRestoredCurItem = -1;
|
|
private Parcelable mRestoredAdapterState = null;
|
|
private ClassLoader mRestoredClassLoader = null;
|
|
private final Scroller mScroller;
|
|
private PagerObserver mObserver;
|
|
|
|
private int mPageMargin;
|
|
private Drawable mMarginDrawable;
|
|
private int mTopPageBounds;
|
|
private int mBottomPageBounds;
|
|
|
|
/**
|
|
* The increment used to move in the "left" direction. Dependent on layout
|
|
* direction.
|
|
*/
|
|
private int mLeftIncr = -1;
|
|
|
|
// Offsets of the first and last items, if known.
|
|
// Set during population, used to determine if we are at the beginning
|
|
// or end of the pager data set during touch scrolling.
|
|
private float mFirstOffset = -Float.MAX_VALUE;
|
|
private float mLastOffset = Float.MAX_VALUE;
|
|
|
|
private int mChildWidthMeasureSpec;
|
|
private int mChildHeightMeasureSpec;
|
|
private boolean mInLayout;
|
|
|
|
private boolean mScrollingCacheEnabled;
|
|
|
|
private boolean mPopulatePending;
|
|
private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;
|
|
|
|
private boolean mIsBeingDragged;
|
|
private boolean mIsUnableToDrag;
|
|
private final int mDefaultGutterSize;
|
|
private int mGutterSize;
|
|
private final int mTouchSlop;
|
|
/**
|
|
* Position of the last motion event.
|
|
*/
|
|
private float mLastMotionX;
|
|
private float mLastMotionY;
|
|
private float mInitialMotionX;
|
|
private float mInitialMotionY;
|
|
/**
|
|
* ID of the active pointer. This is used to retain consistency during
|
|
* drags/flings if multiple pointers are used.
|
|
*/
|
|
private int mActivePointerId = INVALID_POINTER;
|
|
/**
|
|
* Sentinel value for no current active pointer.
|
|
* Used by {@link #mActivePointerId}.
|
|
*/
|
|
private static final int INVALID_POINTER = -1;
|
|
|
|
/**
|
|
* Determines speed during touch scrolling
|
|
*/
|
|
private VelocityTracker mVelocityTracker;
|
|
private final int mMinimumVelocity;
|
|
private final int mMaximumVelocity;
|
|
private final int mFlingDistance;
|
|
private final int mCloseEnough;
|
|
|
|
// If the pager is at least this close to its final position, complete the scroll
|
|
// on touch down and let the user interact with the content inside instead of
|
|
// "catching" the flinging pager.
|
|
private static final int CLOSE_ENOUGH = 2; // dp
|
|
|
|
private final EdgeEffect mLeftEdge;
|
|
private final EdgeEffect mRightEdge;
|
|
|
|
private boolean mFirstLayout = true;
|
|
private boolean mCalledSuper;
|
|
private int mDecorChildCount;
|
|
|
|
private OnPageChangeListener mOnPageChangeListener;
|
|
private OnPageChangeListener mInternalPageChangeListener;
|
|
private OnAdapterChangeListener mAdapterChangeListener;
|
|
private PageTransformer mPageTransformer;
|
|
|
|
private static final int DRAW_ORDER_DEFAULT = 0;
|
|
private static final int DRAW_ORDER_FORWARD = 1;
|
|
private static final int DRAW_ORDER_REVERSE = 2;
|
|
private int mDrawingOrder;
|
|
private ArrayList<View> mDrawingOrderedChildren;
|
|
private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator();
|
|
|
|
/**
|
|
* Indicates that the pager 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 pager is currently being dragged by the user.
|
|
*/
|
|
public static final int SCROLL_STATE_DRAGGING = 1;
|
|
|
|
/**
|
|
* Indicates that the pager is in the process of settling to a final position.
|
|
*/
|
|
public static final int SCROLL_STATE_SETTLING = 2;
|
|
|
|
private final Runnable mEndScrollRunnable = new Runnable() {
|
|
public void run() {
|
|
setScrollState(SCROLL_STATE_IDLE);
|
|
populate();
|
|
}
|
|
};
|
|
|
|
private int mScrollState = SCROLL_STATE_IDLE;
|
|
|
|
/**
|
|
* Callback interface for responding to changing state of the selected page.
|
|
*/
|
|
public interface OnPageChangeListener {
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
@UnsupportedAppUsage
|
|
public void onPageScrolled(int position, float positionOffset, 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.
|
|
*/
|
|
@UnsupportedAppUsage
|
|
public void onPageSelected(int position);
|
|
|
|
/**
|
|
* Called when the scroll state changes. Useful for discovering when the user
|
|
* begins dragging, when the pager is automatically settling to the current page,
|
|
* or when it is fully stopped/idle.
|
|
*
|
|
* @param state The new scroll state.
|
|
* @see com.android.internal.widget.ViewPager#SCROLL_STATE_IDLE
|
|
* @see com.android.internal.widget.ViewPager#SCROLL_STATE_DRAGGING
|
|
* @see com.android.internal.widget.ViewPager#SCROLL_STATE_SETTLING
|
|
*/
|
|
@UnsupportedAppUsage
|
|
public void onPageScrollStateChanged(int state);
|
|
}
|
|
|
|
/**
|
|
* Simple implementation of the {@link OnPageChangeListener} interface with stub
|
|
* implementations of each method. Extend this if you do not intend to override
|
|
* every method of {@link OnPageChangeListener}.
|
|
*/
|
|
public static class SimpleOnPageChangeListener implements OnPageChangeListener {
|
|
@Override
|
|
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
|
// This space for rent
|
|
}
|
|
|
|
@Override
|
|
public void onPageSelected(int position) {
|
|
// This space for rent
|
|
}
|
|
|
|
@Override
|
|
public void onPageScrollStateChanged(int state) {
|
|
// This space for rent
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* <p>As property animation is only supported as of Android 3.0 and forward,
|
|
* setting a PageTransformer on a ViewPager on earlier platform versions will
|
|
* be ignored.</p>
|
|
*/
|
|
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 -1 is one page position to the left.
|
|
*/
|
|
public void transformPage(View page, float position);
|
|
}
|
|
|
|
/**
|
|
* Used internally to monitor when adapters are switched.
|
|
*/
|
|
interface OnAdapterChangeListener {
|
|
public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter);
|
|
}
|
|
|
|
/**
|
|
* Used internally to tag special types of child views that should be added as
|
|
* pager decorations by default.
|
|
*/
|
|
interface Decor {}
|
|
|
|
public ViewPager(Context context) {
|
|
this(context, null);
|
|
}
|
|
|
|
public ViewPager(Context context, AttributeSet attrs) {
|
|
this(context, attrs, 0);
|
|
}
|
|
|
|
public ViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
this(context, attrs, defStyleAttr, 0);
|
|
}
|
|
|
|
public ViewPager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
super(context, attrs, defStyleAttr, defStyleRes);
|
|
|
|
setWillNotDraw(false);
|
|
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
|
|
setFocusable(true);
|
|
|
|
mScroller = new Scroller(context, sInterpolator);
|
|
final ViewConfiguration configuration = ViewConfiguration.get(context);
|
|
final float density = context.getResources().getDisplayMetrics().density;
|
|
|
|
mTouchSlop = configuration.getScaledPagingTouchSlop();
|
|
mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);
|
|
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
|
|
mLeftEdge = new EdgeEffect(context, attrs);
|
|
mRightEdge = new EdgeEffect(context, attrs);
|
|
|
|
mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);
|
|
mCloseEnough = (int) (CLOSE_ENOUGH * density);
|
|
mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density);
|
|
|
|
if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
|
|
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onDetachedFromWindow() {
|
|
removeCallbacks(mEndScrollRunnable);
|
|
super.onDetachedFromWindow();
|
|
}
|
|
|
|
private void setScrollState(int newState) {
|
|
if (mScrollState == newState) {
|
|
return;
|
|
}
|
|
|
|
mScrollState = newState;
|
|
if (mPageTransformer != null) {
|
|
// PageTransformers can do complex things that benefit from hardware layers.
|
|
enableLayers(newState != SCROLL_STATE_IDLE);
|
|
}
|
|
if (mOnPageChangeListener != null) {
|
|
mOnPageChangeListener.onPageScrollStateChanged(newState);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set a PagerAdapter that will supply views for this pager as needed.
|
|
*
|
|
* @param adapter Adapter to use
|
|
*/
|
|
public void setAdapter(PagerAdapter adapter) {
|
|
if (mAdapter != null) {
|
|
mAdapter.unregisterDataSetObserver(mObserver);
|
|
mAdapter.startUpdate(this);
|
|
for (int i = 0; i < mItems.size(); i++) {
|
|
final ItemInfo ii = mItems.get(i);
|
|
mAdapter.destroyItem(this, ii.position, ii.object);
|
|
}
|
|
mAdapter.finishUpdate(this);
|
|
mItems.clear();
|
|
removeNonDecorViews();
|
|
mCurItem = 0;
|
|
scrollTo(0, 0);
|
|
}
|
|
|
|
final PagerAdapter oldAdapter = mAdapter;
|
|
mAdapter = adapter;
|
|
mExpectedAdapterCount = 0;
|
|
|
|
if (mAdapter != null) {
|
|
if (mObserver == null) {
|
|
mObserver = new PagerObserver();
|
|
}
|
|
mAdapter.registerDataSetObserver(mObserver);
|
|
mPopulatePending = false;
|
|
final boolean wasFirstLayout = mFirstLayout;
|
|
mFirstLayout = true;
|
|
mExpectedAdapterCount = mAdapter.getCount();
|
|
if (mRestoredCurItem >= 0) {
|
|
mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
|
|
setCurrentItemInternal(mRestoredCurItem, false, true);
|
|
mRestoredCurItem = -1;
|
|
mRestoredAdapterState = null;
|
|
mRestoredClassLoader = null;
|
|
} else if (!wasFirstLayout) {
|
|
populate();
|
|
} else {
|
|
requestLayout();
|
|
}
|
|
}
|
|
|
|
if (mAdapterChangeListener != null && oldAdapter != adapter) {
|
|
mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
|
|
}
|
|
}
|
|
|
|
private void removeNonDecorViews() {
|
|
for (int i = 0; i < getChildCount(); i++) {
|
|
final View child = getChildAt(i);
|
|
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
|
|
if (!lp.isDecor) {
|
|
removeViewAt(i);
|
|
i--;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve the current adapter supplying pages.
|
|
*
|
|
* @return The currently registered PagerAdapter
|
|
*/
|
|
public PagerAdapter getAdapter() {
|
|
return mAdapter;
|
|
}
|
|
|
|
void setOnAdapterChangeListener(OnAdapterChangeListener listener) {
|
|
mAdapterChangeListener = listener;
|
|
}
|
|
|
|
private int getPaddedWidth() {
|
|
return getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @param item Item index to select
|
|
*/
|
|
public void setCurrentItem(int item) {
|
|
mPopulatePending = false;
|
|
setCurrentItemInternal(item, !mFirstLayout, false);
|
|
}
|
|
|
|
/**
|
|
* Set the currently selected page.
|
|
*
|
|
* @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) {
|
|
mPopulatePending = false;
|
|
setCurrentItemInternal(item, smoothScroll, false);
|
|
}
|
|
|
|
@UnsupportedAppUsage
|
|
public int getCurrentItem() {
|
|
return mCurItem;
|
|
}
|
|
|
|
boolean setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
|
|
return setCurrentItemInternal(item, smoothScroll, always, 0);
|
|
}
|
|
|
|
boolean setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
|
|
if (mAdapter == null || mAdapter.getCount() <= 0) {
|
|
setScrollingCacheEnabled(false);
|
|
return false;
|
|
}
|
|
|
|
item = MathUtils.constrain(item, 0, mAdapter.getCount() - 1);
|
|
if (!always && mCurItem == item && mItems.size() != 0) {
|
|
setScrollingCacheEnabled(false);
|
|
return false;
|
|
}
|
|
|
|
final int pageLimit = mOffscreenPageLimit;
|
|
if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
|
|
// We are doing a jump by more than one page. To avoid
|
|
// glitches, we want to keep all current pages in the view
|
|
// until the scroll ends.
|
|
for (int i = 0; i < mItems.size(); i++) {
|
|
mItems.get(i).scrolling = true;
|
|
}
|
|
}
|
|
|
|
final boolean dispatchSelected = mCurItem != item;
|
|
if (mFirstLayout) {
|
|
// We don't have any idea how big we are yet and shouldn't have any pages either.
|
|
// Just set things up and let the pending layout handle things.
|
|
mCurItem = item;
|
|
if (dispatchSelected && mOnPageChangeListener != null) {
|
|
mOnPageChangeListener.onPageSelected(item);
|
|
}
|
|
if (dispatchSelected && mInternalPageChangeListener != null) {
|
|
mInternalPageChangeListener.onPageSelected(item);
|
|
}
|
|
requestLayout();
|
|
} else {
|
|
populate(item);
|
|
scrollToItem(item, smoothScroll, velocity, dispatchSelected);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void scrollToItem(int position, boolean smoothScroll, int velocity,
|
|
boolean dispatchSelected) {
|
|
final int destX = getLeftEdgeForItem(position);
|
|
|
|
if (smoothScroll) {
|
|
smoothScrollTo(destX, 0, velocity);
|
|
|
|
if (dispatchSelected && mOnPageChangeListener != null) {
|
|
mOnPageChangeListener.onPageSelected(position);
|
|
}
|
|
if (dispatchSelected && mInternalPageChangeListener != null) {
|
|
mInternalPageChangeListener.onPageSelected(position);
|
|
}
|
|
} else {
|
|
if (dispatchSelected && mOnPageChangeListener != null) {
|
|
mOnPageChangeListener.onPageSelected(position);
|
|
}
|
|
if (dispatchSelected && mInternalPageChangeListener != null) {
|
|
mInternalPageChangeListener.onPageSelected(position);
|
|
}
|
|
|
|
completeScroll(false);
|
|
scrollTo(destX, 0);
|
|
pageScrolled(destX);
|
|
}
|
|
}
|
|
|
|
private int getLeftEdgeForItem(int position) {
|
|
final ItemInfo info = infoForPosition(position);
|
|
if (info == null) {
|
|
return 0;
|
|
}
|
|
|
|
final int width = getPaddedWidth();
|
|
final int scaledOffset = (int) (width * MathUtils.constrain(
|
|
info.offset, mFirstOffset, mLastOffset));
|
|
|
|
if (isLayoutRtl()) {
|
|
final int itemWidth = (int) (width * info.widthFactor + 0.5f);
|
|
return MAX_SCROLL_X - itemWidth - scaledOffset;
|
|
} else {
|
|
return scaledOffset;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set a listener that will be invoked whenever the page changes or is incrementally
|
|
* scrolled. See {@link OnPageChangeListener}.
|
|
*
|
|
* @param listener Listener to set
|
|
*/
|
|
public void setOnPageChangeListener(OnPageChangeListener listener) {
|
|
mOnPageChangeListener = listener;
|
|
}
|
|
|
|
/**
|
|
* Set 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 look and feel.
|
|
*
|
|
* <p><em>Note:</em> Prior to Android 3.0 the property animation APIs did not exist.
|
|
* As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.</p>
|
|
*
|
|
* @param reverseDrawingOrder true if the supplied PageTransformer requires page views
|
|
* to be drawn from last to first instead of first to last.
|
|
* @param transformer PageTransformer that will modify each page's animation properties
|
|
*/
|
|
public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) {
|
|
final boolean hasTransformer = transformer != null;
|
|
final boolean needsPopulate = hasTransformer != (mPageTransformer != null);
|
|
mPageTransformer = transformer;
|
|
setChildrenDrawingOrderEnabled(hasTransformer);
|
|
if (hasTransformer) {
|
|
mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD;
|
|
} else {
|
|
mDrawingOrder = DRAW_ORDER_DEFAULT;
|
|
}
|
|
if (needsPopulate) populate();
|
|
}
|
|
|
|
@Override
|
|
protected int getChildDrawingOrder(int childCount, int i) {
|
|
final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i;
|
|
final int result = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex;
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Set a separate OnPageChangeListener for internal use by the support library.
|
|
*
|
|
* @param listener Listener to set
|
|
* @return The old listener that was set, if any.
|
|
*/
|
|
OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) {
|
|
OnPageChangeListener oldListener = mInternalPageChangeListener;
|
|
mInternalPageChangeListener = listener;
|
|
return oldListener;
|
|
}
|
|
|
|
/**
|
|
* 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 1.
|
|
*
|
|
* @return How many pages will be kept offscreen on either side
|
|
* @see #setOffscreenPageLimit(int)
|
|
*/
|
|
public int getOffscreenPageLimit() {
|
|
return mOffscreenPageLimit;
|
|
}
|
|
|
|
/**
|
|
* Set the number of pages that should be retained to either side of the
|
|
* current page in the view hierarchy in an idle state. Pages beyond this
|
|
* limit will be recreated from the adapter when needed.
|
|
*
|
|
* <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.
|
|
* This setting defaults to 1.</p>
|
|
*
|
|
* @param limit How many pages will be kept offscreen in an idle state.
|
|
*/
|
|
public void setOffscreenPageLimit(int limit) {
|
|
if (limit < DEFAULT_OFFSCREEN_PAGES) {
|
|
Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " +
|
|
DEFAULT_OFFSCREEN_PAGES);
|
|
limit = DEFAULT_OFFSCREEN_PAGES;
|
|
}
|
|
if (limit != mOffscreenPageLimit) {
|
|
mOffscreenPageLimit = limit;
|
|
populate();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the margin between pages.
|
|
*
|
|
* @param marginPixels Distance between adjacent pages in pixels
|
|
* @see #getPageMargin()
|
|
* @see #setPageMarginDrawable(android.graphics.drawable.Drawable)
|
|
* @see #setPageMarginDrawable(int)
|
|
*/
|
|
public void setPageMargin(int marginPixels) {
|
|
final int oldMargin = mPageMargin;
|
|
mPageMargin = marginPixels;
|
|
|
|
final int width = getWidth();
|
|
recomputeScrollPosition(width, width, marginPixels, oldMargin);
|
|
|
|
requestLayout();
|
|
}
|
|
|
|
/**
|
|
* Return the margin between pages.
|
|
*
|
|
* @return The size of the margin in pixels
|
|
*/
|
|
public int getPageMargin() {
|
|
return mPageMargin;
|
|
}
|
|
|
|
/**
|
|
* Set a drawable that will be used to fill the margin between pages.
|
|
*
|
|
* @param d Drawable to display between pages
|
|
*/
|
|
public void setPageMarginDrawable(Drawable d) {
|
|
mMarginDrawable = d;
|
|
if (d != null) refreshDrawableState();
|
|
setWillNotDraw(d == null);
|
|
invalidate();
|
|
}
|
|
|
|
/**
|
|
* Set a drawable that will be used to fill the margin between pages.
|
|
*
|
|
* @param resId Resource ID of a drawable to display between pages
|
|
*/
|
|
public void setPageMarginDrawable(@DrawableRes int resId) {
|
|
setPageMarginDrawable(getContext().getDrawable(resId));
|
|
}
|
|
|
|
@Override
|
|
protected boolean verifyDrawable(@NonNull Drawable who) {
|
|
return super.verifyDrawable(who) || who == mMarginDrawable;
|
|
}
|
|
|
|
@Override
|
|
protected void drawableStateChanged() {
|
|
super.drawableStateChanged();
|
|
final Drawable marginDrawable = mMarginDrawable;
|
|
if (marginDrawable != null && marginDrawable.isStateful()
|
|
&& marginDrawable.setState(getDrawableState())) {
|
|
invalidateDrawable(marginDrawable);
|
|
}
|
|
}
|
|
|
|
// We want the duration of the page snap animation to be influenced by the distance that
|
|
// the screen has to travel, however, we don't want this duration to be effected in a
|
|
// purely linear fashion. Instead, we use this method to moderate the effect that the distance
|
|
// of travel has on the overall snap duration.
|
|
float distanceInfluenceForSnapDuration(float f) {
|
|
f -= 0.5f; // center the values about 0.
|
|
f *= 0.3f * Math.PI / 2.0f;
|
|
return (float) Math.sin(f);
|
|
}
|
|
|
|
/**
|
|
* Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately.
|
|
*
|
|
* @param x the number of pixels to scroll by on the X axis
|
|
* @param y the number of pixels to scroll by on the Y axis
|
|
*/
|
|
void smoothScrollTo(int x, int y) {
|
|
smoothScrollTo(x, y, 0);
|
|
}
|
|
|
|
/**
|
|
* Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately.
|
|
*
|
|
* @param x the number of pixels to scroll by on the X axis
|
|
* @param y the number of pixels to scroll by on the Y axis
|
|
* @param velocity the velocity associated with a fling, if applicable. (0 otherwise)
|
|
*/
|
|
void smoothScrollTo(int x, int y, int velocity) {
|
|
if (getChildCount() == 0) {
|
|
// Nothing to do.
|
|
setScrollingCacheEnabled(false);
|
|
return;
|
|
}
|
|
int sx = getScrollX();
|
|
int sy = getScrollY();
|
|
int dx = x - sx;
|
|
int dy = y - sy;
|
|
if (dx == 0 && dy == 0) {
|
|
completeScroll(false);
|
|
populate();
|
|
setScrollState(SCROLL_STATE_IDLE);
|
|
return;
|
|
}
|
|
|
|
setScrollingCacheEnabled(true);
|
|
setScrollState(SCROLL_STATE_SETTLING);
|
|
|
|
final int width = getPaddedWidth();
|
|
final int halfWidth = width / 2;
|
|
final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
|
|
final float distance = halfWidth + halfWidth *
|
|
distanceInfluenceForSnapDuration(distanceRatio);
|
|
|
|
int duration = 0;
|
|
velocity = Math.abs(velocity);
|
|
if (velocity > 0) {
|
|
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
|
|
} else {
|
|
final float pageWidth = width * mAdapter.getPageWidth(mCurItem);
|
|
final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin);
|
|
duration = (int) ((pageDelta + 1) * 100);
|
|
}
|
|
duration = Math.min(duration, MAX_SETTLE_DURATION);
|
|
|
|
mScroller.startScroll(sx, sy, dx, dy, duration);
|
|
postInvalidateOnAnimation();
|
|
}
|
|
|
|
ItemInfo addNewItem(int position, int index) {
|
|
ItemInfo ii = new ItemInfo();
|
|
ii.position = position;
|
|
ii.object = mAdapter.instantiateItem(this, position);
|
|
ii.widthFactor = mAdapter.getPageWidth(position);
|
|
if (index < 0 || index >= mItems.size()) {
|
|
mItems.add(ii);
|
|
} else {
|
|
mItems.add(index, ii);
|
|
}
|
|
return ii;
|
|
}
|
|
|
|
void dataSetChanged() {
|
|
// This method only gets called if our observer is attached, so mAdapter is non-null.
|
|
|
|
final int adapterCount = mAdapter.getCount();
|
|
mExpectedAdapterCount = adapterCount;
|
|
boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 &&
|
|
mItems.size() < adapterCount;
|
|
int newCurrItem = mCurItem;
|
|
|
|
boolean isUpdating = false;
|
|
for (int i = 0; i < mItems.size(); i++) {
|
|
final ItemInfo ii = mItems.get(i);
|
|
final int newPos = mAdapter.getItemPosition(ii.object);
|
|
|
|
if (newPos == PagerAdapter.POSITION_UNCHANGED) {
|
|
continue;
|
|
}
|
|
|
|
if (newPos == PagerAdapter.POSITION_NONE) {
|
|
mItems.remove(i);
|
|
i--;
|
|
|
|
if (!isUpdating) {
|
|
mAdapter.startUpdate(this);
|
|
isUpdating = true;
|
|
}
|
|
|
|
mAdapter.destroyItem(this, ii.position, ii.object);
|
|
needPopulate = true;
|
|
|
|
if (mCurItem == ii.position) {
|
|
// Keep the current item in the valid range
|
|
newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
|
|
needPopulate = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (ii.position != newPos) {
|
|
if (ii.position == mCurItem) {
|
|
// Our current item changed position. Follow it.
|
|
newCurrItem = newPos;
|
|
}
|
|
|
|
ii.position = newPos;
|
|
needPopulate = true;
|
|
}
|
|
}
|
|
|
|
if (isUpdating) {
|
|
mAdapter.finishUpdate(this);
|
|
}
|
|
|
|
Collections.sort(mItems, COMPARATOR);
|
|
|
|
if (needPopulate) {
|
|
// Reset our known page widths; populate will recompute them.
|
|
final int childCount = getChildCount();
|
|
for (int i = 0; i < childCount; i++) {
|
|
final View child = getChildAt(i);
|
|
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
|
|
if (!lp.isDecor) {
|
|
lp.widthFactor = 0.f;
|
|
}
|
|
}
|
|
|
|
setCurrentItemInternal(newCurrItem, false, true);
|
|
requestLayout();
|
|
}
|
|
}
|
|
|
|
public void populate() {
|
|
populate(mCurItem);
|
|
}
|
|
|
|
void populate(int newCurrentItem) {
|
|
ItemInfo oldCurInfo = null;
|
|
int focusDirection = View.FOCUS_FORWARD;
|
|
if (mCurItem != newCurrentItem) {
|
|
focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
|
|
oldCurInfo = infoForPosition(mCurItem);
|
|
mCurItem = newCurrentItem;
|
|
}
|
|
|
|
if (mAdapter == null) {
|
|
sortChildDrawingOrder();
|
|
return;
|
|
}
|
|
|
|
// Bail now if we are waiting to populate. This is to hold off
|
|
// on creating views from the time the user releases their finger to
|
|
// fling to a new position until we have finished the scroll to
|
|
// that position, avoiding glitches from happening at that point.
|
|
if (mPopulatePending) {
|
|
if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
|
|
sortChildDrawingOrder();
|
|
return;
|
|
}
|
|
|
|
// Also, don't populate until we are attached to a window. This is to
|
|
// avoid trying to populate before we have restored our view hierarchy
|
|
// state and conflicting with what is restored.
|
|
if (getWindowToken() == null) {
|
|
return;
|
|
}
|
|
|
|
mAdapter.startUpdate(this);
|
|
|
|
final int pageLimit = mOffscreenPageLimit;
|
|
final int startPos = Math.max(0, mCurItem - pageLimit);
|
|
final int N = mAdapter.getCount();
|
|
final int endPos = Math.min(N-1, mCurItem + pageLimit);
|
|
|
|
if (N != mExpectedAdapterCount) {
|
|
String resName;
|
|
try {
|
|
resName = getResources().getResourceName(getId());
|
|
} catch (Resources.NotFoundException e) {
|
|
resName = Integer.toHexString(getId());
|
|
}
|
|
throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +
|
|
" contents without calling PagerAdapter#notifyDataSetChanged!" +
|
|
" Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +
|
|
" Pager id: " + resName +
|
|
" Pager class: " + getClass() +
|
|
" Problematic adapter: " + mAdapter.getClass());
|
|
}
|
|
|
|
// Locate the currently focused item or add it if needed.
|
|
int curIndex = -1;
|
|
ItemInfo curItem = null;
|
|
for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
|
|
final ItemInfo ii = mItems.get(curIndex);
|
|
if (ii.position >= mCurItem) {
|
|
if (ii.position == mCurItem) curItem = ii;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (curItem == null && N > 0) {
|
|
curItem = addNewItem(mCurItem, curIndex);
|
|
}
|
|
|
|
// Fill 3x the available width or up to the number of offscreen
|
|
// pages requested to either side, whichever is larger.
|
|
// If we have no current item we have no work to do.
|
|
if (curItem != null) {
|
|
float extraWidthLeft = 0.f;
|
|
int itemIndex = curIndex - 1;
|
|
ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
|
|
final int clientWidth = getPaddedWidth();
|
|
final float leftWidthNeeded = clientWidth <= 0 ? 0 :
|
|
2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
|
|
for (int pos = mCurItem - 1; pos >= 0; pos--) {
|
|
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
|
|
if (ii == null) {
|
|
break;
|
|
}
|
|
if (pos == ii.position && !ii.scrolling) {
|
|
mItems.remove(itemIndex);
|
|
mAdapter.destroyItem(this, pos, ii.object);
|
|
if (DEBUG) {
|
|
Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
|
|
" view: " + ii.object);
|
|
}
|
|
itemIndex--;
|
|
curIndex--;
|
|
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
|
|
}
|
|
} else if (ii != null && pos == ii.position) {
|
|
extraWidthLeft += ii.widthFactor;
|
|
itemIndex--;
|
|
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
|
|
} else {
|
|
ii = addNewItem(pos, itemIndex + 1);
|
|
extraWidthLeft += ii.widthFactor;
|
|
curIndex++;
|
|
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
|
|
}
|
|
}
|
|
|
|
float extraWidthRight = curItem.widthFactor;
|
|
itemIndex = curIndex + 1;
|
|
if (extraWidthRight < 2.f) {
|
|
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
|
|
final float rightWidthNeeded = clientWidth <= 0 ? 0 :
|
|
(float) getPaddingRight() / (float) clientWidth + 2.f;
|
|
for (int pos = mCurItem + 1; pos < N; pos++) {
|
|
if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
|
|
if (ii == null) {
|
|
break;
|
|
}
|
|
if (pos == ii.position && !ii.scrolling) {
|
|
mItems.remove(itemIndex);
|
|
mAdapter.destroyItem(this, pos, ii.object);
|
|
if (DEBUG) {
|
|
Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
|
|
" view: " + ii.object);
|
|
}
|
|
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
|
|
}
|
|
} else if (ii != null && pos == ii.position) {
|
|
extraWidthRight += ii.widthFactor;
|
|
itemIndex++;
|
|
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
|
|
} else {
|
|
ii = addNewItem(pos, itemIndex);
|
|
itemIndex++;
|
|
extraWidthRight += ii.widthFactor;
|
|
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
|
|
}
|
|
}
|
|
}
|
|
|
|
calculatePageOffsets(curItem, curIndex, oldCurInfo);
|
|
}
|
|
|
|
if (DEBUG) {
|
|
Log.i(TAG, "Current page list:");
|
|
for (int i=0; i<mItems.size(); i++) {
|
|
Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
|
|
}
|
|
}
|
|
|
|
mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
|
|
|
|
mAdapter.finishUpdate(this);
|
|
|
|
// Check width measurement of current pages and drawing sort order.
|
|
// Update LayoutParams as needed.
|
|
final int childCount = getChildCount();
|
|
for (int i = 0; i < childCount; i++) {
|
|
final View child = getChildAt(i);
|
|
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
|
|
lp.childIndex = i;
|
|
if (!lp.isDecor && lp.widthFactor == 0.f) {
|
|
// 0 means requery the adapter for this, it doesn't have a valid width.
|
|
final ItemInfo ii = infoForChild(child);
|
|
if (ii != null) {
|
|
lp.widthFactor = ii.widthFactor;
|
|
lp.position = ii.position;
|
|
}
|
|
}
|
|
}
|
|
sortChildDrawingOrder();
|
|
|
|
if (hasFocus()) {
|
|
View currentFocused = findFocus();
|
|
ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
|
|
if (ii == null || ii.position != mCurItem) {
|
|
for (int i=0; i<getChildCount(); i++) {
|
|
View child = getChildAt(i);
|
|
ii = infoForChild(child);
|
|
if (ii != null && ii.position == mCurItem) {
|
|
final Rect focusRect;
|
|
if (currentFocused == null) {
|
|
focusRect = null;
|
|
} else {
|
|
focusRect = mTempRect;
|
|
currentFocused.getFocusedRect(mTempRect);
|
|
offsetDescendantRectToMyCoords(currentFocused, mTempRect);
|
|
offsetRectIntoDescendantCoords(child, mTempRect);
|
|
}
|
|
if (child.requestFocus(focusDirection, focusRect)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void sortChildDrawingOrder() {
|
|
if (mDrawingOrder != DRAW_ORDER_DEFAULT) {
|
|
if (mDrawingOrderedChildren == null) {
|
|
mDrawingOrderedChildren = new ArrayList<View>();
|
|
} else {
|
|
mDrawingOrderedChildren.clear();
|
|
}
|
|
final int childCount = getChildCount();
|
|
for (int i = 0; i < childCount; i++) {
|
|
final View child = getChildAt(i);
|
|
mDrawingOrderedChildren.add(child);
|
|
}
|
|
Collections.sort(mDrawingOrderedChildren, sPositionComparator);
|
|
}
|
|
}
|
|
|
|
private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
|
|
final int N = mAdapter.getCount();
|
|
final int width = getPaddedWidth();
|
|
final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
|
|
|
|
// Fix up offsets for later layout.
|
|
if (oldCurInfo != null) {
|
|
final int oldCurPosition = oldCurInfo.position;
|
|
|
|
// Base offsets off of oldCurInfo.
|
|
if (oldCurPosition < curItem.position) {
|
|
int itemIndex = 0;
|
|
float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset;
|
|
for (int pos = oldCurPosition + 1; pos <= curItem.position && itemIndex < mItems.size(); pos++) {
|
|
ItemInfo ii = mItems.get(itemIndex);
|
|
while (pos > ii.position && itemIndex < mItems.size() - 1) {
|
|
itemIndex++;
|
|
ii = mItems.get(itemIndex);
|
|
}
|
|
|
|
while (pos < ii.position) {
|
|
// We don't have an item populated for this,
|
|
// ask the adapter for an offset.
|
|
offset += mAdapter.getPageWidth(pos) + marginOffset;
|
|
pos++;
|
|
}
|
|
|
|
ii.offset = offset;
|
|
offset += ii.widthFactor + marginOffset;
|
|
}
|
|
} else if (oldCurPosition > curItem.position) {
|
|
int itemIndex = mItems.size() - 1;
|
|
float offset = oldCurInfo.offset;
|
|
for (int pos = oldCurPosition - 1; pos >= curItem.position && itemIndex >= 0; pos--) {
|
|
ItemInfo ii = mItems.get(itemIndex);
|
|
while (pos < ii.position && itemIndex > 0) {
|
|
itemIndex--;
|
|
ii = mItems.get(itemIndex);
|
|
}
|
|
|
|
while (pos > ii.position) {
|
|
// We don't have an item populated for this,
|
|
// ask the adapter for an offset.
|
|
offset -= mAdapter.getPageWidth(pos) + marginOffset;
|
|
pos--;
|
|
}
|
|
|
|
offset -= ii.widthFactor + marginOffset;
|
|
ii.offset = offset;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Base all offsets off of curItem.
|
|
final int itemCount = mItems.size();
|
|
float offset = curItem.offset;
|
|
int pos = curItem.position - 1;
|
|
mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
|
|
mLastOffset = curItem.position == N - 1 ?
|
|
curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;
|
|
|
|
// Previous pages
|
|
for (int i = curIndex - 1; i >= 0; i--, pos--) {
|
|
final ItemInfo ii = mItems.get(i);
|
|
while (pos > ii.position) {
|
|
offset -= mAdapter.getPageWidth(pos--) + marginOffset;
|
|
}
|
|
offset -= ii.widthFactor + marginOffset;
|
|
ii.offset = offset;
|
|
if (ii.position == 0) mFirstOffset = offset;
|
|
}
|
|
|
|
offset = curItem.offset + curItem.widthFactor + marginOffset;
|
|
pos = curItem.position + 1;
|
|
|
|
// Next pages
|
|
for (int i = curIndex + 1; i < itemCount; i++, pos++) {
|
|
final ItemInfo ii = mItems.get(i);
|
|
while (pos < ii.position) {
|
|
offset += mAdapter.getPageWidth(pos++) + marginOffset;
|
|
}
|
|
if (ii.position == N - 1) {
|
|
mLastOffset = offset + ii.widthFactor - 1;
|
|
}
|
|
ii.offset = offset;
|
|
offset += ii.widthFactor + marginOffset;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is the persistent state that is saved by ViewPager. Only needed
|
|
* if you are creating a sublass of ViewPager that must save its own
|
|
* state, in which case it should implement a subclass of this which
|
|
* contains that state.
|
|
*/
|
|
public static class SavedState extends AbsSavedState {
|
|
int position;
|
|
Parcelable adapterState;
|
|
ClassLoader loader;
|
|
|
|
public SavedState(@NonNull Parcelable superState) {
|
|
super(superState);
|
|
}
|
|
|
|
@Override
|
|
public void writeToParcel(Parcel out, int flags) {
|
|
super.writeToParcel(out, flags);
|
|
out.writeInt(position);
|
|
out.writeParcelable(adapterState, flags);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "FragmentPager.SavedState{"
|
|
+ Integer.toHexString(System.identityHashCode(this))
|
|
+ " position=" + position + "}";
|
|
}
|
|
|
|
public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
|
|
@Override
|
|
public SavedState createFromParcel(Parcel in, ClassLoader loader) {
|
|
return new SavedState(in, loader);
|
|
}
|
|
|
|
@Override
|
|
public SavedState createFromParcel(Parcel in) {
|
|
return new SavedState(in, null);
|
|
}
|
|
@Override
|
|
public SavedState[] newArray(int size) {
|
|
return new SavedState[size];
|
|
}
|
|
};
|
|
|
|
SavedState(Parcel in, ClassLoader loader) {
|
|
super(in, loader);
|
|
if (loader == null) {
|
|
loader = getClass().getClassLoader();
|
|
}
|
|
position = in.readInt();
|
|
adapterState = in.readParcelable(loader);
|
|
this.loader = loader;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Parcelable onSaveInstanceState() {
|
|
Parcelable superState = super.onSaveInstanceState();
|
|
SavedState ss = new SavedState(superState);
|
|
ss.position = mCurItem;
|
|
if (mAdapter != null) {
|
|
ss.adapterState = mAdapter.saveState();
|
|
}
|
|
return ss;
|
|
}
|
|
|
|
@Override
|
|
public void onRestoreInstanceState(Parcelable state) {
|
|
if (!(state instanceof SavedState)) {
|
|
super.onRestoreInstanceState(state);
|
|
return;
|
|
}
|
|
|
|
SavedState ss = (SavedState)state;
|
|
super.onRestoreInstanceState(ss.getSuperState());
|
|
|
|
if (mAdapter != null) {
|
|
mAdapter.restoreState(ss.adapterState, ss.loader);
|
|
setCurrentItemInternal(ss.position, false, true);
|
|
} else {
|
|
mRestoredCurItem = ss.position;
|
|
mRestoredAdapterState = ss.adapterState;
|
|
mRestoredClassLoader = ss.loader;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void addView(View child, int index, ViewGroup.LayoutParams params) {
|
|
if (!checkLayoutParams(params)) {
|
|
params = generateLayoutParams(params);
|
|
}
|
|
final LayoutParams lp = (LayoutParams) params;
|
|
lp.isDecor |= child instanceof Decor;
|
|
if (mInLayout) {
|
|
if (lp != null && lp.isDecor) {
|
|
throw new IllegalStateException("Cannot add pager decor view during layout");
|
|
}
|
|
lp.needsMeasure = true;
|
|
addViewInLayout(child, index, params);
|
|
} else {
|
|
super.addView(child, index, params);
|
|
}
|
|
|
|
if (USE_CACHE) {
|
|
if (child.getVisibility() != GONE) {
|
|
child.setDrawingCacheEnabled(mScrollingCacheEnabled);
|
|
} else {
|
|
child.setDrawingCacheEnabled(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
public Object getCurrent() {
|
|
final ItemInfo itemInfo = infoForPosition(getCurrentItem());
|
|
return itemInfo == null ? null : itemInfo.object;
|
|
}
|
|
|
|
@Override
|
|
public void removeView(View view) {
|
|
if (mInLayout) {
|
|
removeViewInLayout(view);
|
|
} else {
|
|
super.removeView(view);
|
|
}
|
|
}
|
|
|
|
ItemInfo infoForChild(View child) {
|
|
for (int i=0; i<mItems.size(); i++) {
|
|
ItemInfo ii = mItems.get(i);
|
|
if (mAdapter.isViewFromObject(child, ii.object)) {
|
|
return ii;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
ItemInfo infoForAnyChild(View child) {
|
|
ViewParent parent;
|
|
while ((parent=child.getParent()) != this) {
|
|
if (parent == null || !(parent instanceof View)) {
|
|
return null;
|
|
}
|
|
child = (View)parent;
|
|
}
|
|
return infoForChild(child);
|
|
}
|
|
|
|
ItemInfo infoForPosition(int position) {
|
|
for (int i = 0; i < mItems.size(); i++) {
|
|
ItemInfo ii = mItems.get(i);
|
|
if (ii.position == position) {
|
|
return ii;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
protected void onAttachedToWindow() {
|
|
super.onAttachedToWindow();
|
|
mFirstLayout = true;
|
|
}
|
|
|
|
@Override
|
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
// For simple implementation, our internal size is always 0.
|
|
// We depend on the container to specify the layout size of
|
|
// our view. We can't really know what it is since we will be
|
|
// adding and removing different arbitrary views and do not
|
|
// want the layout to change as this happens.
|
|
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
|
|
getDefaultSize(0, heightMeasureSpec));
|
|
|
|
final int measuredWidth = getMeasuredWidth();
|
|
final int maxGutterSize = measuredWidth / 10;
|
|
mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
|
|
|
|
// Children are just made to fill our space.
|
|
int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
|
|
int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
|
|
|
|
/*
|
|
* Make sure all children have been properly measured. Decor views first.
|
|
* Right now we cheat and make this less complicated by assuming decor
|
|
* views won't intersect. We will pin to edges based on gravity.
|
|
*/
|
|
int size = getChildCount();
|
|
for (int i = 0; i < size; ++i) {
|
|
final View child = getChildAt(i);
|
|
if (child.getVisibility() != GONE) {
|
|
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
|
|
if (lp != null && lp.isDecor) {
|
|
final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
|
|
final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
|
|
int widthMode = MeasureSpec.AT_MOST;
|
|
int heightMode = MeasureSpec.AT_MOST;
|
|
boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
|
|
boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;
|
|
|
|
if (consumeVertical) {
|
|
widthMode = MeasureSpec.EXACTLY;
|
|
} else if (consumeHorizontal) {
|
|
heightMode = MeasureSpec.EXACTLY;
|
|
}
|
|
|
|
int widthSize = childWidthSize;
|
|
int heightSize = childHeightSize;
|
|
if (lp.width != LayoutParams.WRAP_CONTENT) {
|
|
widthMode = MeasureSpec.EXACTLY;
|
|
if (lp.width != LayoutParams.FILL_PARENT) {
|
|
widthSize = lp.width;
|
|
}
|
|
}
|
|
if (lp.height != LayoutParams.WRAP_CONTENT) {
|
|
heightMode = MeasureSpec.EXACTLY;
|
|
if (lp.height != LayoutParams.FILL_PARENT) {
|
|
heightSize = lp.height;
|
|
}
|
|
}
|
|
final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
|
|
final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
|
|
child.measure(widthSpec, heightSpec);
|
|
|
|
if (consumeVertical) {
|
|
childHeightSize -= child.getMeasuredHeight();
|
|
} else if (consumeHorizontal) {
|
|
childWidthSize -= child.getMeasuredWidth();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
|
|
mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
|
|
|
|
// Make sure we have created all fragments that we need to have shown.
|
|
mInLayout = true;
|
|
populate();
|
|
mInLayout = false;
|
|
|
|
// Page views next.
|
|
size = getChildCount();
|
|
for (int i = 0; i < size; ++i) {
|
|
final View child = getChildAt(i);
|
|
if (child.getVisibility() != GONE) {
|
|
if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
|
|
+ ": " + mChildWidthMeasureSpec);
|
|
|
|
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
|
|
if (lp == null || !lp.isDecor) {
|
|
final int widthSpec = MeasureSpec.makeMeasureSpec(
|
|
(int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
|
|
child.measure(widthSpec, mChildHeightMeasureSpec);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
|
super.onSizeChanged(w, h, oldw, oldh);
|
|
|
|
// Make sure scroll position is set correctly.
|
|
if (w != oldw) {
|
|
recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin);
|
|
}
|
|
}
|
|
|
|
private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) {
|
|
if (oldWidth > 0 && !mItems.isEmpty()) {
|
|
final int widthWithMargin = width - getPaddingLeft() - getPaddingRight() + margin;
|
|
final int oldWidthWithMargin = oldWidth - getPaddingLeft() - getPaddingRight()
|
|
+ oldMargin;
|
|
final int xpos = getScrollX();
|
|
final float pageOffset = (float) xpos / oldWidthWithMargin;
|
|
final int newOffsetPixels = (int) (pageOffset * widthWithMargin);
|
|
|
|
scrollTo(newOffsetPixels, getScrollY());
|
|
if (!mScroller.isFinished()) {
|
|
// We now return to your regularly scheduled scroll, already in progress.
|
|
final int newDuration = mScroller.getDuration() - mScroller.timePassed();
|
|
ItemInfo targetInfo = infoForPosition(mCurItem);
|
|
mScroller.startScroll(newOffsetPixels, 0,
|
|
(int) (targetInfo.offset * width), 0, newDuration);
|
|
}
|
|
} else {
|
|
final ItemInfo ii = infoForPosition(mCurItem);
|
|
final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0;
|
|
final int scrollPos = (int) (scrollOffset *
|
|
(width - getPaddingLeft() - getPaddingRight()));
|
|
if (scrollPos != getScrollX()) {
|
|
completeScroll(false);
|
|
scrollTo(scrollPos, getScrollY());
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
|
final int count = getChildCount();
|
|
int width = r - l;
|
|
int height = b - t;
|
|
int paddingLeft = getPaddingLeft();
|
|
int paddingTop = getPaddingTop();
|
|
int paddingRight = getPaddingRight();
|
|
int paddingBottom = getPaddingBottom();
|
|
final int scrollX = getScrollX();
|
|
|
|
int decorCount = 0;
|
|
|
|
// First pass - decor views. We need to do this in two passes so that
|
|
// we have the proper offsets for non-decor views later.
|
|
for (int i = 0; i < count; i++) {
|
|
final View child = getChildAt(i);
|
|
if (child.getVisibility() != GONE) {
|
|
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
|
|
int childLeft = 0;
|
|
int childTop = 0;
|
|
if (lp.isDecor) {
|
|
final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
|
|
final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
|
|
switch (hgrav) {
|
|
default:
|
|
childLeft = paddingLeft;
|
|
break;
|
|
case Gravity.LEFT:
|
|
childLeft = paddingLeft;
|
|
paddingLeft += child.getMeasuredWidth();
|
|
break;
|
|
case Gravity.CENTER_HORIZONTAL:
|
|
childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
|
|
paddingLeft);
|
|
break;
|
|
case Gravity.RIGHT:
|
|
childLeft = width - paddingRight - child.getMeasuredWidth();
|
|
paddingRight += child.getMeasuredWidth();
|
|
break;
|
|
}
|
|
switch (vgrav) {
|
|
default:
|
|
childTop = paddingTop;
|
|
break;
|
|
case Gravity.TOP:
|
|
childTop = paddingTop;
|
|
paddingTop += child.getMeasuredHeight();
|
|
break;
|
|
case Gravity.CENTER_VERTICAL:
|
|
childTop = Math.max((height - child.getMeasuredHeight()) / 2,
|
|
paddingTop);
|
|
break;
|
|
case Gravity.BOTTOM:
|
|
childTop = height - paddingBottom - child.getMeasuredHeight();
|
|
paddingBottom += child.getMeasuredHeight();
|
|
break;
|
|
}
|
|
childLeft += scrollX;
|
|
child.layout(childLeft, childTop,
|
|
childLeft + child.getMeasuredWidth(),
|
|
childTop + child.getMeasuredHeight());
|
|
decorCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
final int childWidth = width - paddingLeft - paddingRight;
|
|
// Page views. Do this once we have the right padding offsets from above.
|
|
for (int i = 0; i < count; i++) {
|
|
final View child = getChildAt(i);
|
|
if (child.getVisibility() == GONE) {
|
|
continue;
|
|
}
|
|
|
|
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
|
|
if (lp.isDecor) {
|
|
continue;
|
|
}
|
|
|
|
final ItemInfo ii = infoForChild(child);
|
|
if (ii == null) {
|
|
continue;
|
|
}
|
|
|
|
if (lp.needsMeasure) {
|
|
// This was added during layout and needs measurement.
|
|
// Do it now that we know what we're working with.
|
|
lp.needsMeasure = false;
|
|
final int widthSpec = MeasureSpec.makeMeasureSpec(
|
|
(int) (childWidth * lp.widthFactor),
|
|
MeasureSpec.EXACTLY);
|
|
final int heightSpec = MeasureSpec.makeMeasureSpec(
|
|
(int) (height - paddingTop - paddingBottom),
|
|
MeasureSpec.EXACTLY);
|
|
child.measure(widthSpec, heightSpec);
|
|
}
|
|
|
|
final int childMeasuredWidth = child.getMeasuredWidth();
|
|
final int startOffset = (int) (childWidth * ii.offset);
|
|
final int childLeft;
|
|
if (isLayoutRtl()) {
|
|
childLeft = MAX_SCROLL_X - paddingRight - startOffset - childMeasuredWidth;
|
|
} else {
|
|
childLeft = paddingLeft + startOffset;
|
|
}
|
|
|
|
final int childTop = paddingTop;
|
|
child.layout(childLeft, childTop, childLeft + childMeasuredWidth,
|
|
childTop + child.getMeasuredHeight());
|
|
}
|
|
|
|
mTopPageBounds = paddingTop;
|
|
mBottomPageBounds = height - paddingBottom;
|
|
mDecorChildCount = decorCount;
|
|
|
|
if (mFirstLayout) {
|
|
scrollToItem(mCurItem, false, 0, false);
|
|
}
|
|
mFirstLayout = false;
|
|
}
|
|
|
|
@Override
|
|
public void computeScroll() {
|
|
if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
|
|
final int oldX = getScrollX();
|
|
final int oldY = getScrollY();
|
|
final int x = mScroller.getCurrX();
|
|
final int y = mScroller.getCurrY();
|
|
|
|
if (oldX != x || oldY != y) {
|
|
scrollTo(x, y);
|
|
|
|
if (!pageScrolled(x)) {
|
|
mScroller.abortAnimation();
|
|
scrollTo(0, y);
|
|
}
|
|
}
|
|
|
|
// Keep on drawing until the animation has finished.
|
|
postInvalidateOnAnimation();
|
|
return;
|
|
}
|
|
|
|
// Done with scroll, clean up state.
|
|
completeScroll(true);
|
|
}
|
|
|
|
private boolean pageScrolled(int scrollX) {
|
|
if (mItems.size() == 0) {
|
|
mCalledSuper = false;
|
|
onPageScrolled(0, 0, 0);
|
|
if (!mCalledSuper) {
|
|
throw new IllegalStateException(
|
|
"onPageScrolled did not call superclass implementation");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Translate to scrollX to scrollStart for RTL.
|
|
final int scrollStart;
|
|
if (isLayoutRtl()) {
|
|
scrollStart = MAX_SCROLL_X - scrollX;
|
|
} else {
|
|
scrollStart = scrollX;
|
|
}
|
|
|
|
final ItemInfo ii = infoForFirstVisiblePage();
|
|
final int width = getPaddedWidth();
|
|
final int widthWithMargin = width + mPageMargin;
|
|
final float marginOffset = (float) mPageMargin / width;
|
|
final int currentPage = ii.position;
|
|
final float pageOffset = (((float) scrollStart / width) - ii.offset) /
|
|
(ii.widthFactor + marginOffset);
|
|
final int offsetPixels = (int) (pageOffset * widthWithMargin);
|
|
|
|
mCalledSuper = false;
|
|
onPageScrolled(currentPage, pageOffset, offsetPixels);
|
|
if (!mCalledSuper) {
|
|
throw new IllegalStateException(
|
|
"onPageScrolled did not call superclass implementation");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* If you override this method you must call through to the superclass implementation
|
|
* (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled
|
|
* returns.
|
|
*
|
|
* @param position Position index of the first page currently being displayed.
|
|
* Page position+1 will be visible if positionOffset is nonzero.
|
|
* @param offset Value from [0, 1) indicating the offset from the page at position.
|
|
* @param offsetPixels Value in pixels indicating the offset from position.
|
|
*/
|
|
protected void onPageScrolled(int position, float offset, int offsetPixels) {
|
|
// Offset any decor views if needed - keep them on-screen at all times.
|
|
if (mDecorChildCount > 0) {
|
|
final int scrollX = getScrollX();
|
|
int paddingLeft = getPaddingLeft();
|
|
int paddingRight = getPaddingRight();
|
|
final int width = getWidth();
|
|
final int childCount = getChildCount();
|
|
for (int i = 0; i < childCount; i++) {
|
|
final View child = getChildAt(i);
|
|
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
|
|
if (!lp.isDecor) continue;
|
|
|
|
final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
|
|
int childLeft = 0;
|
|
switch (hgrav) {
|
|
default:
|
|
childLeft = paddingLeft;
|
|
break;
|
|
case Gravity.LEFT:
|
|
childLeft = paddingLeft;
|
|
paddingLeft += child.getWidth();
|
|
break;
|
|
case Gravity.CENTER_HORIZONTAL:
|
|
childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
|
|
paddingLeft);
|
|
break;
|
|
case Gravity.RIGHT:
|
|
childLeft = width - paddingRight - child.getMeasuredWidth();
|
|
paddingRight += child.getMeasuredWidth();
|
|
break;
|
|
}
|
|
childLeft += scrollX;
|
|
|
|
final int childOffset = childLeft - child.getLeft();
|
|
if (childOffset != 0) {
|
|
child.offsetLeftAndRight(childOffset);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mOnPageChangeListener != null) {
|
|
mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);
|
|
}
|
|
if (mInternalPageChangeListener != null) {
|
|
mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels);
|
|
}
|
|
|
|
if (mPageTransformer != null) {
|
|
final int scrollX = getScrollX();
|
|
final int childCount = getChildCount();
|
|
for (int i = 0; i < childCount; i++) {
|
|
final View child = getChildAt(i);
|
|
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
|
|
|
|
if (lp.isDecor) continue;
|
|
|
|
final float transformPos = (float) (child.getLeft() - scrollX) / getPaddedWidth();
|
|
mPageTransformer.transformPage(child, transformPos);
|
|
}
|
|
}
|
|
|
|
mCalledSuper = true;
|
|
}
|
|
|
|
private void completeScroll(boolean postEvents) {
|
|
boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING;
|
|
if (needPopulate) {
|
|
// Done with scroll, no longer want to cache view drawing.
|
|
setScrollingCacheEnabled(false);
|
|
mScroller.abortAnimation();
|
|
int oldX = getScrollX();
|
|
int oldY = getScrollY();
|
|
int x = mScroller.getCurrX();
|
|
int y = mScroller.getCurrY();
|
|
if (oldX != x || oldY != y) {
|
|
scrollTo(x, y);
|
|
}
|
|
}
|
|
mPopulatePending = false;
|
|
for (int i=0; i<mItems.size(); i++) {
|
|
ItemInfo ii = mItems.get(i);
|
|
if (ii.scrolling) {
|
|
needPopulate = true;
|
|
ii.scrolling = false;
|
|
}
|
|
}
|
|
if (needPopulate) {
|
|
if (postEvents) {
|
|
postOnAnimation(mEndScrollRunnable);
|
|
} else {
|
|
mEndScrollRunnable.run();
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean isGutterDrag(float x, float dx) {
|
|
return (x < mGutterSize && dx > 0) || (x > getWidth() - mGutterSize && dx < 0);
|
|
}
|
|
|
|
private void enableLayers(boolean enable) {
|
|
final int childCount = getChildCount();
|
|
for (int i = 0; i < childCount; i++) {
|
|
final int layerType = enable ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE;
|
|
getChildAt(i).setLayerType(layerType, null);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
|
/*
|
|
* This method JUST determines whether we want to intercept the motion.
|
|
* If we return true, onMotionEvent will be called and we do the actual
|
|
* scrolling there.
|
|
*/
|
|
|
|
final int action = ev.getAction() & MotionEvent.ACTION_MASK;
|
|
|
|
// Always take care of the touch gesture being complete.
|
|
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
|
|
// Release the drag.
|
|
if (DEBUG) Log.v(TAG, "Intercept done!");
|
|
mIsBeingDragged = false;
|
|
mIsUnableToDrag = false;
|
|
mActivePointerId = INVALID_POINTER;
|
|
if (mVelocityTracker != null) {
|
|
mVelocityTracker.recycle();
|
|
mVelocityTracker = null;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Nothing more to do here if we have decided whether or not we
|
|
// are dragging.
|
|
if (action != MotionEvent.ACTION_DOWN) {
|
|
if (mIsBeingDragged) {
|
|
if (DEBUG) Log.v(TAG, "Being dragged, intercept returning true!");
|
|
return true;
|
|
}
|
|
if (mIsUnableToDrag) {
|
|
if (DEBUG) Log.v(TAG, "Unable to drag, intercept returning false!");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
switch (action) {
|
|
case MotionEvent.ACTION_MOVE: {
|
|
/*
|
|
* mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
|
|
* whether the user has moved far enough from their original down touch.
|
|
*/
|
|
|
|
/*
|
|
* Locally do absolute value. mLastMotionY is set to the y value
|
|
* of the down event.
|
|
*/
|
|
final int activePointerId = mActivePointerId;
|
|
if (activePointerId == INVALID_POINTER) {
|
|
// If we don't have a valid id, the touch down wasn't on content.
|
|
break;
|
|
}
|
|
|
|
final int pointerIndex = ev.findPointerIndex(activePointerId);
|
|
final float x = ev.getX(pointerIndex);
|
|
final float dx = x - mLastMotionX;
|
|
final float xDiff = Math.abs(dx);
|
|
final float y = ev.getY(pointerIndex);
|
|
final float yDiff = Math.abs(y - mInitialMotionY);
|
|
if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
|
|
|
|
if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
|
|
canScroll(this, false, (int) dx, (int) x, (int) y)) {
|
|
// Nested view has scrollable area under this point. Let it be handled there.
|
|
mLastMotionX = x;
|
|
mLastMotionY = y;
|
|
mIsUnableToDrag = true;
|
|
return false;
|
|
}
|
|
if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
|
|
if (DEBUG) Log.v(TAG, "Starting drag!");
|
|
mIsBeingDragged = true;
|
|
requestParentDisallowInterceptTouchEvent(true);
|
|
setScrollState(SCROLL_STATE_DRAGGING);
|
|
mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
|
|
mInitialMotionX - mTouchSlop;
|
|
mLastMotionY = y;
|
|
setScrollingCacheEnabled(true);
|
|
} else if (yDiff > mTouchSlop) {
|
|
// The finger has moved enough in the vertical
|
|
// direction to be counted as a drag... abort
|
|
// any attempt to drag horizontally, to work correctly
|
|
// with children that have scrolling containers.
|
|
if (DEBUG) Log.v(TAG, "Starting unable to drag!");
|
|
mIsUnableToDrag = true;
|
|
}
|
|
if (mIsBeingDragged) {
|
|
// Scroll to follow the motion event
|
|
if (performDrag(x, y)) {
|
|
postInvalidateOnAnimation();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case MotionEvent.ACTION_DOWN: {
|
|
/*
|
|
* Remember location of down touch.
|
|
* ACTION_DOWN always refers to pointer index 0.
|
|
*/
|
|
mLastMotionX = mInitialMotionX = ev.getX();
|
|
mLastMotionY = mInitialMotionY = ev.getY();
|
|
mActivePointerId = ev.getPointerId(0);
|
|
mIsUnableToDrag = false;
|
|
|
|
mScroller.computeScrollOffset();
|
|
if (mScrollState == SCROLL_STATE_SETTLING &&
|
|
Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
|
|
// Let the user 'catch' the pager as it animates.
|
|
mScroller.abortAnimation();
|
|
mPopulatePending = false;
|
|
populate();
|
|
mIsBeingDragged = true;
|
|
requestParentDisallowInterceptTouchEvent(true);
|
|
setScrollState(SCROLL_STATE_DRAGGING);
|
|
} else if (mLeftEdge.getDistance() != 0
|
|
|| mRightEdge.getDistance() != 0) {
|
|
// Caught the edge glow animation
|
|
mIsBeingDragged = true;
|
|
setScrollState(SCROLL_STATE_DRAGGING);
|
|
if (mLeftEdge.getDistance() != 0) {
|
|
mLeftEdge.onPullDistance(0f, 1 - mLastMotionY / getHeight());
|
|
}
|
|
if (mRightEdge.getDistance() != 0) {
|
|
mRightEdge.onPullDistance(0f, mLastMotionY / getHeight());
|
|
}
|
|
} else {
|
|
completeScroll(false);
|
|
mIsBeingDragged = false;
|
|
}
|
|
|
|
if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
|
|
+ " mIsBeingDragged=" + mIsBeingDragged
|
|
+ "mIsUnableToDrag=" + mIsUnableToDrag);
|
|
break;
|
|
}
|
|
|
|
case MotionEvent.ACTION_POINTER_UP:
|
|
onSecondaryPointerUp(ev);
|
|
break;
|
|
}
|
|
|
|
if (mVelocityTracker == null) {
|
|
mVelocityTracker = VelocityTracker.obtain();
|
|
}
|
|
mVelocityTracker.addMovement(ev);
|
|
|
|
/*
|
|
* The only time we want to intercept motion events is if we are in the
|
|
* drag mode.
|
|
*/
|
|
return mIsBeingDragged;
|
|
}
|
|
|
|
@Override
|
|
public boolean onTouchEvent(MotionEvent ev) {
|
|
if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
|
|
// Don't handle edge touches immediately -- they may actually belong to one of our
|
|
// descendants.
|
|
return false;
|
|
}
|
|
|
|
if (mAdapter == null || mAdapter.getCount() == 0) {
|
|
// Nothing to present or scroll; nothing to touch.
|
|
return false;
|
|
}
|
|
|
|
if (mVelocityTracker == null) {
|
|
mVelocityTracker = VelocityTracker.obtain();
|
|
}
|
|
mVelocityTracker.addMovement(ev);
|
|
|
|
final int action = ev.getAction();
|
|
boolean needsInvalidate = false;
|
|
|
|
switch (action & MotionEvent.ACTION_MASK) {
|
|
case MotionEvent.ACTION_DOWN: {
|
|
mScroller.abortAnimation();
|
|
mPopulatePending = false;
|
|
populate();
|
|
|
|
// Remember where the motion event started
|
|
mLastMotionX = mInitialMotionX = ev.getX();
|
|
mLastMotionY = mInitialMotionY = ev.getY();
|
|
mActivePointerId = ev.getPointerId(0);
|
|
break;
|
|
}
|
|
case MotionEvent.ACTION_MOVE:
|
|
if (!mIsBeingDragged) {
|
|
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
|
|
final float x = ev.getX(pointerIndex);
|
|
final float xDiff = Math.abs(x - mLastMotionX);
|
|
final float y = ev.getY(pointerIndex);
|
|
final float yDiff = Math.abs(y - mLastMotionY);
|
|
if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
|
|
if (xDiff > mTouchSlop && xDiff > yDiff) {
|
|
if (DEBUG) Log.v(TAG, "Starting drag!");
|
|
mIsBeingDragged = true;
|
|
requestParentDisallowInterceptTouchEvent(true);
|
|
mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
|
|
mInitialMotionX - mTouchSlop;
|
|
mLastMotionY = y;
|
|
setScrollState(SCROLL_STATE_DRAGGING);
|
|
setScrollingCacheEnabled(true);
|
|
|
|
// Disallow Parent Intercept, just in case
|
|
ViewParent parent = getParent();
|
|
if (parent != null) {
|
|
parent.requestDisallowInterceptTouchEvent(true);
|
|
}
|
|
}
|
|
}
|
|
// Not else! Note that mIsBeingDragged can be set above.
|
|
if (mIsBeingDragged) {
|
|
// Scroll to follow the motion event
|
|
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
|
|
final float x = ev.getX(activePointerIndex);
|
|
needsInvalidate |= performDrag(x, ev.getY(activePointerIndex));
|
|
}
|
|
break;
|
|
case MotionEvent.ACTION_UP:
|
|
if (mIsBeingDragged) {
|
|
final VelocityTracker velocityTracker = mVelocityTracker;
|
|
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
|
|
final int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
|
|
|
|
mPopulatePending = true;
|
|
|
|
final float scrollStart = getScrollStart();
|
|
final float scrolledPages = scrollStart / getPaddedWidth();
|
|
final ItemInfo ii = infoForFirstVisiblePage();
|
|
final int currentPage = ii.position;
|
|
final float nextPageOffset;
|
|
if (isLayoutRtl()) {
|
|
nextPageOffset = (ii.offset - scrolledPages) / ii.widthFactor;
|
|
} else {
|
|
nextPageOffset = (scrolledPages - ii.offset) / ii.widthFactor;
|
|
}
|
|
|
|
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
|
|
final float x = ev.getX(activePointerIndex);
|
|
final int totalDelta = (int) (x - mInitialMotionX);
|
|
final int nextPage = determineTargetPage(
|
|
currentPage, nextPageOffset, initialVelocity, totalDelta);
|
|
setCurrentItemInternal(nextPage, true, true, initialVelocity);
|
|
|
|
mActivePointerId = INVALID_POINTER;
|
|
endDrag();
|
|
mLeftEdge.onRelease();
|
|
mRightEdge.onRelease();
|
|
needsInvalidate = true;
|
|
}
|
|
break;
|
|
case MotionEvent.ACTION_CANCEL:
|
|
if (mIsBeingDragged) {
|
|
scrollToItem(mCurItem, true, 0, false);
|
|
mActivePointerId = INVALID_POINTER;
|
|
endDrag();
|
|
mLeftEdge.onRelease();
|
|
mRightEdge.onRelease();
|
|
needsInvalidate = true;
|
|
}
|
|
break;
|
|
case MotionEvent.ACTION_POINTER_DOWN: {
|
|
final int index = ev.getActionIndex();
|
|
final float x = ev.getX(index);
|
|
mLastMotionX = x;
|
|
mActivePointerId = ev.getPointerId(index);
|
|
break;
|
|
}
|
|
case MotionEvent.ACTION_POINTER_UP:
|
|
onSecondaryPointerUp(ev);
|
|
mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));
|
|
break;
|
|
}
|
|
if (needsInvalidate) {
|
|
postInvalidateOnAnimation();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
|
|
final ViewParent parent = getParent();
|
|
if (parent != null) {
|
|
parent.requestDisallowInterceptTouchEvent(disallowIntercept);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If either of the horizontal edge glows are currently active, this consumes part or all of
|
|
* deltaX on the edge glow.
|
|
*
|
|
* @param deltaX The pointer motion, in pixels, in the horizontal direction, positive
|
|
* for moving down and negative for moving up.
|
|
* @param y The vertical position of the pointer.
|
|
* @return The amount of <code>deltaX</code> that has been consumed by the
|
|
* edge glow.
|
|
*/
|
|
private float releaseHorizontalGlow(float deltaX, float y) {
|
|
// First allow releasing existing overscroll effect:
|
|
float consumed = 0;
|
|
float displacement = y / getHeight();
|
|
float pullDistance = (float) deltaX / getWidth();
|
|
if (mLeftEdge.getDistance() != 0) {
|
|
consumed = -mLeftEdge.onPullDistance(-pullDistance, 1 - displacement);
|
|
} else if (mRightEdge.getDistance() != 0) {
|
|
consumed = mRightEdge.onPullDistance(pullDistance, displacement);
|
|
}
|
|
return consumed * getWidth();
|
|
}
|
|
|
|
private boolean performDrag(float x, float y) {
|
|
boolean needsInvalidate = false;
|
|
|
|
final float dX = mLastMotionX - x;
|
|
final int width = getPaddedWidth();
|
|
mLastMotionX = x;
|
|
final float releaseConsumed = releaseHorizontalGlow(dX, y);
|
|
final float deltaX = dX - releaseConsumed;
|
|
if (releaseConsumed != 0) {
|
|
needsInvalidate = true;
|
|
}
|
|
if (Math.abs(deltaX) < 0.0001f) { // ignore rounding errors from releaseHorizontalGlow()
|
|
return needsInvalidate;
|
|
}
|
|
|
|
final EdgeEffect startEdge;
|
|
final EdgeEffect endEdge;
|
|
if (isLayoutRtl()) {
|
|
startEdge = mRightEdge;
|
|
endEdge = mLeftEdge;
|
|
} else {
|
|
startEdge = mLeftEdge;
|
|
endEdge = mRightEdge;
|
|
}
|
|
|
|
// Translate scroll to relative coordinates.
|
|
final float nextScrollX = getScrollX() + deltaX;
|
|
final float scrollStart;
|
|
if (isLayoutRtl()) {
|
|
scrollStart = MAX_SCROLL_X - nextScrollX;
|
|
} else {
|
|
scrollStart = nextScrollX;
|
|
}
|
|
|
|
final float startBound;
|
|
final ItemInfo startItem = mItems.get(0);
|
|
final boolean startAbsolute = startItem.position == 0;
|
|
if (startAbsolute) {
|
|
startBound = startItem.offset * width;
|
|
} else {
|
|
startBound = width * mFirstOffset;
|
|
}
|
|
|
|
final float endBound;
|
|
final ItemInfo endItem = mItems.get(mItems.size() - 1);
|
|
final boolean endAbsolute = endItem.position == mAdapter.getCount() - 1;
|
|
if (endAbsolute) {
|
|
endBound = endItem.offset * width;
|
|
} else {
|
|
endBound = width * mLastOffset;
|
|
}
|
|
|
|
final float clampedScrollStart;
|
|
if (scrollStart < startBound) {
|
|
if (startAbsolute) {
|
|
final float over = startBound - scrollStart;
|
|
startEdge.onPullDistance(over / width, 1 - y / getHeight());
|
|
needsInvalidate = true;
|
|
}
|
|
clampedScrollStart = startBound;
|
|
} else if (scrollStart > endBound) {
|
|
if (endAbsolute) {
|
|
final float over = scrollStart - endBound;
|
|
endEdge.onPullDistance(over / width, y / getHeight());
|
|
needsInvalidate = true;
|
|
}
|
|
clampedScrollStart = endBound;
|
|
} else {
|
|
clampedScrollStart = scrollStart;
|
|
}
|
|
|
|
// Translate back to absolute coordinates.
|
|
final float targetScrollX;
|
|
if (isLayoutRtl()) {
|
|
targetScrollX = MAX_SCROLL_X - clampedScrollStart;
|
|
} else {
|
|
targetScrollX = clampedScrollStart;
|
|
}
|
|
|
|
// Don't lose the rounded component.
|
|
mLastMotionX += targetScrollX - (int) targetScrollX;
|
|
|
|
scrollTo((int) targetScrollX, getScrollY());
|
|
pageScrolled((int) targetScrollX);
|
|
|
|
return needsInvalidate;
|
|
}
|
|
|
|
/**
|
|
* @return Info about the page at the current scroll position.
|
|
* This can be synthetic for a missing middle page; the 'object' field can be null.
|
|
*/
|
|
private ItemInfo infoForFirstVisiblePage() {
|
|
final int startOffset = getScrollStart();
|
|
final int width = getPaddedWidth();
|
|
final float scrollOffset = width > 0 ? (float) startOffset / width : 0;
|
|
final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
|
|
|
|
int lastPos = -1;
|
|
float lastOffset = 0.f;
|
|
float lastWidth = 0.f;
|
|
boolean first = true;
|
|
ItemInfo lastItem = null;
|
|
|
|
final int N = mItems.size();
|
|
for (int i = 0; i < N; i++) {
|
|
ItemInfo ii = mItems.get(i);
|
|
|
|
// Seek to position.
|
|
if (!first && ii.position != lastPos + 1) {
|
|
// Create a synthetic item for a missing page.
|
|
ii = mTempItem;
|
|
ii.offset = lastOffset + lastWidth + marginOffset;
|
|
ii.position = lastPos + 1;
|
|
ii.widthFactor = mAdapter.getPageWidth(ii.position);
|
|
i--;
|
|
}
|
|
|
|
final float offset = ii.offset;
|
|
final float startBound = offset;
|
|
if (first || scrollOffset >= startBound) {
|
|
final float endBound = offset + ii.widthFactor + marginOffset;
|
|
if (scrollOffset < endBound || i == mItems.size() - 1) {
|
|
return ii;
|
|
}
|
|
} else {
|
|
return lastItem;
|
|
}
|
|
|
|
first = false;
|
|
lastPos = ii.position;
|
|
lastOffset = offset;
|
|
lastWidth = ii.widthFactor;
|
|
lastItem = ii;
|
|
}
|
|
|
|
return lastItem;
|
|
}
|
|
|
|
private int getScrollStart() {
|
|
if (isLayoutRtl()) {
|
|
return MAX_SCROLL_X - getScrollX();
|
|
} else {
|
|
return getScrollX();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param currentPage the position of the page with the first visible starting edge
|
|
* @param pageOffset the fraction of the right-hand page that's visible
|
|
* @param velocity the velocity of the touch event stream
|
|
* @param deltaX the distance of the touch event stream
|
|
* @return the position of the target page
|
|
*/
|
|
private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
|
|
int targetPage;
|
|
if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity
|
|
&& mLeftEdge.getDistance() == 0 // don't fling while stretched
|
|
&& mRightEdge.getDistance() == 0) {
|
|
targetPage = currentPage - (velocity < 0 ? mLeftIncr : 0);
|
|
} else {
|
|
final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
|
|
targetPage = (int) (currentPage - mLeftIncr * (pageOffset + truncator));
|
|
}
|
|
|
|
if (mItems.size() > 0) {
|
|
final ItemInfo firstItem = mItems.get(0);
|
|
final ItemInfo lastItem = mItems.get(mItems.size() - 1);
|
|
|
|
// Only let the user target pages we have items for
|
|
targetPage = MathUtils.constrain(targetPage, firstItem.position, lastItem.position);
|
|
}
|
|
|
|
return targetPage;
|
|
}
|
|
|
|
@Override
|
|
public void draw(Canvas canvas) {
|
|
super.draw(canvas);
|
|
boolean needsInvalidate = false;
|
|
|
|
final int overScrollMode = getOverScrollMode();
|
|
if (overScrollMode == View.OVER_SCROLL_ALWAYS ||
|
|
(overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS &&
|
|
mAdapter != null && mAdapter.getCount() > 1)) {
|
|
if (!mLeftEdge.isFinished()) {
|
|
final int restoreCount = canvas.save();
|
|
final int height = getHeight() - getPaddingTop() - getPaddingBottom();
|
|
final int width = getWidth();
|
|
|
|
canvas.rotate(270);
|
|
canvas.translate(-height + getPaddingTop(), mFirstOffset * width);
|
|
mLeftEdge.setSize(height, width);
|
|
needsInvalidate |= mLeftEdge.draw(canvas);
|
|
canvas.restoreToCount(restoreCount);
|
|
}
|
|
if (!mRightEdge.isFinished()) {
|
|
final int restoreCount = canvas.save();
|
|
final int width = getWidth();
|
|
final int height = getHeight() - getPaddingTop() - getPaddingBottom();
|
|
|
|
canvas.rotate(90);
|
|
canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width);
|
|
mRightEdge.setSize(height, width);
|
|
needsInvalidate |= mRightEdge.draw(canvas);
|
|
canvas.restoreToCount(restoreCount);
|
|
}
|
|
} else {
|
|
mLeftEdge.finish();
|
|
mRightEdge.finish();
|
|
}
|
|
|
|
if (needsInvalidate) {
|
|
// Keep animating
|
|
postInvalidateOnAnimation();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onDraw(Canvas canvas) {
|
|
super.onDraw(canvas);
|
|
|
|
// Draw the margin drawable between pages if needed.
|
|
if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {
|
|
final int scrollX = getScrollX();
|
|
final int width = getWidth();
|
|
|
|
final float marginOffset = (float) mPageMargin / width;
|
|
int itemIndex = 0;
|
|
ItemInfo ii = mItems.get(0);
|
|
float offset = ii.offset;
|
|
|
|
final int itemCount = mItems.size();
|
|
final int firstPos = ii.position;
|
|
final int lastPos = mItems.get(itemCount - 1).position;
|
|
for (int pos = firstPos; pos < lastPos; pos++) {
|
|
while (pos > ii.position && itemIndex < itemCount) {
|
|
ii = mItems.get(++itemIndex);
|
|
}
|
|
|
|
final float itemOffset;
|
|
final float widthFactor;
|
|
if (pos == ii.position) {
|
|
itemOffset = ii.offset;
|
|
widthFactor = ii.widthFactor;
|
|
} else {
|
|
itemOffset = offset;
|
|
widthFactor = mAdapter.getPageWidth(pos);
|
|
}
|
|
|
|
final float left;
|
|
final float scaledOffset = itemOffset * width;
|
|
if (isLayoutRtl()) {
|
|
left = MAX_SCROLL_X - scaledOffset;
|
|
} else {
|
|
left = scaledOffset + widthFactor * width;
|
|
}
|
|
|
|
offset = itemOffset + widthFactor + marginOffset;
|
|
|
|
if (left + mPageMargin > scrollX) {
|
|
mMarginDrawable.setBounds((int) left, mTopPageBounds,
|
|
(int) (left + mPageMargin + 0.5f), mBottomPageBounds);
|
|
mMarginDrawable.draw(canvas);
|
|
}
|
|
|
|
if (left > scrollX + width) {
|
|
break; // No more visible, no sense in continuing
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void onSecondaryPointerUp(MotionEvent ev) {
|
|
final int pointerIndex = ev.getActionIndex();
|
|
final int pointerId = ev.getPointerId(pointerIndex);
|
|
if (pointerId == mActivePointerId) {
|
|
// This was our active pointer going up. Choose a new
|
|
// active pointer and adjust accordingly.
|
|
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
|
|
mLastMotionX = ev.getX(newPointerIndex);
|
|
mActivePointerId = ev.getPointerId(newPointerIndex);
|
|
if (mVelocityTracker != null) {
|
|
mVelocityTracker.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void endDrag() {
|
|
mIsBeingDragged = false;
|
|
mIsUnableToDrag = false;
|
|
|
|
if (mVelocityTracker != null) {
|
|
mVelocityTracker.recycle();
|
|
mVelocityTracker = null;
|
|
}
|
|
}
|
|
|
|
private void setScrollingCacheEnabled(boolean enabled) {
|
|
if (mScrollingCacheEnabled != enabled) {
|
|
mScrollingCacheEnabled = enabled;
|
|
if (USE_CACHE) {
|
|
final int size = getChildCount();
|
|
for (int i = 0; i < size; ++i) {
|
|
final View child = getChildAt(i);
|
|
if (child.getVisibility() != GONE) {
|
|
child.setDrawingCacheEnabled(enabled);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public boolean canScrollHorizontally(int direction) {
|
|
if (mAdapter == null) {
|
|
return false;
|
|
}
|
|
|
|
final int width = getPaddedWidth();
|
|
final int scrollX = getScrollX();
|
|
if (direction < 0) {
|
|
return (scrollX > (int) (width * mFirstOffset));
|
|
} else if (direction > 0) {
|
|
return (scrollX < (int) (width * mLastOffset));
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tests scrollability within child views of v given a delta of dx.
|
|
*
|
|
* @param v View to test for horizontal scrollability
|
|
* @param checkV Whether the view v passed should itself be checked for scrollability (true),
|
|
* or just its children (false).
|
|
* @param dx Delta scrolled in pixels
|
|
* @param x X coordinate of the active touch point
|
|
* @param y Y coordinate of the active touch point
|
|
* @return true if child views of v can be scrolled by delta of dx.
|
|
*/
|
|
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
|
|
if (v instanceof ViewGroup) {
|
|
final ViewGroup group = (ViewGroup) v;
|
|
final int scrollX = v.getScrollX();
|
|
final int scrollY = v.getScrollY();
|
|
final int count = group.getChildCount();
|
|
// Count backwards - let topmost views consume scroll distance first.
|
|
for (int i = count - 1; i >= 0; i--) {
|
|
// TODO: Add support for transformed views.
|
|
final View child = group.getChildAt(i);
|
|
if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight()
|
|
&& y + scrollY >= child.getTop() && y + scrollY < child.getBottom()
|
|
&& canScroll(child, true, dx, x + scrollX - child.getLeft(),
|
|
y + scrollY - child.getTop())) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return checkV && v.canScrollHorizontally(-dx);
|
|
}
|
|
|
|
@Override
|
|
public boolean dispatchKeyEvent(KeyEvent event) {
|
|
// Let the focused view and/or our descendants get the key first
|
|
return super.dispatchKeyEvent(event) || executeKeyEvent(event);
|
|
}
|
|
|
|
/**
|
|
* You can call this function yourself to have the scroll view perform
|
|
* scrolling from a key event, just as if the event had been dispatched to
|
|
* it by the view hierarchy.
|
|
*
|
|
* @param event The key event to execute.
|
|
* @return Return true if the event was handled, else false.
|
|
*/
|
|
public boolean executeKeyEvent(KeyEvent event) {
|
|
boolean handled = false;
|
|
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
|
switch (event.getKeyCode()) {
|
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
|
handled = arrowScroll(FOCUS_LEFT);
|
|
break;
|
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
|
handled = arrowScroll(FOCUS_RIGHT);
|
|
break;
|
|
case KeyEvent.KEYCODE_TAB:
|
|
if (event.hasNoModifiers()) {
|
|
handled = arrowScroll(FOCUS_FORWARD);
|
|
} else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
|
|
handled = arrowScroll(FOCUS_BACKWARD);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return handled;
|
|
}
|
|
|
|
public boolean arrowScroll(int direction) {
|
|
View currentFocused = findFocus();
|
|
if (currentFocused == this) {
|
|
currentFocused = null;
|
|
} else if (currentFocused != null) {
|
|
boolean isChild = false;
|
|
for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
|
|
parent = parent.getParent()) {
|
|
if (parent == this) {
|
|
isChild = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!isChild) {
|
|
// This would cause the focus search down below to fail in fun ways.
|
|
final StringBuilder sb = new StringBuilder();
|
|
sb.append(currentFocused.getClass().getSimpleName());
|
|
for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
|
|
parent = parent.getParent()) {
|
|
sb.append(" => ").append(parent.getClass().getSimpleName());
|
|
}
|
|
Log.e(TAG, "arrowScroll tried to find focus based on non-child " +
|
|
"current focused view " + sb.toString());
|
|
currentFocused = null;
|
|
}
|
|
}
|
|
|
|
boolean handled = false;
|
|
|
|
View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused,
|
|
direction);
|
|
if (nextFocused != null && nextFocused != currentFocused) {
|
|
if (direction == View.FOCUS_LEFT) {
|
|
// If there is nothing to the left, or this is causing us to
|
|
// jump to the right, then what we really want to do is page left.
|
|
final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
|
|
final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
|
|
if (currentFocused != null && nextLeft >= currLeft) {
|
|
handled = pageLeft();
|
|
} else {
|
|
handled = nextFocused.requestFocus();
|
|
}
|
|
} else if (direction == View.FOCUS_RIGHT) {
|
|
// If there is nothing to the right, or this is causing us to
|
|
// jump to the left, then what we really want to do is page right.
|
|
final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
|
|
final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
|
|
if (currentFocused != null && nextLeft <= currLeft) {
|
|
handled = pageRight();
|
|
} else {
|
|
handled = nextFocused.requestFocus();
|
|
}
|
|
}
|
|
} else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) {
|
|
// Trying to move left and nothing there; try to page.
|
|
handled = pageLeft();
|
|
} else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) {
|
|
// Trying to move right and nothing there; try to page.
|
|
handled = pageRight();
|
|
}
|
|
if (handled) {
|
|
playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
|
|
}
|
|
return handled;
|
|
}
|
|
|
|
private Rect getChildRectInPagerCoordinates(Rect outRect, View child) {
|
|
if (outRect == null) {
|
|
outRect = new Rect();
|
|
}
|
|
if (child == null) {
|
|
outRect.set(0, 0, 0, 0);
|
|
return outRect;
|
|
}
|
|
outRect.left = child.getLeft();
|
|
outRect.right = child.getRight();
|
|
outRect.top = child.getTop();
|
|
outRect.bottom = child.getBottom();
|
|
|
|
ViewParent parent = child.getParent();
|
|
while (parent instanceof ViewGroup && parent != this) {
|
|
final ViewGroup group = (ViewGroup) parent;
|
|
outRect.left += group.getLeft();
|
|
outRect.right += group.getRight();
|
|
outRect.top += group.getTop();
|
|
outRect.bottom += group.getBottom();
|
|
|
|
parent = group.getParent();
|
|
}
|
|
return outRect;
|
|
}
|
|
|
|
boolean pageLeft() {
|
|
return setCurrentItemInternal(mCurItem + mLeftIncr, true, false);
|
|
}
|
|
|
|
boolean pageRight() {
|
|
return setCurrentItemInternal(mCurItem - mLeftIncr, true, false);
|
|
}
|
|
|
|
@Override
|
|
public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
|
|
super.onRtlPropertiesChanged(layoutDirection);
|
|
|
|
if (layoutDirection == LAYOUT_DIRECTION_LTR) {
|
|
mLeftIncr = -1;
|
|
} else {
|
|
mLeftIncr = 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* We only want the current page that is being shown to be focusable.
|
|
*/
|
|
@Override
|
|
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
|
|
final int focusableCount = views.size();
|
|
|
|
final int descendantFocusability = getDescendantFocusability();
|
|
|
|
if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
|
|
for (int i = 0; i < getChildCount(); i++) {
|
|
final View child = getChildAt(i);
|
|
if (child.getVisibility() == VISIBLE) {
|
|
ItemInfo ii = infoForChild(child);
|
|
if (ii != null && ii.position == mCurItem) {
|
|
child.addFocusables(views, direction, focusableMode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// we add ourselves (if focusable) in all cases except for when we are
|
|
// FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is
|
|
// to avoid the focus search finding layouts when a more precise search
|
|
// among the focusable children would be more interesting.
|
|
if (
|
|
descendantFocusability != FOCUS_AFTER_DESCENDANTS ||
|
|
// No focusable descendants
|
|
(focusableCount == views.size())) {
|
|
// Note that we can't call the superclass here, because it will
|
|
// add all views in. So we need to do the same thing View does.
|
|
if (!isFocusable()) {
|
|
return;
|
|
}
|
|
if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE &&
|
|
isInTouchMode() && !isFocusableInTouchMode()) {
|
|
return;
|
|
}
|
|
if (views != null) {
|
|
views.add(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* We only want the current page that is being shown to be touchable.
|
|
*/
|
|
@Override
|
|
public void addTouchables(ArrayList<View> views) {
|
|
// Note that we don't call super.addTouchables(), which means that
|
|
// we don't call View.addTouchables(). This is okay because a ViewPager
|
|
// is itself not touchable.
|
|
for (int i = 0; i < getChildCount(); i++) {
|
|
final View child = getChildAt(i);
|
|
if (child.getVisibility() == VISIBLE) {
|
|
ItemInfo ii = infoForChild(child);
|
|
if (ii != null && ii.position == mCurItem) {
|
|
child.addTouchables(views);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* We only want the current page that is being shown to be focusable.
|
|
*/
|
|
@Override
|
|
protected boolean onRequestFocusInDescendants(int direction,
|
|
Rect previouslyFocusedRect) {
|
|
int index;
|
|
int increment;
|
|
int end;
|
|
int count = getChildCount();
|
|
if ((direction & FOCUS_FORWARD) != 0) {
|
|
index = 0;
|
|
increment = 1;
|
|
end = count;
|
|
} else {
|
|
index = count - 1;
|
|
increment = -1;
|
|
end = -1;
|
|
}
|
|
for (int i = index; i != end; i += increment) {
|
|
View child = getChildAt(i);
|
|
if (child.getVisibility() == VISIBLE) {
|
|
ItemInfo ii = infoForChild(child);
|
|
if (ii != null && ii.position == mCurItem) {
|
|
if (child.requestFocus(direction, previouslyFocusedRect)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
|
|
return new LayoutParams();
|
|
}
|
|
|
|
@Override
|
|
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
|
|
return generateDefaultLayoutParams();
|
|
}
|
|
|
|
@Override
|
|
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
|
|
return p instanceof LayoutParams && super.checkLayoutParams(p);
|
|
}
|
|
|
|
@Override
|
|
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
|
|
return new LayoutParams(getContext(), attrs);
|
|
}
|
|
|
|
|
|
@Override
|
|
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
|
|
super.onInitializeAccessibilityEvent(event);
|
|
|
|
event.setClassName(ViewPager.class.getName());
|
|
event.setScrollable(canScroll());
|
|
|
|
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED && mAdapter != null) {
|
|
event.setItemCount(mAdapter.getCount());
|
|
event.setFromIndex(mCurItem);
|
|
event.setToIndex(mCurItem);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
|
|
super.onInitializeAccessibilityNodeInfo(info);
|
|
|
|
info.setClassName(ViewPager.class.getName());
|
|
info.setScrollable(canScroll());
|
|
|
|
if (canScrollHorizontally(1)) {
|
|
info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD);
|
|
info.addAction(AccessibilityAction.ACTION_SCROLL_RIGHT);
|
|
}
|
|
|
|
if (canScrollHorizontally(-1)) {
|
|
info.addAction(AccessibilityAction.ACTION_SCROLL_BACKWARD);
|
|
info.addAction(AccessibilityAction.ACTION_SCROLL_LEFT);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean performAccessibilityAction(int action, Bundle args) {
|
|
if (super.performAccessibilityAction(action, args)) {
|
|
return true;
|
|
}
|
|
|
|
switch (action) {
|
|
case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
|
|
case R.id.accessibilityActionScrollRight:
|
|
if (canScrollHorizontally(1)) {
|
|
setCurrentItem(mCurItem + 1);
|
|
return true;
|
|
}
|
|
return false;
|
|
case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
|
|
case R.id.accessibilityActionScrollLeft:
|
|
if (canScrollHorizontally(-1)) {
|
|
setCurrentItem(mCurItem - 1);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private boolean canScroll() {
|
|
return mAdapter != null && mAdapter.getCount() > 1;
|
|
}
|
|
|
|
private class PagerObserver extends DataSetObserver {
|
|
@Override
|
|
public void onChanged() {
|
|
dataSetChanged();
|
|
}
|
|
@Override
|
|
public void onInvalidated() {
|
|
dataSetChanged();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Layout parameters that should be supplied for views added to a
|
|
* ViewPager.
|
|
*/
|
|
public static class LayoutParams extends ViewGroup.LayoutParams {
|
|
/**
|
|
* true if this view is a decoration on the pager itself and not
|
|
* a view supplied by the adapter.
|
|
*/
|
|
public boolean isDecor;
|
|
|
|
/**
|
|
* Gravity setting for use on decor views only:
|
|
* Where to position the view page within the overall ViewPager
|
|
* container; constants are defined in {@link android.view.Gravity}.
|
|
*/
|
|
@InspectableProperty(
|
|
name = "layout_gravity",
|
|
valueType = InspectableProperty.ValueType.GRAVITY)
|
|
public int gravity;
|
|
|
|
/**
|
|
* Width as a 0-1 multiplier of the measured pager width
|
|
*/
|
|
float widthFactor = 0.f;
|
|
|
|
/**
|
|
* true if this view was added during layout and needs to be measured
|
|
* before being positioned.
|
|
*/
|
|
boolean needsMeasure;
|
|
|
|
/**
|
|
* Adapter position this view is for if !isDecor
|
|
*/
|
|
int position;
|
|
|
|
/**
|
|
* Current child index within the ViewPager that this view occupies
|
|
*/
|
|
int childIndex;
|
|
|
|
public LayoutParams() {
|
|
super(FILL_PARENT, FILL_PARENT);
|
|
}
|
|
|
|
public LayoutParams(Context context, AttributeSet attrs) {
|
|
super(context, attrs);
|
|
|
|
final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
|
|
gravity = a.getInteger(0, Gravity.TOP);
|
|
a.recycle();
|
|
}
|
|
}
|
|
|
|
static class ViewPositionComparator implements Comparator<View> {
|
|
@Override
|
|
public int compare(View lhs, View rhs) {
|
|
final LayoutParams llp = (LayoutParams) lhs.getLayoutParams();
|
|
final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams();
|
|
if (llp.isDecor != rlp.isDecor) {
|
|
return llp.isDecor ? 1 : -1;
|
|
}
|
|
return llp.position - rlp.position;
|
|
}
|
|
}
|
|
}
|