开源项目ViewPagerIndicator源码分析
2016-08-09 23:50
465 查看
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()方法。
MeasureSpec.UNSPECIFIED/MeasureSpec.AT_MOST:手动计算尺寸,如果父视图限定了尺寸,再取两者中的较小值。
onDraw()方法核心代码如下。在该方法中,绘制圆点指示器。
重点在于onTouchEvent()方法。在该方法中,根据手指的触摸和平移,计算出偏移量,来拖动ViewPager。且支持多点触摸。其实如果我们的PageIndicator类仅仅只需要指示器功能的话,onTouchEvent()方法可以不用重写,比如广告栏中的圆点指示器。
剩下的LinePageIndicator、TitlePageIndicator和UnderlinePageIndicator不再具体分析,基本就是onDraw()方法的实现不同。
(2).再来看继承自HorizontalScrollView的两个类,TabPageIndicator和IconPageIndicator。因为HorizontalScrollView已经帮我们实现了很多代码,所以这两个类比上面的四个类简单很多。
以TabPageIndicator类为例,核心方法如下。
构造方法。
notifyDataSetChanged()方法。
setCurrentItem()方法。
如图所示。
项目地址:
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: // 计算移动距离,拖动ViewPager final 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()方法进行平移。
相关文章推荐
- Android 开源项目源码解析 -->ViewPagerindicator 源码解析(十)
- 开源项目源码解析-ViewPagerindicator 源码解析
- ViewPagerIndicator-master源码分析 2
- viewpager开源项目 ViewPagerIndicator
- 【Android开源项目分析】自定义圆形头像CircleImageView的使用和源码分析
- 开源项目GridPasswordView使用和源码分析
- 【Android开源项目分析】自定义圆形头像CircleImageView的使用和源码分析
- 开源项目GridPasswordView使用和源码分析
- Android从零开搞系列:自定义View(5)基本的自定义ViewPager指示器+开源项目分析(下)
- Eoe客户端源码分析---ViewPager、 PageAdapter和PageIndicator 的使用
- 【Android开源项目分析】自定义圆形头像CircleImageView的使用和源码分析
- android studio 项目引入viewpagerindicator开源控件
- 【Android开源项目分析】TAB导航栏PagerSlidingTabStrip的使用和源码分析
- 开源项目推荐(1):Android-ViewPagerIndicator 分页指示器,实现左右滑分页视图
- Android实战之 自定义GitHub开源项目ViewPagerIndicator
- ViewPagerIndicator-master源码分析 3
- 开源项目 无限循环ViewPager InfiniteViewPager 分析(二)
- 黑马北京新闻项目连载(4)--->ViewPagerIndicator结合FragmentPagerAdapter(开源库的Git上的demo)
- 开源项目ExpandableTextView使用和源码分析
- 关于ViewPagerIndicator开源项目的一些问题