標籤:
轉自:http://www.codekk.com/open-source-project-analysis/detail/Android/lightSky/ViewPagerindicator%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90
ViewPagerindicator 源碼解析
本文為 Android 開源項目源碼解析 中 ViewPagerindicator 部分
項目地址:ViewPagerIndicator,分析的版本:8cd549f,Demo 地址:ViewPagerIndicator Demo
分析者:lightSky,校對者:aaronplay,校對狀態:完成
1. 功能介紹1.1 ViewPagerIndicator
ViewPagerIndicator用於各種基於AndroidSupportLibrary中ViewPager的介面導航。主要特點:使用簡單、樣式全、易擴充。
2. 總體設計
該項目總體設計非常簡單,一個pageIndicator介面類,具體樣式的導航類實現該介面,然後根據具體樣式去實現相應的邏輯。IcsLinearLayout:LinearLayout的擴充,支援了4.0以上的divider特性。CirclePageIndicator、LinePageIndicator、UnderlinePageIndicator、TitlePagerIndicator繼承自View。TabPageIndicator、IconPageIndicator 繼承自HorizontalScrollView。
CirclePageIndicator、LinePageIndicator、UnderlinePageIndicator繼承自View的原因是它們樣式相對簡單,繼承View,自己去定製一套測量和繪製邏輯更簡單,而且免去了Measure部分繁瑣的步驟,效率更高。
TitlePagerIndicator相對複雜,Android系統提供的控制項中沒有類似的,而且實現底部線條的精準控制也複雜,所以只能繼承自View,實現繪製邏輯,達到理想的UI效果。
TabPageIndicator、IconPageIndicator繼承自HorizontalScrollView是由於它們各自的ChildView較多,而且具有相似性些,繼承自LinearLayout,通過for迴圈一個個add上去更簡單,而且HorizontalScrollView具有水平滑動的功能,當tab比較多的時候,可以左右滑動。
3. 詳細設計3.1類別關係圖
3.2 自訂控制項相關知識
由於ViewPagerIndicator項目全部都是自訂View,因此對於其原理的分析,就是對自訂View的分析,自訂View涉及到的核心部分有:View的繪製機制和Touch事件傳遞機制。對於View的繪製機制,這裡做了詳細的闡述,這一部分在Android中為公用知識點,已經將這一部分的內容單獨去了,在tech目錄下,而對於Touch事件,由於該項目只是Indicator,因此沒有涉及到複雜的Touch傳遞機制,該項目中與Touch機制相關只有onTouch(Event)方法,因此只對該方法涉及到的相關知識進行介紹。
3.2.1 自訂控制項步驟
-
建立自訂的View
- 繼承View或View的子類,添加必要的建構函式
- 定義自訂屬性(外觀與行為)
- 應用自訂屬性:在布局中指定屬性值,在初始化時擷取並應用到View上
- 添加控制屬性的方法
-
自訂View的繪製
重寫onDraw()方法,按需求繪製自訂的view。每次螢幕發生繪製以及動畫執行過程中,onDraw方法都會被調用到,避免在onDraw方法裡面執行複雜的操作,避免建立對象,比如繪製過程中使用的Paint,應該在初始化的時候就建立,而不是在onDraw方法中。
-
使View具有互動性
一個好的自訂View還應該具有互動性,使使用者可以感受到UI上的微小變化,並且這些變化應該儘可能的和現實世界的物理規律保持一致,更自然。Android提供一個輸入事件模型,協助你處理使用者的輸入事件,你可以藉助GestureDetector、Scroller、屬性動畫等使得過渡更加自然和流暢。
3.2.2 Android的拖拽事件
該項目使用的是ViewPager,本來不用處理拖拽事件的,但項目中的CirclePageIndicator、LinePageIndicator、UnderlinePageIndicator、TitlePageIndicator都對onTouchEvent進行了處理,開始不明白,後來看到該項目的Issue,有問到該問題的:CirclePageIndicator consuming TouchEvents 原來是模仿了IOS中springboard的Indicator,使得點擊Indicator的左1/3和右邊1/3都可以切換Page,如果你使用該項目同時又處理了Touch事件,有可能它們會出現衝突問題,下面是涉及到的拖拽事件相關的知識點:
3.2.2.1 區分原始點及之後的任意觸摸點
ACTION_POINTER_DOWN、ACTION_POINTER_UP:多觸摸手勢事件中的按下和抬起事件。每當第二個觸控點按下或拿起時,會觸發該事件。在onTouchEvent()中可以捕捉並處理。
3.2.2.2 確保操作中點的ID(the active pointer ID)有效
當ACTION_POINTER_UP事件發生時,樣本程式會移除對該點的索引值的引用,確保操作中的點的ID(the active pointer ID)不會引用已經不在觸控螢幕上的觸摸點。這種情況下,app會選擇另一個觸摸點來作為操作中(active)的點,並儲存它當前的x、y。可以在觸控點MOVE時,始終能拿到有效Pointer正確的計算移動的距離。
3.2.2.3 mTouchSlop
指在使用者觸摸事件可被識別為移動手勢前,移動過的那一段像素距離。Touchslop通常用來預防使用者在做一些其他動作時意外地滑動,例如觸控螢幕幕上的元素時。
在本項目中,對於onTouche的處理是模板方法,因為沒有複雜的互動,僅僅是追蹤有效手勢以及確定Page的切換時機。官方文檔中在拖拽與縮放中有詳細的講解Dragging and Scaling 本項目中的onTouchEvent中的代碼就是官方文檔的模板代碼,就是為了確保擷取到可用、可信的點,然後對ViewPager相應處理。
3.2.3 View繪製機制
請直接參考公用技術點viewdrawflow部分
3.3 核心類及功能介紹3.3.1 CirclePageIndicator
繼承自 View 實現了 PageIndicator,整個繪製過程中用到的方法調用規則為:
(1) 主要成員變數含義
1.mCurrentPage 當前介面的索引
2.mSnapPage Sanp模式下,當前介面的索引
3.mPageOffset ViewPager的水平位移量
4.mScrollState ViewPager的滑動狀態
5.mOrientation Indicator的模式:水平、豎直
6.mLastMotionX 每一次onTouch事件產生時水平位置的最後位移量
7.mActivePointerId 當前處於活動中pointer的ID預設值為 -1
8.mIsDragging 使用者是否主觀的滑動螢幕的標識
9.mSnap
circle有2種繪製模式:
mSnap = true:ViewPager滑動過程中,circle之間不繪製,只繪製最終的實心點
mSnap = false:ViewPager滑動過程中,相鄰circle之間根據mPageOffset即時繪製circle
10.mTouchSlop
指在使用者觸摸事件可被識別為移動手勢前,移動過的那一段像素距離。
Touchslop通常用來預防使用者在做一些其他動作時意外地滑動,例如觸控螢幕幕上的元素時產生的滑動。
(2) 核心方法
1.onDraw(Canvas canvas)
threeRadius兩相鄰circle的間距
shortOffset當前方向的垂直方向的圓心座標位置
longOffset 當前方向的圓心位置
//迴圈的 draw circle for (int iLoop = 0; iLoop < count; iLoop++) { float drawLong = longOffset + (iLoop * threeRadius);//計算當前方向的每個circle位移量 canvas.drawCircle(dX, dY, pageFillRadius, mPaintPageFill);//繪製空心的circle canvas.drawCircle(dX, dY, mRadius, mPaintStroke);//繪製stroke ... 計算實心的circle的座標 ... canvas.drawCircle(dX, dY, mRadius, mPaintFill);//繪製當前page的circle }
2.onTouchEvent(MotionEvent ev)
核心思想:擷取拖拽過程中有效觸摸點,正確計算移動距離。這一部分為模板代碼,在其它幾種Indicator的實現中,對於Touch的事件的處理是相同的,
MotionEvent.ACTION_DOWN:記錄第一觸摸點的ID,擷取當前水平移動距離
MotionEvent.ACTION_MOVE: 擷取第一點的索引並計算其位移,處理使用者是否是主觀的滑動螢幕
MotionEvent.ACTION_CANCEL:如果使用者不是主動滑動,則以Indicator的1/3和2/3為臨界點進行previous和next
page的處理,處理完成,還原mIsDragging,mActivePointerId、viewpager的fakeDragging狀態
MotionEvent.ACTION_UP:同上
MotionEventCompat.ACTION_POINTER_DOWN:當除最初點外的第一個外出現在螢幕上時,觸發該事件,這時記錄新的mLastMotionX,mActivePointerId
MotionEventCompat.ACTION_POINTER_UP:當非第一點離開螢幕時,擷取抬起手指的ID,如果之前跟蹤的mActivePointerId是當前抬起的手指ID,那麼就重新為mActivePointerId 賦值另一個活動中的pointerId,最後再次擷取仍活動在螢幕上pointer的X座標值
3.onMeasure(int widthMeasureSpec, int heightMeasureSpec)
View在測量階段的最終大小的設定是由setMeasuredDimension()方法決定的,也是必須要調用的方法,否則會報異常,這裡就直接調用了setMeasuredDimension()方法設定值了。根據CircleIndicator的方向,計算相應的width、height
if (mOrientation == HORIZONTAL) { setMeasuredDimension(measureLong(widthMeasureSpec), measureShort(heightMeasureSpec)); } else { setMeasuredDimension(measureShort(widthMeasureSpec), measureLong(heightMeasureSpec)); } }
4.measureLong
與之對應的有measureShort,只是處理的方向不同
如果該View的測量要求為EXACTLY,則能直接確定子View的大小,該大小就是MeasureSpec.getSize(measureSpec)的值
如果該View的測量要求為UNSPECIFIED或AT_MOST模式,則根據實際需求計算寬度
if ((specMode == MeasureSpec.EXACTLY) || (mViewPager == null)){ //We were told how big to be result = specSize; } else { final int count = mViewPager.getAdapter().getCount(); result = (int)(getPaddingLeft() + getPaddingRight() + (count * 2 * mRadius) + (count - 1) * mRadius + 1); //如果父視圖的測量要求為AT_MOST,即限定了一個最大值,則再從系統建議值和自己計算值中取一個較小值 if (specMode == MeasureSpec.AT_MOST) { result = Math.min(result, specSize); } } return result;
3.3.2 IconPageIndicator、TabPageIndicator
都是繼承自HorizontalScrollView,而且實現邏輯很相似,所以這裡只對IconPageIndicator分析
(1) 主要成員變數含義
1.mIconsLayout 管理Icon的父視圖。在初始化的時候,直接通過addView()加入到根節點。
2.mIconSelector IconPageIndicator Post的Runnable對象,該對象會執行滑動到指定的Icon位置的邏輯。
(2) 核心方法
1.notifyDataSetChanged 在setViewPager中調用,用於建立IconPageIndicator,內部通過一個for迴圈不斷new ImageView,然後add到mIconsLayout上去,緊接著請求布局。
2.animateToIcon滑動到指定的Icon,及時回收上一次的mIconSelector,同時post一個新的mIconSelector。
3.onAttachedToWindow 該方法在View attach 到Window的時候被調用,此時View擁有了一片可繪製地區,此時可以做一些初始化的操作,這裡初始化了之前選定的Icon。
4.onDetachedFromWindow該方法調用後,View不再擁有可繪製的地區。此時可以對View進行一些清理操作。這裡將mIconSelector從訊息佇列中移除。
3.3.3 TitlePageIndicator
由於效果的即時性和複雜性,整個Indicator全部都是繪製出來的,主要邏輯都在onDraw中。
(1) 主要成員變數含義
1.mPaintText繪製Text的Paint
2.mPaintFooterLine繪製底線的Paint
3.mPaintFooterIndicator 當前title底部指標的Paint
(2) 核心方法
1.calculateAllBounds 根據當前選中的Title計算所有Title的邊界值
2.clipViewOnTheLeft為左邊的TextView設定邊界,當TextView向左移至Indicator的邊界時,則設定該TextViwe的left座標值始終為Indicator的left座標值,從而呈現停留的效果。
3.clipViewOnTheRight同上,反向相反
整個繪製流程:
3.3.4 LinePageIndicator、UnderLineIndicator
類似CirclePageIndicator,可以參考CirclePageIndicator的分析。
3.4 建立自訂View的步驟分析
這裡以CirclePageIndicator為例
3.4.1 繼承自View,實現建構函式
CirclePageIndicator extends View implements PageIndicator { public CirclePageIndicator(Context context) { this(context, null); } public CirclePageIndicator(Context context, AttributeSet attrs) { this(context, attrs, R.attr.vpiCirclePageIndicatorStyle); } public CirclePageIndicator(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); ... }}
3.4.2 定義屬性
vpi_attrs.xml
<declare-styleable name="CirclePageIndicator"> <!-- Color of the filled circle that represents the current page. --> <attr name="fillColor" format="color" /> <!-- Color of the filled circles that represents pages. --> <attr name="pageColor" format="color" /> ... </declare-styleable>
3.4.3 應用屬性
在布局中應用
<!--首先指定命名空間,屬性才可以使用--><LinearLayout xmlns:app="http://schemas.android.com/apk/res-auto"><!--應用屬性值--> <com.viewpagerindicator.CirclePageIndicator ... app:fillColor="#FF888888" app:pageColor="#88FF0000" /></LinearLayout>
在代碼中載入布局中的屬性值並應用:
public CirclePageIndicator(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); //載入預設值 final Resources res = getResources(); final int defaultPageColor = res.getColor(R.color.default_circle_indicator_page_color); //擷取並應用屬性值 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CirclePageIndicator, defStyle, 0); //應用屬性值 mPaintPageFill.setColor(a.getColor(R.styleable.CirclePageIndicator_pageColor, defaultPageColor)); ... a.recycle();//記得及時釋放資源 }
3.4.4 自訂View的繪製
請參考上面的CirclePageIndicator的onDraw,也可以參考tech下的View的繪製流程的Draw部分。
3.4.5 使View可互動
請參考上面的CirclePageIndicator的onTouch ,這裡只是簡單的處理了onTouch事件,互動更好的自訂控制項往往會加一些自然的動畫等。
4. 雜談
大多數的App中的導航都類似,ViewPagerIndicator能夠滿足你開發的基本需求,如果不能滿足,你可以在源碼的基礎上進行一些簡單 的改造。其中有一點是很多朋友提出的就是LineIndicator沒有實現TextView顏色狀態的聯動。這個有已經實現的開源庫:PagerSlidingTabStrip,你可以作為參考。
對於什麼時候需要自訂控制項以及如何更好的進行自訂控制項的定製,你可以參考這篇文章深入解析Android的自訂布局 相信會有一些啟發。
整片文章看下來,確實比較多,也是花了一部分時間寫的,其實之前是自己整理了一些相關知識,這次一下全部跟大家分享了。整篇文章都在講View的繪製機制,三個過程也都很詳細的通過源碼分析介紹了。如果你對View的繪製機制還不清楚,而且希望將來往更進階的方向發展,這一步一定會經曆的,那麼請你耐心看完,你可以分多次研讀,過程中出現問題或者原文分析不到位的地方,歡迎PR。
當你掌握了這些基本的知識,你可以去研究GitHub上的一部分開源項目了(因為Touch事件這裡介紹的不多,而很多項目和Touch事件相關)。
參考文獻
http://developer.android.com/training/gestures/index.html
http://developer.android.com/training/custom-views/create-view.html
Google Android官方培訓課程中文版
View的繪製:
http://blog.csdn.net/wangjinyu501/article/details/9008271
http://blog.csdn.net/qinjuning/article/details/7110211
http://blog.csdn.net/qinjuning/article/details/8074262
相關資源
Google I/O 2013 - Writing Custom Views for Android
best-practices-for-android-user-interface
深入解析Android的自訂布局
慕課網自訂FlowLayout課程
Touch事件傳遞
http://blog.csdn.net/xiaanming/article/details/21696315
http://blog.csdn.net/wangjinyu501/article/details/22584465
[轉]ViewPagerindicator 源碼解析