• 热门专题

Android开源项目ViewPagerIndicator源码分析

作者:  发布日期:2016-08-10 21:56:12
Tag标签:源码  项目  
  • 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()方法进行平移。
About IT165 - 广告服务 - 隐私声明 - 版权申明 - 免责条款 - 网站地图 - 网友投稿 - 联系方式
本站内容来自于互联网,仅供用于网络技术学习,学习中请遵循相关法律法规