編輯:關於Android編程
為什麼我說它是最實用的 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 是如何引用的
再來看看代碼中的引用
mIndicatorView = (IndicatorView) findViewById(R.id.id_indicator) ;
mIndicatorView.setViewPager(mViewPager);
代碼簡潔明了。
四、總結
整體來說,不是很難,代碼量很少,主要用到的知識點,1、自定義屬性,2、如何測量 View,2、Cavans 中一些方法的使用;最後,看了如果覺得有用,請頂一下,謝謝!
前言Android自定義控件經常會用到Canvas繪制2D圖形,在優化自己自定義控件技能之前,必須熟練掌握Canvas繪圖機制。本文從以下三個方面對Canvas繪圖機制進
一、NDK與JNI簡介NDK全稱為native development kit本地語言(C&C++)開發包。而對應的是經常接觸的Android-SDK,(software
好了下面進入正題,我們先看一下實現效果吧:下面來介紹一下代碼: 本思路就是: 1.先到手機中掃描jpeg和png的圖片 2.獲取導圖片的路徑和圖片的父路徑名
前言上一篇我們了解了HTTP協議原理,這一篇我們來講講Apache的HttpClient和Java的HttpURLConnection,這兩種都是我們平常請求網絡會用到的