Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 開源項目ViewPagerIndicator源碼分析

Android 開源項目ViewPagerIndicator源碼分析

編輯:關於Android編程

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()方法進行平移。

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved