編輯:Android資訊
為什麼我說它是最實用的 ViewPager 指示器控件呢?它有以下幾個特點:
傳統版指示器的效果圖:
流行版指示器的效果
如果單純的要實現此功能,相信,大家都能實現,而我也不會拿出來這裡講了,這裡我是要把它打造成一個控件,通俗一點講就是,在以後可以直接拿來用,而不需要修改代碼。
控件,那就離不開自定義 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 Overflow menu的幾個相關問題: 什麼是Overflow menu Android 3.0以上默認不顯示o
之前做過一個項目(隨心壁紙),主要展示過去每期的壁紙主題以及相應的壁紙,而且策劃要求,最好可以動態變換主題呈現方式,這樣用戶體驗會比較好。嗯,好吧,策劃的話,咱們
整個框架式不同於androidannotations,Roboguice等ioc框架,這是一個類似spring的實現方式。在整應用的生命周期中找到切入點,然後對a
1. 對話保持的解決方案。 要求: 1、app中使用webview訪問具體網站的內容,但是app與服務器的溝通是使用HttpURLConnection來完成。 2