Android自訂控制項---導覽列SlideTab(Fragment+ViewPager)

來源:互聯網
上載者:User

標籤:

一、前言

好久沒有更新過部落格了,趁今天有空分享一個導覽列的自訂控制項。有關此控制項的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)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.