標籤: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 指標控制項