开源项目ViewPagerIndicator源码分析

2023-10-18 06:48

本文主要是介绍开源项目ViewPagerIndicator源码分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

ViewPagerIndicator,配合ViewPager使用的指示器,可以是标签类型Tab指示器(如各种新闻app),也可以是小圆圈或小横线类型的指示器(如引导页),来自于github上大名鼎鼎的JakeWharton。


如图所示。


项目地址:
https://github.com/JakeWharton/ViewPagerIndicator
http://viewpagerindicator.com/

项目中定义的接口和类如下:
(1). PageIndicator接口,继承自ViewPager.OnPageChangeListener,定义指示器需要实现的方法。
(2). IcsLinearLayout类,继承自LinearLayout,支持Android 4.0+分割线特性。
(3). CirclePageIndicator、LinePageIndicator、TitlePageIndicator、UnderlinePageIndicator,具体的指示器类,继承自View,实现了PageIndicator接口。
(4). TabPageIndicator、IconPageIndicator,具体的指示器类,继承自HorizontalScrollView,实现了PageIndicator接口。

类的设计图:


(1).先来看继承自View的四个类,其核心都是重写onMeasure()、onDraw()、onTouchEvent()。主要区别在于onDraw()方法,根据需要绘制不同的形状,而onTouchEvent()方法几乎是一致的。

以CirclePageIndicator类为例,onMeasure()方法核心代码如下。

onMeasure()方法。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}private int measureWidth(int measureSpec) {int result;int specMode = MeasureSpec.getMode(measureSpec);int specSize = MeasureSpec.getSize(measureSpec);if ((specMode == MeasureSpec.EXACTLY) || (mViewPager == null)) {// 确定的宽度result = specSize;} else {// 计算宽度final int count = mViewPager.getAdapter().getCount();result = (int) (getPaddingLeft() + getPaddingRight() + (count * 2 * mRadius) + (count - 1) * mRadius + 1);// 如果父视图限定了宽度,则取两者中的较小值if (specMode == MeasureSpec.AT_MOST) {result = Math.min(result, specSize);}}return result;
}private int measureHeight(int measureSpec) {int result;int specMode = MeasureSpec.getMode(measureSpec);int specSize = MeasureSpec.getSize(measureSpec);if (specMode == MeasureSpec.EXACTLY) {// 确定的高度result = specSize;} else {// 计算高度result = (int) (2 * mRadius + getPaddingTop() + getPaddingBottom() + 1);// 如果父视图限定了高度,则取两者中的较小值if (specMode == MeasureSpec.AT_MOST) {result = Math.min(result, specSize);}}return result;
}
MeasureSpec.EXACTLY:直接取子View的确定大小。
MeasureSpec.UNSPECIFIED/MeasureSpec.AT_MOST:手动计算尺寸,如果父视图限定了尺寸,再取两者中的较小值。

onDraw()方法核心代码如下。在该方法中,绘制圆点指示器。

@Override
protected void onDraw(Canvas canvas) {super.onDraw(canvas);final int count = mViewPager.getAdapter().getCount();int longSize = getWidth();int longPaddingBefore = getPaddingLeft();int longPaddingAfter = getPaddingRight();int shortPaddingBefore = getPaddingTop();// threeRadius:两个相邻圆点的圆心之间的间距final float threeRadius = mRadius * 3;// shortOffset:圆点的垂直方向坐标final float shortOffset = shortPaddingBefore + mRadius;// longOffset:圆点的水平方向坐标float longOffset = longPaddingBefore + mRadius;if (mCentered) {longOffset += ((longSize - longPaddingBefore - longPaddingAfter) / 2.0f) - ((count * threeRadius) / 2.0f);}float dX;float dY;float pageFillRadius = mRadius;if (mPaintStroke.getStrokeWidth() > 0) {pageFillRadius -= mPaintStroke.getStrokeWidth() / 2.0f;}// 根据页面的数量,循环绘制出空心圆for (int iLoop = 0; iLoop < count; iLoop++) {// drawLong:当前绘制的圆点的x坐标float drawLong = longOffset + (iLoop * threeRadius);dX = drawLong;dY = shortOffset;if (pageFillRadius != mRadius) {canvas.drawCircle(dX, dY, mRadius, mPaintStroke);}}// 随着页面的滑动,绘制实心圆// mSnap==true时,实心圆点不跟随手势移动float cx = (mSnap ? mSnapPage : mCurrentPage) * threeRadius;if (!mSnap) {cx += mPageOffset * threeRadius;}dX = longOffset + cx;dY = shortOffset;canvas.drawCircle(dX, dY, mRadius, mPaintFill);
}

重点在于onTouchEvent()方法。在该方法中,根据手指的触摸和平移,计算出偏移量,来拖动ViewPager。且支持多点触摸。其实如果我们的PageIndicator类仅仅只需要指示器功能的话,onTouchEvent()方法可以不用重写,比如广告栏中的圆点指示器。

@Override
public boolean onTouchEvent(android.view.MotionEvent ev) {if (super.onTouchEvent(ev)) {return true;}if ((mViewPager == null) || (mViewPager.getAdapter().getCount() == 0)) {return false;}// 获得动作类型final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;switch (action) {case MotionEvent.ACTION_DOWN:// 按下时,记录首次触摸点的id和位置mActivePointerId = MotionEventCompat.getPointerId(ev, 0);mLastMotionX = ev.getX();break;case MotionEventCompat.ACTION_POINTER_DOWN:// 在已有触摸点的情况下,又出现了新的触摸点按下,获取新触摸点的id和位置final int index = MotionEventCompat.getActionIndex(ev);mActivePointerId = MotionEventCompat.getPointerId(ev, index);mLastMotionX = MotionEventCompat.getX(ev, index);break;case MotionEvent.ACTION_MOVE:// 计算移动距离,拖动ViewPagerfinal int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);final float x = MotionEventCompat.getX(ev, activePointerIndex);final float deltaX = x - mLastMotionX;if (!mIsDragging) {if (Math.abs(deltaX) > mTouchSlop) {mIsDragging = true;}}if (mIsDragging) {mLastMotionX = x;if (mViewPager.isFakeDragging() || mViewPager.beginFakeDrag()) {mViewPager.fakeDragBy(deltaX);}}break;case MotionEventCompat.ACTION_POINTER_UP:final int pointerIndex = MotionEventCompat.getActionIndex(ev);final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);if (pointerId == mActivePointerId) {final int newPointerIndex = pointerIndex == 0 ? 1 : 0;mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);}mLastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId));break;case MotionEvent.ACTION_CANCEL:case MotionEvent.ACTION_UP:if (!mIsDragging) {final int count = mViewPager.getAdapter().getCount();final int width = getWidth();final float halfWidth = width / 2f;final float sixthWidth = width / 6f;// ACTION_UP时,手指离开屏幕的点,小于指示器宽度的1/3,ViewPager滑动到上一页if ((mCurrentPage > 0) && (ev.getX() < halfWidth - sixthWidth)) {if (action != MotionEvent.ACTION_CANCEL) {mViewPager.setCurrentItem(mCurrentPage - 1);}return true;// ACTION_UP时,手指离开屏幕的点,大于指示器宽度的2/3,ViewPager滑动到下一页} else if ((mCurrentPage < count - 1) && (ev.getX() > halfWidth + sixthWidth)) {if (action != MotionEvent.ACTION_CANCEL) {mViewPager.setCurrentItem(mCurrentPage + 1);}return true;}}// 重置状态mIsDragging = false;mActivePointerId = INVALID_POINTER;if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag();break;}return true;
}


剩下的LinePageIndicator、TitlePageIndicator和UnderlinePageIndicator不再具体分析,基本就是onDraw()方法的实现不同。

(2).再来看继承自HorizontalScrollView的两个类,TabPageIndicator和IconPageIndicator。因为HorizontalScrollView已经帮我们实现了很多代码,所以这两个类比上面的四个类简单很多。

以TabPageIndicator类为例,核心方法如下。

构造方法。

public TabPageIndicator(Context context, AttributeSet attrs) {super(context, attrs);setHorizontalScrollBarEnabled(false);mTabLayout = new IcsLinearLayout(context, R.attr.vpiTabPageIndicatorStyle);addView(mTabLayout, new ViewGroup.LayoutParams(WRAP_CONTENT, MATCH_PARENT));
}
在构造方法中,创建一个IcsLinearLayout水平布局对象,调用addView()方法添加到当前视图,之后会将每一个tab(TextView或ImageView)添加到IcsLinearLayout水平布局中。

notifyDataSetChanged()方法。

public void notifyDataSetChanged() {mTabLayout.removeAllViews();PagerAdapter adapter = mViewPager.getAdapter();final int count = adapter.getCount();for (int i = 0; i < count; i++) {CharSequence title = adapter.getPageTitle(i);addTab(i, title);}setCurrentItem(mSelectedTabIndex);requestLayout();
}
private void addTab(int index, CharSequence text) {final TabView tabView = new TabView(getContext());tabView.mIndex = index;tabView.setFocusable(true);tabView.setOnClickListener(mTabClickListener);tabView.setText(text);mTabLayout.addView(tabView, new LinearLayout.LayoutParams(0, MATCH_PARENT, 1));
}
在notifyDataSetChanged()方法中,遍历Adapter中的标题,生成TabView并添加到IcsLinearLayout中。

setCurrentItem()方法。

@Override
public void setCurrentItem(int item) {mViewPager.setCurrentItem(item);final int tabCount = mTabLayout.getChildCount();for (int i = 0; i < tabCount; i++) {final View child = mTabLayout.getChildAt(i);final boolean isSelected = (i == item);child.setSelected(isSelected);if (isSelected) {animateToTab(item);}}
}
private void animateToTab(final int position) {final View tabView = mTabLayout.getChildAt(position);if (mTabSelector != null) {removeCallbacks(mTabSelector);}mTabSelector = new Runnable() {public void run() {final int scrollPos = tabView.getLeft() - (getWidth() - tabView.getWidth()) / 2;smoothScrollTo(scrollPos, 0);mTabSelector = null;}};post(mTabSelector);
}
在setCurrentItem()方法中,先选中ViewPager中的页面,然后将当前的Tab设置为选中状态,当TabPageIndicator的宽度超出屏幕宽度时,通过调用smoothScrollTo()方法进行平移。

这篇关于开源项目ViewPagerIndicator源码分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/230893

相关文章

Nginx分布式部署流程分析

《Nginx分布式部署流程分析》文章介绍Nginx在分布式部署中的反向代理和负载均衡作用,用于分发请求、减轻服务器压力及解决session共享问题,涵盖配置方法、策略及Java项目应用,并提及分布式事... 目录分布式部署NginxJava中的代理代理分为正向代理和反向代理正向代理反向代理Nginx应用场景

vite搭建vue3项目的搭建步骤

《vite搭建vue3项目的搭建步骤》本文主要介绍了vite搭建vue3项目的搭建步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学... 目录1.确保Nodejs环境2.使用vite-cli工具3.进入项目安装依赖1.确保Nodejs环境

Redis中的有序集合zset从使用到原理分析

《Redis中的有序集合zset从使用到原理分析》Redis有序集合(zset)是字符串与分值的有序映射,通过跳跃表和哈希表结合实现高效有序性管理,适用于排行榜、延迟队列等场景,其时间复杂度低,内存占... 目录开篇:排行榜背后的秘密一、zset的基本使用1.1 常用命令1.2 Java客户端示例二、zse

Redis中的AOF原理及分析

《Redis中的AOF原理及分析》Redis的AOF通过记录所有写操作命令实现持久化,支持always/everysec/no三种同步策略,重写机制优化文件体积,与RDB结合可平衡数据安全与恢复效率... 目录开篇:从日记本到AOF一、AOF的基本执行流程1. 命令执行与记录2. AOF重写机制二、AOF的

idea+spring boot创建项目的搭建全过程

《idea+springboot创建项目的搭建全过程》SpringBoot是Spring社区发布的一个开源项目,旨在帮助开发者快速并且更简单的构建项目,:本文主要介绍idea+springb... 目录一.idea四种搭建方式1.Javaidea命名规范2JavaWebTomcat的安装一.明确tomcat

pycharm跑python项目易出错的问题总结

《pycharm跑python项目易出错的问题总结》:本文主要介绍pycharm跑python项目易出错问题的相关资料,当你在PyCharm中运行Python程序时遇到报错,可以按照以下步骤进行排... 1. 一定不要在pycharm终端里面创建环境安装别人的项目子模块等,有可能出现的问题就是你不报错都安装

MyBatis Plus大数据量查询慢原因分析及解决

《MyBatisPlus大数据量查询慢原因分析及解决》大数据量查询慢常因全表扫描、分页不当、索引缺失、内存占用高及ORM开销,优化措施包括分页查询、流式读取、SQL优化、批处理、多数据源、结果集二次... 目录大数据量查询慢的常见原因优化方案高级方案配置调优监控与诊断总结大数据量查询慢的常见原因MyBAT

uni-app小程序项目中实现前端图片压缩实现方式(附详细代码)

《uni-app小程序项目中实现前端图片压缩实现方式(附详细代码)》在uni-app开发中,文件上传和图片处理是很常见的需求,但也经常会遇到各种问题,下面:本文主要介绍uni-app小程序项目中实... 目录方式一:使用<canvas>实现图片压缩(推荐,兼容性好)示例代码(小程序平台):方式二:使用uni

分析 Java Stream 的 peek使用实践与副作用处理方案

《分析JavaStream的peek使用实践与副作用处理方案》StreamAPI的peek操作是中间操作,用于观察元素但不终止流,其副作用风险包括线程安全、顺序混乱及性能问题,合理使用场景有限... 目录一、peek 操作的本质:有状态的中间操作二、副作用的定义与风险场景1. 并行流下的线程安全问题2. 顺

MyBatis/MyBatis-Plus同事务循环调用存储过程获取主键重复问题分析及解决

《MyBatis/MyBatis-Plus同事务循环调用存储过程获取主键重复问题分析及解决》MyBatis默认开启一级缓存,同一事务中循环调用查询方法时会重复使用缓存数据,导致获取的序列主键值均为1,... 目录问题原因解决办法如果是存储过程总结问题myBATis有如下代码获取序列作为主键IdMappe