news 2026/5/7 14:12:48

VerticalViewPager基于 Android 原生 ViewPager 修改而来,专门用于支持垂直方向的页面切换

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VerticalViewPager基于 Android 原生 ViewPager 修改而来,专门用于支持垂直方向的页面切换

VerticalViewPager基于 Android 原生 ViewPager 修改而来,专门用于支持垂直方向的页面切换

VerticalViewPager.java 功能总结

这个组件是用户提供了流畅的垂直滑动浏览体验。

核心功能

VerticalViewPager
是一个自定义的垂直滚动 ViewPager 组件,基于 Android 原生 ViewPager 修改而来,专门用于支持垂直方向的页面切换。

主要特性

1. 垂直滚动支持

  • 将标准 ViewPager 的水平滚动改为垂直滚动
  • 支持上下滑动切换页面
  • 实现了完整的垂直触摸事件处理

2. 页面管理

  • 支持通过 PagerAdapter 管理页面内容
  • 实现页面的创建、销毁和复用机制
  • 支持离屏页面预加载(setOffscreenPageLimit

3. 触摸交互

  • 拖动检测和速度追踪
  • 滑动阈值设置(mTouchSlopmMinimumVelocity
  • 支持惯性滑动(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 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 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 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; /** * 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); ArrayList<ViewPager.OnPageChangeListener> listeners = (ArrayList<ViewPager.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 ArrayList<ViewPager.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/> * <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> *
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/11 16:37:04

EtherNet/IP 转 CC-Link IE 工业 PLC 网关稳定对接罗克韦尔与三菱系统

一、项目应用背景某新能源商用车制造基地&#xff0c;在总装车间智能化输送线项目中&#xff0c;产线采用 “中央主控 分区执行” 的分布式控制架构。 中央控制系统&#xff1a;采用罗克韦尔 ControlLogix L73 PLC&#xff0c;基于 EtherNet/IP 协议。作为整线 “大脑”&#…

作者头像 李华
网站建设 2026/4/11 12:35:25

AI Agent 跑完任务怎么通知你?我写了个微信推送服务隙

1、普通的insert into 如果&#xff08;主键/唯一建&#xff09;存在&#xff0c;则会报错 新需求&#xff1a;就算冲突也不报错&#xff0c;用其他处理逻辑 回到顶部 2、基本语法&#xff08;INSERT INTO ... ON CONFLICT (...) DO (UPDATE SET ...)/(NOTHING)&#xff09; 语…

作者头像 李华
网站建设 2026/4/12 3:36:48

【Zotero】Zotero翻译插件时好时坏?一文讲透原因与解决方案

学术人的痛&#xff1a;今天的翻译&#xff0c;明天的“请求错误” 作为一名重度文献阅读者&#xff0c;我每天与Zotero相伴的时间比跟室友说话还多。它的PDF Translate插件堪称学术神器——划词即译&#xff0c;让外语文献阅读效率翻倍。 但有一个问题一直困扰着我&#xff…

作者头像 李华
网站建设 2026/4/12 1:40:11

Pixel Dimension Fissioner 版本管理实战:Git协作开发工作流

Pixel Dimension Fissioner 版本管理实战&#xff1a;Git协作开发工作流 1. 为什么需要版本管理 在团队开发Pixel Dimension Fissioner这类AI项目时&#xff0c;代码、模型配置和Prompt模板的变更非常频繁。没有版本管理就像在走钢丝——一个不小心的修改可能导致整个项目崩溃…

作者头像 李华
网站建设 2026/4/12 5:19:24

WarcraftHelper:魔兽争霸III终极兼容性修复工具完全指南

WarcraftHelper&#xff1a;魔兽争霸III终极兼容性修复工具完全指南 【免费下载链接】WarcraftHelper Warcraft III Helper , support 1.20e, 1.24e, 1.26a, 1.27a, 1.27b 项目地址: https://gitcode.com/gh_mirrors/wa/WarcraftHelper 还在为魔兽争霸III这款经典游戏在…

作者头像 李华