开源项目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:
	    // 计算移动距离,拖动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()方法进行平移。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值