Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android自定義控件---導航欄SlideTab(Fragment+ViewPager)

Android自定義控件---導航欄SlideTab(Fragment+ViewPager)

編輯:關於Android編程

一、前言

好久沒有更新過博客了,趁今天有空分享一個導航欄的自定義控件。有關此控件的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容器,綠色框是游標。
這裡寫圖片描述
綠色框高度的值就是indicatZ喎?/kf/ware/vc/" target="_blank" class="keylink">vclBhaW50LmdldFN0cm9rZVdpZHRoKCmho73Tz8K1xLmk1/e+zcrHvMbL48/CzbzX89PSwb2x37XEs8jJq7XjoaM8YnIgLz4NCjxpbWcgYWx0PQ=="這裡寫圖片描述" src="/uploadfile/Collfiles/20160721/20160721111549899.png" title="\" />
紅色橫線表示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在所難免。僅限於學習。希望能幫到各位。

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