VerticalViewPager基于 Android 原生 ViewPager 修改而来,专门用于支持垂直方向的页面切换
VerticalViewPager基于 Android 原生 ViewPager 修改而来,专门用于支持垂直方向的页面切换VerticalViewPager.java 功能总结这个组件是用户提供了流畅的垂直滑动浏览体验。核心功能VerticalViewPager是一个自定义的垂直滚动 ViewPager 组件,基于 Android 原生 ViewPager 修改而来,专门用于支持垂直方向的页面切换。主要特性1.垂直滚动支持将标准 ViewPager 的水平滚动改为垂直滚动支持上下滑动切换页面实现了完整的垂直触摸事件处理2.页面管理支持通过PagerAdapter管理页面内容实现页面的创建、销毁和复用机制支持离屏页面预加载(setOffscreenPageLimit)3.触摸交互拖动检测和速度追踪滑动阈值设置(mTouchSlop、mMinimumVelocity)支持惯性滑动(fling)和快速切换多点触控处理4.滚动动画自定义插值器实现平滑滚动效果支持平滑滚动和立即切换页面转换器支持(PageTransformer)硬件加速优化5.状态管理三种滚动状态:空闲(IDLE)、拖动(DRAGGING)、沉降(SETTLING)边缘效果(顶部和底部弹性效果)滚动状态监听和回调6.监听器支持页面变化监听器(OnPageChangeListener)支持多个监听器的复合管理内部和外部监听器分离7.性能优化视图缓存机制(USE_CACHE)绘制顺序优化硬件加速动态控制滚动缓存启用/禁用8.无障碍支持实现无障碍代理(AccessibilityDelegate)支持无障碍事件和节点信息键盘导航支持应用场景主要用于抖音风格的视频列表,实现:上下滑动切换视频流畅的页面切换体验视频播放和页面切换的协调控制类似抖音的垂直滑动交互体验技术实现继承关系:ViewGroup核心机制:自定义触摸事件处理 + 滚动器(Scroller)适配器模式:使用PagerAdapter提供页面内容观察者模式:通过DataSetObserver监听数据变化完整代码:/** * VerticalViewPager 自定义的垂直滚动 ViewPager 组件,基于 Android 原生 ViewPager 修改而来,专门用于支持垂直方向的页面切换。 */ public class VerticalViewPager extends ViewGroup { private static final String TAG = "ViewPager"; private static final boolean DEBUG = false; 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[]{ android.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; int position; boolean scrolling; float heightFactor; float offset; } private static final ComparatorItemInfo COMPARATOR = new ComparatorItemInfo() { @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 ArrayListItemInfo mItems = new ArrayListItemInfo(); 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 Scroller mScroller; private PagerObserver mObserver; private int mPageMargin; private Drawable mMarginDrawable; private int mLeftPageBounds; private int mRightPageBounds; // 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 boolean mIgnoreGutter; private int mDefaultGutterSize; private int mGutterSize; private 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 int mMinimumVelocity; private int mMaximumVelocity; private int mFlingDistance; private 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 boolean mFakeDragging; private long mFakeDragBeginTime; private EdgeEffectCompat mTopEdge; private EdgeEffectCompat mBottomEdge; private boolean mFirstLayout = true; private boolean mNeedCalculatePageOffsets = false; private boolean mCalledSuper; private int mDecorChildCount; // 跟踪是否为手动拖动 private boolean mIsManualScroll = false; // 跟踪滚动方向:1 = 向下滑动(翻到下一页);-1 = 向上滑动(翻到上一页);0 = 未知/初始状态 private int mScrollDirection = 0; private ViewPager.OnPageChangeListener mOnPageChangeListener; private ViewPager.OnPageChangeListener mInternalPageChangeListener; private OnAdapterChangeListener mAdapterChangeListener; private ViewPager.PageTransformer mPageTransformer; private Method mSetChildrenDrawingOrderEnabled; 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 ArrayListView 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; /** * 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 VerticalViewPager(Context context) { super(context); initViewPager(); } public VerticalViewPager(Context context, AttributeSet attrs) { super(context, attrs); initViewPager(); } /** * 初始化ViewPager的基本设置 * 包括滚动器、触摸参数、边缘效果等 */ void initViewPager() { // 设置为可绘制 setWillNotDraw(false); // 设置焦点获取方式为后代优先 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); // 设置为可获取焦点 setFocusable(true); final Context context = getContext(); // 初始化滚动器,使用自定义插值器 mScroller = new Scroller(context, sInterpolator); final ViewConfiguration configuration = ViewConfiguration.get(context); final float density = context.getResources().getDisplayMetrics().density; // 获取触摸阈值,用于判断是否开始拖动 mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); // 最小滑动速度 mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); // 最大滑动速度 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); // 顶部边缘效果 mTopEdge = new EdgeEffectCompat(context); // 底部边缘效果 mBottomEdge = new EdgeEffectCompat(context); // 最小滑动距离 mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); // 足够接近的阈值,用于判断是否完成滚动 mCloseEnough = (int) (CLOSE_ENOUGH * density); // 默认边缘区域大小 mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); // 设置无障碍代理 ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate()); // 设置无障碍重要性 if (ViewCompat.getImportantForAccessibility(this) == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); } } @Override protected void onDetachedFromWindow() { removeCallbacks(mEndScrollRunnable); super.onDetachedFromWindow(); } /** * 设置滚动状态 * * @param newState 新的滚动状态 * SCROLL_STATE_IDLE - 空闲状态 * SCROLL_STATE_DRAGGING - 拖动状态 * SCROLL_STATE_SETTLING - settle状态(正在滚动到最终位置) */ private void setScrollState(int newState) { // 如果状态没有变化,直接返回 if (mScrollState == newState) { return; } // 更新滚动状态 mScrollState = newState; // 如果设置了页面转换器,根据状态启用或禁用硬件加速 if (mPageTransformer != null) { // 当状态不是空闲时启用硬件加速,以提高页面转换性能 enableLayers(newState != SCROLL_STATE_IDLE); } // 通知监听器状态变化 if (mOnPageChangeListener != null) { mOnPageChangeListener.onPageScrollStateChanged(newState); // 调用新的 onPageScrollStateChanged 方法 if (mOnPageChangeListener instanceof SimpleOnPageChangeListener) { ((SimpleOnPageChangeListener) mOnPageChangeListener).onPageScrollStateChanged(newState, mIsManualScroll, mScrollDirection); } else if (mOnPageChangeListener instanceof CompositeOnPageChangeListener) { // 遍历复合监听器中的所有监听器,调用新方法 CompositeOnPageChangeListener compositeListener = (CompositeOnPageChangeListener) mOnPageChangeListener; try { // 使用反射获取 mListeners 列表 java.lang.reflect.Field listenersField = CompositeOnPageChangeListener.class.getDeclaredField("mListeners"); listenersField.setAccessible(true); ArrayListViewPager.OnPageChangeListener listeners = (ArrayListViewPager.OnPageChangeListener) listenersField.get(compositeListener); for (ViewPager.OnPageChangeListener listener : listeners) { if (listener instanceof SimpleOnPageChangeListener) { ((SimpleOnPageChangeListener) listener).onPageScrollStateChanged(newState, mIsManualScroll, mScrollDirection); } } } catch (Exception e) { // 反射失败时忽略 } } } } /** * 设置PagerAdapter,用于为ViewPager提供页面视图 * * @param adapter 要使用的适配器 */ 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 getClientWidth() { // return getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); // } private int getClientHeight() { return getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); } /** * 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); } public int getCurrentItem() { return mCurItem; } void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { setCurrentItemInternal(item, smoothScroll, always, 0); } void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { if (mAdapter == null || mAdapter.getCount() = 0) { setScrollingCacheEnabled(false); return; } if (!always mCurItem == item mItems.size() != 0) { setScrollingCacheEnabled(false); return; } if (item 0) { item = 0; } else if (item = mAdapter.getCount()) { item = mAdapter.getCount() - 1; } 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); } } private void scrollToItem(int item, boolean smoothScroll, int velocity, boolean dispatchSelected) { final ItemInfo curInfo = infoForPosition(item); int destY = 0; if (curInfo != null) { final int height = getClientHeight(); destY = (int) (height * Math.max(mFirstOffset, Math.min(curInfo.offset, mLastOffset))); } if (smoothScroll) { smoothScrollTo(0, destY, velocity); if (dispatchSelected mOnPageChangeListener != null) { mOnPageChangeListener.onPageSelected(item); } if (dispatchSelected mInternalPageChangeListener != null) { mInternalPageChangeListener.onPageSelected(item); } } else { if (dispatchSelected mOnPageChangeListener != null) { mOnPageChangeListener.onPageSelected(item); } if (dispatchSelected mInternalPageChangeListener != null) { mInternalPageChangeListener.onPageSelected(item); } completeScroll(false); scrollTo(0, destY); pageScrolled(destY); } } /** * 设置页面变化监听器,当页面改变或滚动时会被调用 * 参见 {@link ViewPager.OnPageChangeListener} * * @param listener 要设置的监听器 */ public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { mOnPageChangeListener = listener; } /** * 添加页面变化监听器 * 与setOnPageChangeListener不同,此方法允许添加多个监听器 * * @param listener 要添加的监听器 */ public void addOnPageChangeListener(ViewPager.OnPageChangeListener listener) { if (mOnPageChangeListener == null) { mOnPageChangeListener = listener; } else if (mOnPageChangeListener instanceof CompositeOnPageChangeListener) { ((CompositeOnPageChangeListener) mOnPageChangeListener).addListener(listener); } else { CompositeOnPageChangeListener compositeListener = new CompositeOnPageChangeListener(); compositeListener.addListener(mOnPageChangeListener); compositeListener.addListener(listener); mOnPageChangeListener = compositeListener; } } /** * 移除页面变化监听器 * * @param listener 要移除的监听器 */ public void removeOnPageChangeListener(ViewPager.OnPageChangeListener listener) { if (mOnPageChangeListener == listener) { mOnPageChangeListener = null; } else if (mOnPageChangeListener instanceof CompositeOnPageChangeListener) { ((CompositeOnPageChangeListener) mOnPageChangeListener).removeListener(listener); } } /** * 复合页面变化监听器,用于管理多个监听器 */ private static class CompositeOnPageChangeListener implements ViewPager.OnPageChangeListener { private final ArrayListViewPager.OnPageChangeListener mListeners = new ArrayList(); /** * 添加监听器 * * @param listener 要添加的监听器 */ public void addListener(ViewPager.OnPageChangeListener listener) { if (listener != null) { mListeners.add(listener); } } /** * 移除监听器 * * @param listener 要移除的监听器 */ public void removeListener(ViewPager.OnPageChangeListener listener) { if (listener != null) { mListeners.remove(listener); } } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { for (ViewPager.OnPageChangeListener listener : mListeners) { listener.onPageScrolled(position, positionOffset, positionOffsetPixels); } } @Override public void onPageSelected(int position) { for (ViewPager.OnPageChangeListener listener : mListeners) { listener.onPageSelected(position); } } @Override public void onPageScrollStateChanged(int state) { for (ViewPager.OnPageChangeListener listener : mListeners) { listener.onPageScrollStateChanged(state); } } } /** * Set a {@link ViewPager.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/ * pemNote:/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 *