Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android自定義View,你必須知道的幾點

Android自定義View,你必須知道的幾點

編輯:關於Android編程

為什麼我們覺得自定義View是學習Android的一道坎?
為什麼那麼多Android大神卻認為自定義View又是如此的簡單?
為什麼google隨便定義一個View都是上千行的代碼?
以上這些問題,相信學Android的同學或多或少都有過這樣的疑問。
那麼,看完此文,希望對你們的疑惑有所幫助。

回到主題,自定義View ,需要掌握的幾個點是什麼呢?
我們先把自定義View細分一下,分為兩種
1) 自定義ViewGroup
2) 自定義View

其實ViewGroup最終還是繼承之View,當然它內部做了許多操作;繼承之ViewGroup的View我們一般稱之為容器,而今天我們不講這方面,後續有機會再講。
來看看自定義View 需要掌握的幾點,主要就是兩點

一、重寫 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}方法。
二、重寫 protected void onDraw(Canvas canvas) {}方法

空講理論很難理解,我們還得用例子來說明,記得我前面來寫了一篇 Android 微信6.1 tab欄圖標和字體顏色漸變的實現 的博客,裡面tab的每個item就是通過自定義View來實現的,那麼接下來就通過此例子來說明問題。

我們可以把View理解為一張白紙,而自定義View就是在這張白紙上畫上我們自己繪制的圖案,可以在繪制任何圖案,也可以在白紙的任何位置繪制,那麼問題來了,白紙哪裡來?圖案哪裡來?位置如何計算?

a)白紙好說,只要我們繼承之View,在onDraw(Canvas canvas)中的canvas就是我們所說的白紙

/**
 * Created by moon.zhong on 2015/2/13.
 */
public class CustomView extends View {
    public CustomView(Context context) {
        super(context);
    }

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // canvas 即為白紙
        super.onDraw(canvas);
    }
}

b)圖案呢?這裡的圖案就是有圖片和文字組成,這個也好說,定義一個Bitmap 成員變量,和一個String的成員變量

private Bitmap mBitmap ;
private String mName ;
mName = "這裡直接賦值";
mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher) ;

圖片可以通過資源文件可以拿到。

c)計算位置
所以最核心的也是我們認為最麻煩的地方就是計算繪制的位置,計算位置就得先測量自身的大小,也就是我們必須掌握的兩點中的第一點:需要重寫 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}方法
先來看一下google寫的TextView的onMeasure()方法是如何實現的

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int width;
    int height;

    BoringLayout.Metrics boring = UNKNOWN_BORING;
    BoringLayout.Metrics hintBoring = UNKNOWN_BORING;

    if (mTextDir == null) {
        mTextDir = getTextDirectionHeuristic();
    }

    int des = -1;
    boolean fromexisting = false;

    if (widthMode == MeasureSpec.EXACTLY) {
        // Parent has told us how big to be. So be it.
        width = widthSize;
    } else {
        if (mLayout != null && mEllipsize == null) {
            des = desired(mLayout);
        }

        if (des < 0) {
            boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
            if (boring != null) {
                mBoring = boring;
            }
        } else {
            fromexisting = true;
        }

        if (boring == null || boring == UNKNOWN_BORING) {
            if (des < 0) {
                des = (int) FloatMath.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint));
            }
            width = des;
        } else {
            width = boring.width;
        }

        final Drawables dr = mDrawables;
        if (dr != null) {
            width = Math.max(width, dr.mDrawableWidthTop);
            width = Math.max(width, dr.mDrawableWidthBottom);
        }

        if (mHint != null) {
            int hintDes = -1;
            int hintWidth;

            if (mHintLayout != null && mEllipsize == null) {
                hintDes = desired(mHintLayout);
            }

            if (hintDes < 0) {
                hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring);
                if (hintBoring != null) {
                    mHintBoring = hintBoring;
                }
            }

            if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
                if (hintDes < 0) {
                    hintDes = (int) FloatMath.ceil(Layout.getDesiredWidth(mHint, mTextPaint));
                }
                hintWidth = hintDes;
            } else {
                hintWidth = hintBoring.width;
            }

            if (hintWidth > width) {
                width = hintWidth;
            }
        }

        width += getCompoundPaddingLeft() + getCompoundPaddingRight();

        if (mMaxWidthMode == EMS) {
            width = Math.min(width, mMaxWidth * getLineHeight());
        } else {
            width = Math.min(width, mMaxWidth);
        }

        if (mMinWidthMode == EMS) {
            width = Math.max(width, mMinWidth * getLineHeight());
        } else {
            width = Math.max(width, mMinWidth);
        }

        // Check against our minimum width
        width = Math.max(width, getSuggestedMinimumWidth());

        if (widthMode == MeasureSpec.AT_MOST) {
            width = Math.min(widthSize, width);
        }
    }

    int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
    int unpaddedWidth = want;

    if (mHorizontallyScrolling) want = VERY_WIDE;

    int hintWant = want;
    int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();

    if (mLayout == null) {
        makeNewLayout(want, hintWant, boring, hintBoring,
                      width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
    } else {
        final boolean layoutChanged = (mLayout.getWidth() != want) ||
                (hintWidth != hintWant) ||
                (mLayout.getEllipsizedWidth() !=
                        width - getCompoundPaddingLeft() - getCompoundPaddingRight());

        final boolean widthChanged = (mHint == null) &&
                (mEllipsize == null) &&
                (want > mLayout.getWidth()) &&
                (mLayout instanceof BoringLayout || (fromexisting && des >= 0 && des <= want));

        final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);

        if (layoutChanged || maximumChanged) {
            if (!maximumChanged && widthChanged) {
                mLayout.increaseWidthTo(want);
            } else {
                makeNewLayout(want, hintWant, boring, hintBoring,
                        width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
            }
        } else {
            // Nothing has changed
        }
    }

    if (heightMode == MeasureSpec.EXACTLY) {
        // Parent has told us how big to be. So be it.
        height = heightSize;
        mDesiredHeightAtMeasure = -1;
    } else {
        int desired = getDesiredHeight();

        height = desired;
        mDesiredHeightAtMeasure = desired;

        if (heightMode == MeasureSpec.AT_MOST) {
            height = Math.min(desired, heightSize);
        }
    }

    int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
    if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
        unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
    }

    /*
     * We didn't let makeNewLayout() register to bring the cursor into view,
     * so do it here if there is any possibility that it is needed.
     */
    if (mMovement != null ||
        mLayout.getWidth() > unpaddedWidth ||
        mLayout.getHeight() > unpaddedHeight) {
        registerForPreDraw();
    } else {
        scrollTo(0, 0);
    }

    setMeasuredDimension(width, height);
}

哇!好長!而且方法中還嵌套方法,如果真要算下來,代碼量不會低於500行,看到這麼多代碼,頭都大了,我想這也是我們為什麼在學習Android自定義View的時候覺得如此困難的原因。大多數情況下,因為我們是自定義的View,可以說是根據我們的需求定制的View,所以很多裡面的功能我們完全沒必要,只需要幾十行代碼就能搞定。看到幾十行代碼就能搞定,感覺頓時信心倍增(^.^)
在重寫這個方法之前,得先了解一個類 MeasureSpec ,如果不了解,沒關系,下面就一起來了解一下這個類。先把代碼貼出來,膜拜一下
public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    public static final int AT_MOST     = 2 << MODE_SHIFT;
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

這裡我把裡面一些我認為沒必要的代碼都去掉了,只留了以上幾行代碼,這樣看起來很清晰,也非常容易理解。
我們先做個轉化,把上面幾個成員變量轉化成二進制

這個就不需要轉化了,這裡代表的只是一個移動的位置,也就是一個單純的數字
private static final int MODE_SHIFT = 30;
0x3 就是 11 左移30位 ,就是補30個0;
private static final int MODE_MASK  = 1100 0000 0000 0000 0000 0000 0000 0000 ;
00 左移30位
public static final int UNSPECIFIED = 0000 0000 0000 0000 0000 0000 0000 0000 ;
01 左移30位
public static final int EXACTLY     = 0100 0000 0000 0000 0000 0000 0000 0000 ;
10 左移30位
public static final int AT_MOST     = 1000 0000 0000 0000 0000 0000 0000 0000 ;

你就會問了,這樣寫有什麼好處呢? 細心的人看了上面這幾個方法就明白了,每個方法中都有一個 & 的操作,所以我們接下來看看這集幾個方法的含義是什麼,先從下往上看,先易後難
 1、       public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
 顧名思義,通過measureSpec這個參數,獲取size ,兩個都是int類型,怎麼通過一個int類型的數獲取另一個int類型的數。我們在學習java的時候知道,一個int類型是32位,任何int類型的數都是有32位,比如一個int類型的數值3,它也是占有32位,只是高30位全部為0。google 也是利用這一點,讓這個int類型的measureSpec數存了兩個信息,一個就是size,保存在int類型的低30位,另一個就是mode,保存在int類型的高2位。前面我們看到了有幾個成員變量,UNSPECIFIED,EXACTLY,AT_MOST
 者就是mode的三種選擇,目前也只有這三種選擇,所以只需要2位就能實現。
 2、      ` public static int getMode(int measureSpec) {
                return (measureSpec & MODE_MASK);
        }`
  這也好理解,獲取模式,但這些模式有啥用處呢?
  1)、EXACTLY 模式: 准確的、精確的;這種模式,是最容易理解和處理的,可以理解為大小固定,比如在定義layout_width的時候,定義為固定大小 10dp,20dp,或者match_parent(此時父控件是固定的)這時候,獲取出來的mode就是EXACTLY
  2)、AT_MOST 模式: 最大的;這種模式稍微難處理些,不過也好理解,就是View的大小最大不能超過父控件,超過了,取父控件的大小,沒有,則取自身大小,這種情況一般都是在layout_width設為warp_content時。
  3)、UNSPECIFIED 模式:不指定大小,這種情況,我們幾乎用不上,它是什麼意思呢,就是View的大小想要多大,就給多大,不受父View的限制,幾個例子就好理解了,ScrollView控件就是。

  3、        `public static int makeMeasureSpec(int size, int mode) {
              if (sUseBrokenMakeMeasureSpec) {
                  return size + mode;
              } else {
                  return (size & ~MODE_MASK) | (mode & MODE_MASK);
              }
          }`
  這個方法也好理解,封裝measureSpec的值,在定義一個View的大小時,我們只是固定了大小,你下次想要獲取mode的時候,肯定無法拿到,所以就得自己把模式添加進去,這個方法,在自定義View中,也基本不需要用到,他所使用的場所,是在設置子View的大小的時候需要用到,所以如果是自定義ViewGroup的話,就需要用到。

  感覺講了這麼多,還是不知道怎麼使用,接下來就來重寫onMeasure()方法,寫完之後,你就明白了,這裡把注解下載代碼裡頭。
  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     //這裡方法套路都是一樣,不管三七 二十一,上來就先把mode 和 size 獲取出來。

      int widthMode = MeasureSpec.getMode(widthMeasureSpec);
      int heightMode = MeasureSpec.getMode(heightMeasureSpec);
      int widthSize = MeasureSpec.getSize(widthMeasureSpec);
      int heightSize = MeasureSpec.getSize(heightMeasureSpec);
      //View 真正需要顯示的大小
      int width = 0, height = 0;
      //這裡是去測量字體大小
      measureText();
      //字體寬度加圖片寬度取最大寬度,這裡因為字體和圖片是上下排列
      int contentWidth = Math.max(mBoundText.width(), mIconNormal.getWidth());
     // 我們渴望得到的寬度
      int desiredWidth = getPaddingLeft() + getPaddingRight() + contentWidth;
      //重點來了,判斷模式,這個模式哪裡來的呢,就是在編寫xml的時候,設置的layout_width
      switch (widthMode) {
      //如果是AT_MOST,不能超過父View的寬度
          case MeasureSpec.AT_MOST:
              width = Math.min(widthSize, desiredWidth);
              break;
              //如果是精確的,好說,是多少,就給多少;
          case MeasureSpec.EXACTLY:
              width = widthSize;
              break;
              //這種情況,純屬在這裡打醬油的,可以不考慮
          case MeasureSpec.UNSPECIFIED://我是路過的
              width = desiredWidth;
              break;
      }
      int contentHeight = mBoundText.height() + mIconNormal.getHeight();
      int desiredHeight = getPaddingTop() + getPaddingBottom() + contentHeight;
      switch (heightMode) {
          case MeasureSpec.AT_MOST:
              height = Math.min(heightSize, desiredHeight);
              break;
          case MeasureSpec.EXACTLY:
              height = heightSize;
              break;
          case MeasureSpec.UNSPECIFIED:
              height = contentHeight;
              break;
      }
      //最後不要忘記了,調用父類的測量方法
      setMeasuredDimension(width, height);
  }

  到這裡,就算View的大小就已經完成了,自定義View的計算過程和以上方法基本類似。接著就是計算需要顯示的圖標和字體的位置。這裡希望圖片和字體垂直排列,並居中顯示在View當中,因為當前的View的寬高已經測量好了,接下來的計算也就非常簡單了,這裡就放在onDraw()方法中計算


d)繪制圖標和字體
繪制圖標,可以用canvas.drawBitmap(Bitmap bitmap, int left, int top ,Paint paint)方法,bitmap 已經有了,如果不需要對圖片作特殊處理 paint 可以傳入null表示原圖原樣的繪制在白紙上,所以就差繪制的位置 left ,top前面已經分析過了,需要把圖繪制在View的中間,當然這裡還需包含字體,所以可以這樣計算left 和top。

int left = (mViewWidth - mIconNormal.getWidth())/2 ;
int top = (mViewHeight - mIconNormal.getHeight() - mBoundText.height()) /2 ;


mViewWidth --->View的寬度,mIconNormal --->圖片的寬度, mBoundText.height() --->字體的高度;繪制字體,繪制字體,就比繪制圖片稍微麻煩點,因為繪制字體需要用到畫筆Paint ,這裡定義一個畫筆Paint,直接new 一個出來
    mTextPaintNormal = new Paint();
    //設置字體大小
    mTextPaintNormal.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, mTextSize, getResources().getDisplayMetrics()));
    //設置畫筆顏色,也就是字體顏色
    mTextPaintNormal.setColor(mTextColorNormal);
    //設置抗鋸齒
    mTextPaintNormal.setAntiAlias(true);
        這裡也是調用Canvas的方法 canvas.drawText(mTextValue,x,y, mTextPaintNormal);mTextValue需要繪制的字體內容, mTextPaintNormal畫筆,x,y需要繪制的位置
    float x = (mViewWidth - mBoundText.width())/2.0f ;
    float y = (mViewHeight + mIconNormal.getHeight() + mBoundText.height()) /2.0F ;

    整體來說代碼還是相當少的。下面把onDraw的代碼也貼出來
    @Override
    protected void onDraw(Canvas canvas) {
        drawBitmap(canvas) ;
        drawText(canvas) ;
    }
    private void drawBitmap(Canvas canvas) {
        int left = (mViewWidth - mIconNormal.getWidth())/2 ;
        int top = (mViewHeight - mIconNormal.getHeight() - mBoundText.height()) /2 ;
        canvas.drawBitmap(mIconNormal, left, top ,null);
    }
    private void drawText(Canvas canvas) {
        float x = (mViewWidth - mBoundText.width())/2.0f ;
        float y = (mViewHeight + mIconNormal.getHeight() + mBoundText.height()) /2.0F ;
        canvas.drawText(mTextValue,x,y, mTextPaintNormal);
    }

“`
總結:
onMeasure() 方法只要了解了 MeasureSpec 類就不是什麼問題,而MeasureSpec 也很簡單,onDraw() 方法就需要了解Canvas 類的繪制方法,並且通過簡單的Api查詢,就基本能實現我們所需的要求。對於自定義View,如果你會重寫 測量 和 onDraw 方法,那麼就具備了此技能,而如果需要了解更深,自定義有個性,更絢麗的View,就還得深入了解Canvas 、Paint等方法,

源碼下載地址,請前往我的另一篇博客 Android 微信6.1 tab欄圖標和字體顏色漸變的實現

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved