Android 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頭部指示器控制元件 ViewPagerBelowIndicatorAndroid控制元件ViewpagerIndicator
- Android 自定義控制元件玩轉字型變色 打造炫酷ViewPager指示器Android控制元件Viewpager
- 自定義ViewPager指示器Viewpager
- ViewPager最佳實踐Viewpager
- Android實現雙層ViewPager巢狀AndroidViewpager巢狀
- 敲酷炫的 ViewPager 切換效果和彈性指示器。Viewpager
- Android中使用RecyclerView + SnapHelper實現類似ViewPager效果AndroidViewpager
- 自定義控制元件ViewPager控制元件Viewpager
- Android使用(TabLayout+ViewPager+fragment)與(FragmentTabHost+ViewPager+Fragment)實現底部狀態列切換AndroidTabLayoutViewpagerFragment
- Android照片牆加強版,使用ViewPager實現畫廊效果AndroidViewpager
- Android之ViewPager+GridView實現GridView介面滑動AndroidViewpager
- Android開發之ViewPager+Fragment+FragmentTabHost實現底部選單AndroidViewpagerFragment
- android 三種實現水平向滑動方式(ViewPager、ViewFilpper、ViewFlow)的比較AndroidViewpager
- Android ViewPager 的使用總結AndroidViewpager
- Android中實現類似iOS的SwitchButton控制元件AndroidiOS控制元件
- Android 實現平滑滾動的歌詞控制元件Android控制元件
- Android Banner - ViewPager 02AndroidViewpager
- Android實現圖片滾動控制元件Android控制元件
- Flutter 基礎控制元件篇-->進度指示器Flutter控制元件
- ViewPager、Fragment和TabLayout實現切頁效果ViewpagerFragmentTabLayout
- TabLayout+ViewPager+Fragment實現切頁展示TabLayoutViewpagerFragment
- TabLayout+ViewPager+fragment實現懶載入TabLayoutViewpagerFragment
- TabLayout+ViewPager+Fragment懶載入實現TabLayoutViewpagerFragment
- Android ViewPager使用詳解AndroidViewpager
- VirtualView Android實現詳解(二)—— 虛擬控制元件的設計與實現ViewAndroid控制元件
- 【Android】 banner+tab吸頂+viewpager切換+重新整理載入之實現AndroidViewpager
- ViewPager兩種方式實現無限輪播Viewpager
- 利用ViewPager和Fragment實現頁卡切換ViewpagerFragment
- ViewPager實現左右無限迴圈滑動Viewpager
- Android通過Chronometer控制元件實現計時功能Android控制元件
- Android 禁止ViewPager左右滑動AndroidViewpager
- 搶購倒數計時自定義控制元件的實現與最佳化控制元件
- Android Studio中Spinner控制元件的資料繫結實現Android控制元件
- Android中修改原始碼實現AutoCompeteTextView控制元件的模糊匹配Android原始碼TextView控制元件
- Android自定義日曆控制元件的實現過程詳解Android控制元件
- 一行程式碼實現ViewPager卡片效果行程Viewpager
- 安卓開發:viewpager + fragment 實現滑動切換安卓ViewpagerFragment
- 仿愛奇藝/騰訊視訊ViewPager導航條實現Viewpager