打造Android 最實用的ViewPager 指標控制項

來源:互聯網
上載者:User

標籤:viewpager   指標   控制項   

為什麼我說它是最實用的 ViewPager 指標控制項呢?
它有以下幾個特點:
1、通過自訂 View 來實現,代碼簡單易懂;
2、使用起來非常方便;
3、通用性高,大部分涉及到 ViewPager 指標的地方都能使用此控制項;
4、實現了兩種指標效果(具體請看)

一、先來看
傳統版指標的:

流行版指標的效果

二、分析
如果單純的要實現此功能,相信,大家都能實現,而我也不會拿出來這裡講了,這裡我是要把它打造成一個控制項,通俗一點講就是,在以後可以直接拿來用,而不需要修改代碼。
控制項,那就離不開自訂 View,我在前面也講了一篇關於自訂 View 的文章 Android自訂View,你必須知道的幾點 ,雖然講的很淺,但我覺得還是非常有用處的,有興趣的可以閱讀一下,對理解這篇文章很有協助。額,跑題了! 回顧下那兩張,整個 View 需要的資源其實只有兩張圖片;唯一的痛點,就是對圖片繪製的位置如何計算;既然是實現通用型易用的控制項,那就不能再 ViewPager 的 OnPagerChangerListener 中來改變指標的狀態,所以這個時候,就得把 ViewPager 傳入到這個控制項中,到這裡,分析的差不多了;

三、編碼實現功能
像白飯要一口一口的吃,這裡就得先建立一個類,然後讓他繼承之 View,前期步驟跟我的上一篇 blog 很像,就不累贅了,直接上代碼

public class IndicatorView extends View implements ViewPager.OnPageChangeListener{    //指標表徵圖,這裡是一個 drawable,包含兩種狀態,    //選中和飛選中狀態    private Drawable mIndicator;    //指標表徵圖的大小,根據表徵圖的寬和高來確定,選取較大者    private int mIndicatorSize ;    //整個指標控制項的寬度    private int mWidth ;    /*表徵圖加空格在家 padding 的寬度*/    private int mContextWidth ;    //指標表徵圖的個數,就是當前ViwPager 的 item 個數    private int mCount ;    /*每個指標之間的間隔大小*/    private int mMargin ;    /*當前 view 的 item,主要作用,是用於判斷當前指標的選中情況*/    private int mSelectItem ;    /*指標根據ViewPager 滑動的位移量*/    private float mOffset ;    /*指標是否即時重新整理*/    private boolean mSmooth ;    /*因為ViewPager 的 pageChangeListener 被佔用了,所以需要定義一個    * 以便其他調用    * */    private ViewPager.OnPageChangeListener mPageChangeListener ;    public IndicatorView(Context context) {        this(context, null);    }    public IndicatorView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public IndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        //通過 TypedArray 擷取自訂屬性        TypedArray typedArray = getResources().obtainAttributes(attrs, R.styleable.IndicatorView);        //擷取自訂屬性的個數        int N = typedArray.getIndexCount();        for (int i = 0; i < N; i++) {            int attr = typedArray.getIndex(i);            switch (attr) {                case R.styleable.IndicatorView_indicator_icon:                    //通過自訂屬性拿到指標                    mIndicator = typedArray.getDrawable(attr);                    break;                case R.styleable.IndicatorView_indicator_margin:                    float defaultMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,5,getResources().getDisplayMetrics());                    mMargin = (int) typedArray.getDimension(attr , defaultMargin);                    break ;                case R.styleable.IndicatorView_indicator_smooth:                    mSmooth = typedArray.getBoolean(attr,false) ;                    break;            }        }        //使用完成之後記得回收        typedArray.recycle();        initIndicator() ;    }    private void initIndicator() {        //擷取指標的大小值。一般情況下是正方形的,也是時,你的美工手抖了一下,切出一個長方形來了,        //不用怕,這裡做了處理不會變形的        mIndicatorSize = Math.max(mIndicator.getIntrinsicWidth(),mIndicator.getIntrinsicHeight()) ;        /*設定指標的邊框*/        mIndicator.setBounds(0,0,mIndicator.getIntrinsicWidth(),mIndicator.getIntrinsicWidth());    }}

這裡需要注意一點的就是 Drawable mIndicator這個成員變數,它是在 drawable 檔案夾下定義的一個 drawable 檔案,包含了選中和為選中兩張圖片。

接著是測量工作

    /**     * 測量View 的大小,這個方法我前面的 blog 講了很多了,     * @param widthMeasureSpec     * @param heightMeasureSpec     */    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        setMeasuredDimension(measureWidth(widthMeasureSpec),measureHeight(heightMeasureSpec));    }    /**     * 測量寬度,計算當前View 的寬度     * @param widthMeasureSpec     * @return     */    private int measureWidth(int widthMeasureSpec){        int mode = MeasureSpec.getMode(widthMeasureSpec) ;        int size = MeasureSpec.getSize(widthMeasureSpec) ;        int width ;        int desired = getPaddingLeft() + getPaddingRight() + mIndicatorSize*mCount + mMargin*(mCount -1) ;        mContextWidth = desired ;        if(mode == MeasureSpec.EXACTLY){            width = Math.max(desired, size)  ;        }else {            if(mode == MeasureSpec.AT_MOST){                width = Math.min(desired,size) ;            }else {                width = desired ;            }        }        mWidth = width ;        return width ;    }    private int measureHeight(int heightMeasureSpec){        int mode = MeasureSpec.getMode(heightMeasureSpec) ;        int size = MeasureSpec.getSize(heightMeasureSpec) ;        int height ;        if(mode == MeasureSpec.EXACTLY){            height = size ;        }else {            int desired = getPaddingTop() + getPaddingBottom() + mIndicatorSize ;            if(mode == MeasureSpec.AT_MOST){                height = Math.min(desired,size) ;            }else {                height = desired ;            }        }        return height ;    }

測量完了,就到了繪製 View 的階段了。這裡重點看看 onDraw()方法,先說一下,大致流程,
首先,繪製所有為選中的指標,這裡是繪製 Drawable,所以需要用到 Canvas中的某些方法來平移畫布,讓其順序的繪製所有的 Drawable,這裡特別注意的一點就是 Canvas.restore() 方法,這個方法是在繪製完成之後,想要回到原來的位置和狀態調用,但它必須配合Canvas.save()來配套使用。Canvas.save()就是記錄當前畫布的狀態,所以這裡,我覺得這個方法的名字應該換成 record()是不是更符合我們的理解呢?這裡純屬個人見解,理解了就好,如何命名不妨礙我們的工作,下面是 onDraw()的代碼,注釋很詳細

    /**     * 繪製指標     * @param canvas     */    @Override    protected void onDraw(Canvas canvas) {        /*        * 首先得儲存畫布的目前狀態,如果位置行這個方法        * 等一下的 restore()將會失效,canvas 不知道恢複到什麼狀態        * 所以這個 save、restore 都是成對出現的,這樣就很好理解了。        * */        canvas.save() ;        /*        * 這裡開始就是計算需要繪製的位置,        * 如果不好理解,請按照我說的做,拿起        * 附近的紙和筆,在紙上繪製一下,然後        * 你就一目瞭然了,        *        * */        int left = mWidth/2 - mContextWidth/2 +getPaddingLeft() ;        canvas.translate(left,getPaddingTop());        for(int i = 0 ; i < mCount ; i++){            /*            * 這裡也需要解釋一下,            * 因為我們額 drawable 是一個selector 檔案            * 所以我們需要設定他的狀態,也就是 state            * 來擷取相應的圖片。            * 這裡是擷取未選中的圖片            * */            mIndicator.setState(EMPTY_STATE_SET) ;            /*繪製 drawable*/            mIndicator.draw(canvas);            /*每繪製一個指標,向右移動一次*/            canvas.translate(mIndicatorSize+mMargin,0);        }        /*        * 恢複畫布的所有設定,也不是所有的啦,        * 根據 google 說法,就是matrix/clip        * 只能恢複到最後調用 save 方法的位置。        * */        canvas.restore();        /*這裡又開始計算繪製的位置了*/        float leftDraw = (mIndicatorSize+mMargin)*(mSelectItem + mOffset);        /*        * 計算完了,又來了,平移,為什麼要平移兩次呢?        * 也是為了好理解。        * */        canvas.translate(left,getPaddingTop());        canvas.translate(leftDraw,0);        /*        * 把Drawable 的狀態設為已選中狀態        * 這樣擷取到的Drawable 就是已選中        * 的那張圖片。        * */        mIndicator.setState(SELECTED_STATE_SET) ;        /*這裡又開始繪圖了*/        mIndicator.draw(canvas);    }

現在我們的控制項其實就差一步沒有實現了,就是在何時何地更新 View,一開始就分析了,這個 View 是需要傳入 ViewPager 的,傳入 ViewPager 的目的是什麼,其實有三個,
1、擷取 ViewPager 的 item 的個數,從而來確定指標的個數;
2、擷取當前 ViewPager 選中的 item,也是確定指標選中的 item;
3、擷取 OnPagerChangeListener,來控制 View 什麼時候需要重新整理;

    /**     * 此ViewPager 一定是先設定了Adapter,     * 並且Adapter 需要所有資料,後續還不能     * 修改資料     * @param viewPager     */    public void setViewPager(ViewPager viewPager){        if(viewPager == null){            return;        }        PagerAdapter pagerAdapter = viewPager.getAdapter() ;        if(pagerAdapter == null){            throw new RuntimeException("請看使用說明");        }        mCount = pagerAdapter.getCount() ;        viewPager.setOnPageChangeListener(this);        mSelectItem = viewPager.getCurrentItem() ;        invalidate();    }    public void setOnPageChangeListener(ViewPager.OnPageChangeListener mPageChangeListener) {        this.mPageChangeListener = mPageChangeListener;    }    @Override    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {        Log.v("zgy","========"+position+",===offset" + positionOffset) ;        if (mSmooth){            mSelectItem = position ;            mOffset = positionOffset ;            invalidate();        }        if(mPageChangeListener != null){            mPageChangeListener.onPageScrolled(position,positionOffset,positionOffsetPixels);        }    }    @Override    public void onPageSelected(int position) {        mSelectItem = position ;        invalidate();        if(mPageChangeListener != null){            mPageChangeListener.onPageSelected(position);        }    }    @Override    public void onPageScrollStateChanged(int state) {        if(mPageChangeListener != null){            mPageChangeListener.onPageScrollStateChanged(state);        }    }

這個位置也有個點需要提一下,就是當 mSmooth 為 true 的時候,這個時候是需要即時重新整理的,所以需要在onPageScrolled(int position, float positionOffset, int positionOffsetPixels)調用 invalidate(),並把位移量儲存起來,用於計算繪製指標的位置。

好了,以上就是指標控制項的實現全過程;

既然是一個控制項,接下來看看在 xml 是如何引用的

    <com.gyzhong.viewpagerindicator.IndicatorView        android:id="@+id/id_indicator"        android:layout_centerHorizontal="true"        android:layout_alignParentBottom="true"        android:layout_marginBottom="20dp"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:padding="5dp"        zgy:indicator_icon="@drawable/indicator_selector"        zgy:indicator_margin="5dp"/>

再來看看代碼中的引用

        mIndicatorView = (IndicatorView) findViewById(R.id.id_indicator) ;        mIndicatorView.setViewPager(mViewPager);

代碼簡潔明了。

四、總結
整體來說,不是很難,代碼量很少,主要用到的知識點,1、自訂屬性,2、如何測量 View,2、Cavans 中一些方法的使用;最後,看了如果覺得有用,請頂一下,謝謝!

源碼:戳我

打造Android 最實用的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.