標籤:
一、前言
好久沒有更新過部落格了,趁今天有空分享一個導覽列的自訂控制項。有關此控制項的demo相信在網上已經爛大街了,一搜一大把。
我現在只著重分享一些我認為比較難理解的知識點。整個控制項的痛點大概有三個
1、遊標的繪製。
2、ViewPager監聽器的理解。
3、遊標的移動。
本文將注重這三個方面重點分析。
先上Demo的最終效果
二、Demo結構圖和知識點
範例Module,有四個java檔案和兩個xml檔案
總結一下此控制項的主要知識點
1、ViewGroup繪製流程。
2、ViewPager的用法。
3、OnPageChangeListener介面的用法。
4、scrollTo方法的使用。
需要完整代碼,請看底部連結,謝謝!(^_^)。下面我直接講核心代碼。
三、SlideTab瀏覽列控制項
(1)SlideTab繼承了HorizontalScrollView控制項之後,咋們需要重寫onDraw方法。接下來需要看個圖瞭解SlideTab控制項的內部組成
整個SlideTab控制項就是由這三個類型的控制群組成。假設SlideTab控制項已經初始化完成了。第一次由系統開始調用onDraw方法。
/** * @param canvas */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //擷取當前Tab的左右兩邊的橫座標值 View currentTabView = horizontalContainer.getChildAt(currentPosition); float currentTabLeftX = currentTabView.getLeft(); float currentTabRightX = currentTabView.getRight(); int childCount = horizontalContainer.getChildCount(); //ViewPager在滑動的過程中會重複調用onDraw方法。下面if語句的內容是用來計算遊標的起點座標和終點座標 if (currentPositionOffset > 0f && currentPosition < childCount - 1) { //擷取下一個Tab左右兩邊的橫座標值 View nextTab = horizontalContainer.getChildAt(currentPosition + 1); float nextTabLeftX = nextTab.getLeft(); float nextTabRightX = nextTab.getRight(); //計算起點 currentTabLeftX = (currentPositionOffset * nextTabLeftX + (1f - currentPositionOffset) * currentTabLeftX); //計算終點 currentTabRightX = (currentPositionOffset * nextTabRightX + (1f - currentPositionOffset) * currentTabRightX); } //繪製底線// drawUnderline(canvas, horizontalContainer); //繪製指標 drawIndicator(canvas, currentTabLeftX, currentTabRightX); }
currentPosition變數的初始值為0。
第8行代碼View currentTabView = horizontalContainer.getChildAt(currentPosition);擷取到LinearLayout容器裡面第一個View(實際是TextView)視圖。
第9,10行分別得到View視圖的左上方座標和右上方座標。
第11行擷取LinearLayout容器中TextView控制項的總數。
第13行的判斷語句。currentPositionOffset 這是一個記錄著當前頁面滑動過程中的位移量(如果不理解先放下,後面再講)初始值為0,很明顯,currentPositionOffset 不大於 0f,if語句不成立,略過(if語句裡面的內容稍後再分析)。繼續往下走。
來到底28行執行drawIndicator方法,開始繪製遊標。
(2)現在來解決此控制項的第一個痛點,遊標的繪製。
這個遊標實際上是一條線。要畫一條先就必須得確定兩點的座標值(初中知識,兩點才能確定一條直線嘛)。
/** * 繪製遊標 * * @param canvas SlideTab控制項的畫板 * @param currentLeftX 標題控制項的左座標 * @param currentRightX 標題控制項的右座標 */ private void drawIndicator(Canvas canvas, float currentLeftX, float currentRightX) { float indicatorMiddle = (indicatorPaint.getStrokeWidth() / 2); float indicatorY = getHeight() - indicatorMiddle; canvas.drawLine(currentLeftX, indicatorY, currentRightX, indicatorY, indicatorPaint); }
為了更好的理解這幾段代碼,還是畫個圖。黑色框是LinearLayout容器,綠色框是遊標。
綠色框高度的值就是indicatorPaint.getStrokeWidth()。接下的工作就是計算左右兩邊的橙色點。
紅色橫線表示currentLeftX變數。
藍色橫線表示currentRightX變數。
紅色豎線表示indicatorMiddle變數。
粉色豎線表示indicatorY變數。
由第9,10行的計算方法得到
左邊橙色座標點(currentLeftX,indicatorY)。
右邊橙色座標點(currentRightX,indicatorY)。
在第11行調用canvas.drawLine方法(這是一個繪製直線的方法)繪製遊標。這個方法的最後的參數indicatorPaint是一個畫筆(用來描述這個直線的狀態,例如顏色,寬度,直線末端是否圓角等等。)
以上的內容就是遊標繪製的流程。
(3)繪製好遊標之後如何讓瀏覽列控制項跟隨著ViewPager的滑動而滑動呢?現在我們需要寫一個setViewPager方法,將(Fragment+ViewPager)與SlideTab控制項關聯起來。這個方法是提供給使用者(使用你控制項的程式猿)調用。他們只需要傳來一個ViewPager的執行個體和一個標題名稱數組即可完成此控制項的調用。
/** * @param viewPager 使用者傳進來的ViewPager * @param titleString 標題名稱 */ public void setViewPager(ViewPager viewPager, String[] titleString) { this.viewPager = viewPager; addTab(titleString); //設定ViewPager監聽事件 viewPager.addOnPageChangeListener(new SlideTabPageViewListener()); } /** * 添加Tab * * @param titleString 標題數組 */ private void addTab(String[] titleString) { //清空所有控制項 horizontalContainer.removeAllViews(); for (int i = 0; i < viewPager.getAdapter().getCount(); i++) { //建立垂直容器,用來包裹住下面TextView tabVerticalContainer = new LinearLayout(context); tabVerticalContainer.setOrientation(LinearLayout.VERTICAL); tabVerticalContainer.setHorizontalGravity(Gravity.CENTER_HORIZONTAL); //設定點擊事件 tabVerticalContainer.setOnClickListener(new ViewPagerClickListener(i)); tabVerticalContainer.setVerticalGravity(Gravity.CENTER_VERTICAL); //將垂直LinearLayout容器放入水平LinearLayout容器中 horizontalContainer.addView(tabVerticalContainer, isExtendTab ? expandedTabLayoutParams : defaultTabLayoutParams); if (titleString != null) { //建立標題 TextView textViews = new TextView(context); textViews.setText(titleString[i]); textViews.setTextSize(14); textViews.setTextColor(Color.parseColor("#000000")); textViews.setSingleLine(true); tabVerticalContainer.addView(textViews, textViewLayoutParams); } } }
第8行是一個自訂方法。根據ViewPager的頁面總數,設定標題導覽列。邏輯比較簡單只是單純的堆代碼,咋們略過吧。
我們重點關注第10行代碼。ViewPager註冊了一個監聽事件的執行個體。此執行個體有三個回調方法用來監聽使用者對螢幕的滑動操作。具體詳情請往下看,(SlideTab控制項的遊標滑動與這個監聽事件有很大關係。)
(4)SlideTabPageViewListener是SlideTab控制項的內部類,實現了ViewPager.OnPageChangeListener介面,這個介面必須實現3個方法。
現在來解決第2個痛點。就是ViewPager的監聽事件。
public void onPageScrolled(int position, float positionOffset,int positionOffsetPixels)
當你滑動頁面的時候會調用此方法,在滑動停止之前,此方法回一直被調用。
position表示當前頁面的下標。例如你有三個選項卡,現在從第一頁滑動到第二頁的過程中,這個position的下標是0(下標從0開始),滑動到第二頁position的時候下標就變成1了。
positionOffset表示當前頁面位移的百分比。這個參數我們待會就會用到。
positionOffsetPixels表示當前頁面位移的像素,一般情況不用。
public void onPageScrollStateChanged(int state)
當ViewPager頁面的狀態被改變的時候會調用此方法。怎麼理解這句話呢?
1、假如你觸控螢幕幕從第一頁滑動到第二頁這個過程中,會回調此方法(如果你手指一直處於滑動狀態此方法就會一直被調用),傳過來的state的值是1表示正在滑動。
2、滑動結束之後,會再次回調此方法,傳過來的state的值是2表示滑動結束了。
3、結束滑動之後如果沒有其他的滑動操作,會再次回調此方法,傳過來的state的值是0,表示ViewPager處理閑置狀態。
public void onPageSelected(int position)
此方法是從當前頁面滑動到另一個頁面才會調用,並且這個position是新頁面的下標。注意如果你從當前頁往下一頁滑動的過程中(不鬆手)又滑回原頁面,此方法不會調用。
以上關於ViewPager的OnPageChangeListener介面的方法詳情,有了這些基礎之後就比較好解釋SlideTabPageViewListener內部類。
(5)SlideTabPageViewListener內部類
/** * ViewPager滾動監聽事件 */ public class SlideTabPageViewListener implements ViewPager.OnPageChangeListener { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { currentPosition = position; currentPositionOffset = positionOffset; scrollToCurrentPosition(position, (int) (positionOffset * (horizontalContainer) .getChildAt(position).getWidth())); //重新繪製onDraw方法 invalidate(); } @Override public void onPageScrollStateChanged(int state) { } @Override public void onPageSelected(int position) { } }
最後,咋們來解決此控制項的最後一個難題,如何控制遊標的移動。這是我感覺最難講清楚的一部分。
假設咋們正在觸控螢幕幕從第一頁滑動到第二頁,在這個過程中。以上的onPageScrolled方法會一直被調用。
第8,9行的代碼是更新當前最新的頁面下標(currentPosition )和頁面的位移值(currentPositionOffset )。這兩個變數咋們已經在上面的onDraw方法中見過。
第10行調用scrollToCurrentPosition方法(這個方法很重要)。將當前的頁面下標(值為0)和當前View(TextView)的位移值傳遞過去。具體代碼如下。
/***** * * * @param position * @param offset */ public void scrollToCurrentPosition(int position, int offset) { int currentOffsetX = horizontalContainer.getChildAt(position).getLeft() + offset; int startScrollX = currentOffsetX; if (position > 0 || offset > 0) { //remainOffset表示剩餘位移量 startScrollX = currentOffsetX - remainOffset; } //如果位移發生變化,則滑動 if (startScrollX != lastScrollX) { //更新最後一次滑動的距離 lastScrollX = startScrollX; //horizontalContainer控制項開始滑動 scrollTo(startScrollX, 0); } }
第11行的語句成立。計算得到startScrollX這是horizontalContainer 容器實際的滑動位移值。
第17行lastScrollX預設初始值為0,因此if語句也成立。
最終在21行開始滑動horizontalContainer容器。(需要注意的是如果startScrollX的值大於0則往左滑動,小於0往右滑動。)
緊接著調用invalidate()方法,其內部代碼又會回調咋們剛才所說的onDraw方法。在onDraw方法中,執行了前面的代碼後來到了剛才沒有講解的if語句,由於此時處於滑動狀態。currentPositionOffset和currentPosition的值肯定是成立的。
(6)我再貼一下onDraw方法中if語句的代碼。
if (currentPositionOffset > 0f && currentPosition < childCount - 1) { //擷取下一個Tab左右兩邊的橫座標值 View nextTab = horizontalContainer.getChildAt(currentPosition + 1); float nextTabLeftX = nextTab.getLeft(); float nextTabRightX = nextTab.getRight(); //計算起點 currentTabLeftX = (currentPositionOffset * nextTabLeftX + (1f - currentPositionOffset) * currentTabLeftX); //計算終點 currentTabRightX = (currentPositionOffset * nextTabRightX + (1f - currentPositionOffset) * currentTabRightX); }
在第3行,由於滑動沒有結束,此時currentPosition 的值還是0,又因為currentPosition + 1,所以nextTab得到的是第二個TextView的值。
第4,5行擷取nextTab控制項(實際就是TextView)左右兩邊的座標。
第7,10行計算滑動過程中遊標的起點和終點,也就是中左右兩邊的紅點的座標。
計算完畢之後。最後就是再次調用drawIndicator方法重新繪製遊標。
至此有關SlideTab控制項與ViewPager滑動的流程就走完了。謝謝(^_^)Y
四、結束
鑒於篇幅的關係,我就不再示範控制項的使用了。各位可以下載下面的demo看看源碼。demo中的SlideTabDemonstration類是入口。
此demo有BUG在所難免。僅限於學習。希望能幫到各位。
Demo源碼請戳這裡(Android Studio編譯器)
Android自訂控制項---導覽列SlideTab(Fragment+ViewPager)