天天看點

Android UI 之一步步教你自定義控件(自定義屬性、合理設計onMeasure、合理設計onDraw等)

 Android開發做到了一定程度,多少都會用到自定義控件,一方面是更加靈活,另一方面在大資料量的情況下自定義控件的效率比寫布局檔案更高。

    一個相對完善的自定義控件在布局檔案中和java代碼中都應能靈活設定屬性。另外在普通的布局中和AdapterView中都應能正确繪制,這就要求合理設計onMeasure方法,下文中會做比較詳細的講解。

    本文原創,如需轉載,請注明轉載位址:http://blog.csdn.net/carrey1989/article/details/11757409

    接下來我就一步一步來講解如何設計和編寫一個比較完善的自定義控件。

    首先要來設計好我們要完成的效果,我們今天來實作下圖所示的這樣一個控件:

Android UI 之一步步教你自定義控件(自定義屬性、合理設計onMeasure、合理設計onDraw等)

    用文字來描述一下:我們要定義的控件上方會顯示一張圖檔,我們可以設定這張圖檔的内容,長寬比,透明度,伸縮模式,以及圖檔四周的填充空間大小。圖檔下方會顯示一行文字,作為一級标題,我們可以設定文字的内容,大小,顔色,以及文字區域四周的填充空間的大小。一級标題下方顯示一行二級标題,具體設定内容和一級标題相同。

    我們不妨先來直接看一下完成後的效果,這樣可以更直覺的了解要實作的控件的樣子。

Android UI 之一步步教你自定義控件(自定義屬性、合理設計onMeasure、合理設計onDraw等)
Android UI 之一步步教你自定義控件(自定義屬性、合理設計onMeasure、合理設計onDraw等)

    左圖的樣子是在正常的布局中自定義控件的樣子,右圖則是在大資料量的情況下自定義控件作為AdapterView的item的時候繪制出來的樣子。

    上面我們大體完成了初步的控件設計,下面我們開始編寫代碼。

    第一步,我們寫好自定義屬性,根據我們上面所做的設計,我們的自定義屬性涉及到三個方面,分别是圖檔相關的屬性,一級标題相關的屬性,二級标題相關的屬性。

    按照慣例,我們首先在res/values檔案目錄下建立一個attrs.xml檔案。

    然後我們在attrs.xml檔案中完成我們對屬性的定義,代碼片段如下:

[html]  view plain copy

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <resources>  
  3.     <attr name="imageSrc" format="reference"/>  
  4.     <attr name="imageAspectRatio" format="float"/>  
  5.     <attr name="imageAlpha" format="float"/>  
  6.     <attr name="imagePaddingLeft" format="dimension"/>  
  7.     <attr name="imagePaddingTop" format="dimension"/>  
  8.     <attr name="imagePaddingRight" format="dimension"/>  
  9.     <attr name="imagePaddingBottom" format="dimension"/>  
  10.     <attr name="imageScaleType">  
  11.         <enum name="fillXY" value="0"/>  
  12.         <enum name="center" value="1"/>  
  13.     </attr>  
  14.     <attr name="titleText" format="string"/>  
  15.     <attr name="titleTextSize" format="dimension"/>  
  16.     <attr name="titleTextColor" format="color"/>  
  17.     <attr name="titlePaddingLeft" format="dimension"/>  
  18.     <attr name="titlePaddingTop" format="dimension"/>  
  19.     <attr name="titlePaddingRight" format="dimension"/>  
  20.     <attr name="titlePaddingBottom" format="dimension"/>  
  21.     <attr name="subTitleText" format="string"/>  
  22.     <attr name="subTitleTextSize" format="dimension"/>  
  23.     <attr name="subTitleTextColor" format="color"/>  
  24.     <attr name="subTitlePaddingLeft" format="dimension"/>  
  25.     <attr name="subTitlePaddingTop" format="dimension"/>  
  26.     <attr name="subTitlePaddingRight" format="dimension"/>  
  27.     <attr name="subTitlePaddingBottom" format="dimension"/>  
  28.     <declare-styleable name="CustomView">  
  29.         <attr name="imageSrc"/>  
  30.         <attr name="imageAspectRatio" />  
  31.         <attr name="imageAlpha" />  
  32.         <attr name="imagePaddingLeft" />  
  33.         <attr name="imagePaddingTop" />  
  34.         <attr name="imagePaddingRight" />  
  35.         <attr name="imagePaddingBottom" />  
  36.         <attr name="imageScaleType" />  
  37.         <attr name="titleText" />  
  38.         <attr name="titleTextSize" />  
  39.         <attr name="titleTextColor" />  
  40.         <attr name="titlePaddingLeft" />  
  41.         <attr name="titlePaddingTop" />  
  42.         <attr name="titlePaddingRight" />  
  43.         <attr name="titlePaddingBottom" />  
  44.         <attr name="subTitleText" />  
  45.         <attr name="subTitleTextSize" />  
  46.         <attr name="subTitleTextColor" />  
  47.         <attr name="subTitlePaddingLeft" />  
  48.         <attr name="subTitlePaddingTop" />  
  49.         <attr name="subTitlePaddingRight" />  
  50.         <attr name="subTitlePaddingBottom" />  
  51.     </declare-styleable>  
  52. </resources>  

    這裡需要說明幾點:<attr>标簽的format屬性值代表屬性的類型,這個類型值一共有10種,分别是:reference,float,color,dimension,boolean,string,enum,integer,fraction,flag

。但是我們作為開發者常用的基本上隻有reference,float,color,dimension,boolean,string,enum這7種。在attrs.xml檔案中的<declare-styleable>标簽的name屬性的值,按照慣例我們都是寫成自定義控件類的名字。一個同名的<attr>在attrs.xml中隻可以定義一次。

    除此之外,上面的代碼都是針對前面的設計來定義了各種屬性,相信各位同學都能看懂。

    第二步就是編寫我們自定義控件的java類了,我們首先将之前做的自定義屬性在自定義控件類中做好聲明:

[java]  view plain copy

  1. private Bitmap imageBitmap;  
  2. private float imageAspectRatio;  
  3. private float imageAlpha;  
  4. private int imagePaddingLeft;  
  5. private int imagePaddingTop;  
  6. private int imagePaddingRight;  
  7. private int imagePaddingBottom;  
  8. private int imageScaleType;  
  9. private static final int SCALE_TYPE_FILLXY = 0;  
  10. private static final int SCALE_TYPE_CENTER = 1;  
  11. private String titleText;  
  12. private int titleTextSize;  
  13. private int titleTextColor;  
  14. private int titlePaddingLeft;  
  15. private int titlePaddingTop;  
  16. private int titlePaddingRight;  
  17. private int titlePaddingBottom;  
  18. private String subTitleText;  
  19. private int subTitleTextSize;  
  20. private int subTitleTextColor;  
  21. private int subTitlePaddingLeft;  
  22. private int subTitlePaddingTop;  
  23. private int subTitlePaddingRight;  
  24. private int subTitlePaddingBottom;  
  25. private Paint paint;  
  26. private TextPaint textPaint;  
  27. private Rect rect;  
  28. private static final int MIN_SIZE = 12;  
  29. private int mViewWidth;  
  30. private int mViewHeight;  

    然後我們要在構造方法中,将從布局檔案中讀取的自定義屬性解析出來。

[java]  view plain copy

  1. TypedArray a = context.getTheme().obtainStyledAttributes(  
  2.         attrs, R.styleable.CustomView, defStyle, 0);  
  3. int n = a.getIndexCount();  
  4. for (int i = 0; i < n; i++) {  
  5.     int attr = a.getIndex(i);  
  6.     switch (attr) {  
  7.     case R.styleable.CustomView_imageSrc:  
  8.         imageBitmap = BitmapFactory.decodeResource(  
  9.                 getResources(), a.getResourceId(attr, 0));  
  10.         break;  
  11.     case R.styleable.CustomView_imageAspectRatio:  
  12.         imageAspectRatio = a.getFloat(attr, 1.0f);//預設長寬相等  
  13.         break;  
  14.     case R.styleable.CustomView_imageAlpha:  
  15.         imageAlpha = a.getFloat(attr, 1.0f);//預設不透明  
  16.         if (imageAlpha > 1.0f) imageAlpha = 1.0f;  
  17.         if (imageAlpha < 0.0f) imageAlpha = 0.0f;  
  18.         break;  
  19.     case R.styleable.CustomView_imagePaddingLeft:  
  20.         imagePaddingLeft = a.getDimensionPixelSize(attr, 0);  
  21.         break;  
  22.     case R.styleable.CustomView_imagePaddingTop:  
  23.         imagePaddingTop = a.getDimensionPixelSize(attr, 0);  
  24.         break;  
  25.     case R.styleable.CustomView_imagePaddingRight:  
  26.         imagePaddingRight = a.getDimensionPixelSize(attr, 0);  
  27.         break;  
  28.     case R.styleable.CustomView_imagePaddingBottom:  
  29.         imagePaddingBottom = a.getDimensionPixelSize(attr, 0);  
  30.         break;  
  31.     case R.styleable.CustomView_imageScaleType:  
  32.         imageScaleType = a.getInt(attr, 0);  
  33.         break;  
  34.     case R.styleable.CustomView_titleText:  
  35.         titleText = a.getString(attr);  
  36.         break;  
  37.     case R.styleable.CustomView_titleTextSize:  
  38.         titleTextSize = a.getDimensionPixelSize(  
  39.                 attr, (int) TypedValue.applyDimension(  
  40.                         TypedValue.COMPLEX_UNIT_SP, 25, getResources().getDisplayMetrics()));//預設标題字型大小25sp  
  41.         break;  
  42.     case R.styleable.CustomView_titleTextColor:  
  43.         titleTextColor = a.getColor(attr, 0x00000000);//預設黑色字型  
  44.         break;  
  45.     case R.styleable.CustomView_titlePaddingLeft:  
  46.         titlePaddingLeft = a.getDimensionPixelSize(attr, 0);  
  47.         break;  
  48.     case R.styleable.CustomView_titlePaddingTop:  
  49.         titlePaddingTop = a.getDimensionPixelSize(attr, 0);  
  50.         break;  
  51.     case R.styleable.CustomView_titlePaddingRight:  
  52.         titlePaddingRight = a.getDimensionPixelSize(attr, 0);  
  53.         break;  
  54.     case R.styleable.CustomView_titlePaddingBottom:  
  55.         titlePaddingBottom = a.getDimensionPixelSize(attr, 0);  
  56.         break;  
  57.     case R.styleable.CustomView_subTitleText:  
  58.         subTitleText = a.getString(attr);  
  59.         break;  
  60.     case R.styleable.CustomView_subTitleTextSize:  
  61.         subTitleTextSize = a.getDimensionPixelSize(attr,   
  62.                 (int) TypedValue.applyDimension(  
  63.                         20, TypedValue.COMPLEX_UNIT_SP, getResources().getDisplayMetrics()));//預設子标題字型大小20sp  
  64.         break;  
  65.     case R.styleable.CustomView_subTitleTextColor:  
  66.         subTitleTextColor = a.getColor(attr, 0x00000000);  
  67.         break;  
  68.     case R.styleable.CustomView_subTitlePaddingLeft:  
  69.         subTitlePaddingLeft = a.getDimensionPixelSize(attr, 0);  
  70.         break;  
  71.     case R.styleable.CustomView_subTitlePaddingTop:  
  72.         subTitlePaddingTop = a.getDimensionPixelSize(attr, 0);  
  73.         break;  
  74.     case R.styleable.CustomView_subTitlePaddingRight:  
  75.         subTitlePaddingRight = a.getDimensionPixelSize(attr, 0);  
  76.         break;  
  77.     case R.styleable.CustomView_subTitlePaddingBottom:  
  78.         subTitlePaddingBottom = a.getDimensionPixelSize(attr, 0);  
  79.         break;  
  80.     }  
  81. }  
  82. a.recycle();  

    這裡需要說明幾點,TypedArray對象在使用完畢後一定要調用recycle()方法。我之前曾在一篇文章中總結過在java代碼中進行px與dip(dp)、px與sp機關值的轉換。實際上,android中也提供了機關轉換的函數,我們也可以使用TypedValue.applyDimension(int unit, float value, DisplayMetrics metrics)方法來進行機關的互換,其中,第一個參數是你想要得到的機關,第二個參數是你想得到的機關的數值,比如:我要得到一個25sp,那麼我就用TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 25,getResources().getDisplayMetrics()),傳回的就是25sp對應的px數值了。

    接下來我們要開始設計onMeasure方法,再設計onMeasure之前我們簡單了解幾個概念。

    MeasureSpec的三種模式:

    EXACTLY:表示我們設定了MATCH_PARENT或者一個準确的數值,含義是父布局要給子布局一個确切的大小。

    AT_MOST:表示子布局将被限制在一個最大值之内,通常是子布局設定了wrap_content。

    UNSPECIFIED:表示子布局想要多大就可以要多大,通常出現在AdapterView中item的heightMode中。

    了解了上面幾個概念,我們就可以開始設計onMeasure了,具體代碼如下:

[java]  view plain copy

  1. @Override  
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  3.     int widthMode = MeasureSpec.getMode(widthMeasureSpec);  
  4.     int widthSize = MeasureSpec.getSize(widthMeasureSpec);  
  5.     int heightMode = MeasureSpec.getMode(heightMeasureSpec);  
  6.     int heightSize = MeasureSpec.getSize(heightMeasureSpec);  
  7.     int width;  
  8.     int height;  
  9.     if (widthMode == MeasureSpec.EXACTLY) {  
  10.         width = widthSize;  
  11.     } else {  
  12.         int desired = getPaddingLeft() + getPaddingRight() +   
  13.                 imagePaddingLeft + imagePaddingRight;  
  14.         desired += (imageBitmap != null) ? imageBitmap.getWidth() : 0;  
  15.         width = Math.max(MIN_SIZE, desired);  
  16.         if (widthMode == MeasureSpec.AT_MOST) {  
  17.             width = Math.min(desired, widthSize);  
  18.         }  
  19.     }  
  20.     if (heightMode == MeasureSpec.EXACTLY) {  
  21.         height = heightSize;  
  22.     } else {  
  23.         int rawWidth = width - getPaddingLeft() - getPaddingRight();  
  24.         int desired = (int) (getPaddingTop() + getPaddingBottom() + imageAspectRatio * rawWidth);  
  25.         if (titleText != null) {  
  26.             paint.setTextSize(titleTextSize);  
  27.             FontMetrics fm = paint.getFontMetrics();  
  28.             int textHeight = (int) Math.ceil(fm.descent - fm.ascent);  
  29.             desired += (textHeight + titlePaddingTop + titlePaddingBottom);  
  30.         }  
  31.         if (subTitleText != null) {  
  32.             paint.setTextSize(subTitleTextSize);  
  33.             FontMetrics fm = paint.getFontMetrics();  
  34.             int textHeight = (int) Math.ceil(fm.descent - fm.ascent);  
  35.             desired += (textHeight + subTitlePaddingTop + subTitlePaddingBottom);  
  36.         }  
  37.         height = Math.max(MIN_SIZE, desired);  
  38.         if (heightMode == MeasureSpec.AT_MOST) {  
  39.             height = Math.min(desired, heightSize);  
  40.         }  
  41.     }  
  42.     setMeasuredDimension(width, height);  
  43. }  

    思路是這樣的:我們首先判斷是不是EXACTLY模式,如果是,那就可以直接設定值了,如果不是,我們先按照UNSPECIFIED模式處理,讓子布局得到自己想要的最大值,然後判斷是否是AT_MOST模式,來做最後的限制。

    完成onMeasure過程之後,我們需要開始onDraw的設計,在onDraw中我們需要考慮各個部分設定的padding值,然後對應做出坐标的處理,整體的思路是從下向上繪制。具體的代碼如下:

[java]  view plain copy

  1. @Override  
  2. protected void onDraw(Canvas canvas) {  
  3.     rect.left = getPaddingLeft();  
  4.     rect.top = getPaddingTop();  
  5.     rect.right = mViewWidth - getPaddingRight();  
  6.     rect.bottom = mViewHeight - getPaddingBottom();  
  7.     paint.setAlpha(255);  
  8.     if (subTitleText != null) {  
  9.         paint.setTextSize(subTitleTextSize);  
  10.         paint.setColor(subTitleTextColor);  
  11.         paint.setTextAlign(Paint.Align.LEFT);  
  12.         FontMetrics fm = paint.getFontMetrics();  
  13.         int textHeight = (int) Math.ceil(fm.descent - fm.ascent);  
  14.         int left = getPaddingLeft() + subTitlePaddingLeft;  
  15.         int right = mViewWidth - getPaddingRight() - subTitlePaddingRight;  
  16.         int bottom = mViewHeight - getPaddingBottom() - subTitlePaddingBottom;  
  17.         String msg = TextUtils.ellipsize(subTitleText, textPaint, right - left, TextUtils.TruncateAt.END).toString();  
  18.         float textWidth = paint.measureText(msg);  
  19.         float x = textWidth < (right - left) ? left + (right - left - textWidth) / 2 : left;  
  20.         canvas.drawText(msg, x, bottom - fm.descent, paint);  
  21.         rect.bottom -= (textHeight + subTitlePaddingTop + subTitlePaddingBottom);  
  22.     }  
  23.     if (titleText != null) {  
  24.         paint.setTextSize(titleTextSize);  
  25.         paint.setColor(titleTextColor);  
  26.         paint.setTextAlign(Paint.Align.LEFT);  
  27.         FontMetrics fm = paint.getFontMetrics();  
  28.         int textHeight = (int) Math.ceil(fm.descent - fm.ascent);  
  29.         float left = getPaddingLeft() + titlePaddingLeft;  
  30.         float right = mViewWidth - getPaddingRight() - titlePaddingRight;  
  31.         float bottom = rect.bottom - titlePaddingBottom;  
  32.         String msg = TextUtils.ellipsize(titleText, textPaint, right - left, TextUtils.TruncateAt.END).toString();  
  33.         float textWidth = paint.measureText(msg);  
  34.         float x = textWidth < right - left ? left + (right - left - textWidth) / 2 : left;  
  35.         canvas.drawText(msg, x, bottom - fm.descent, paint);  
  36.         rect.bottom -= (textHeight + titlePaddingTop + titlePaddingBottom);  
  37.     }  
  38.     if (imageBitmap != null) {  
  39.         paint.setAlpha((int) (255 * imageAlpha));  
  40.         rect.left += imagePaddingLeft;  
  41.         rect.top += imagePaddingTop;  
  42.         rect.right -= imagePaddingRight;  
  43.         rect.bottom -= imagePaddingBottom;  
  44.         if (imageScaleType == SCALE_TYPE_FILLXY) {  
  45.             canvas.drawBitmap(imageBitmap, null, rect, paint);  
  46.         } else if (imageScaleType == SCALE_TYPE_CENTER) {  
  47.             int bw = imageBitmap.getWidth();  
  48.             int bh = imageBitmap.getHeight();  
  49.             if (bw < rect.right - rect.left) {  
  50.                 int delta = (rect.right - rect.left - bw) / 2;  
  51.                 rect.left += delta;  
  52.                 rect.right -= delta;  
  53.             }  
  54.             if (bh < rect.bottom - rect.top) {  
  55.                 int delta = (rect.bottom - rect.top - bh) / 2;  
  56.                 rect.top += delta;  
  57.                 rect.bottom -= delta;  
  58.             }  
  59.             canvas.drawBitmap(imageBitmap, null, rect, paint);  
  60.         }  
  61.     }  
  62. }  

    當做完這一步的時候,我們的自定義控件已經能夠在布局檔案中進行使用了,但是我們還不能在AdapterView中用我們設計的布局檔案,因為AdapterView中每一個item屬性都是在java代碼中動态設定的,是以我們就需要給我們的自定義控件開放屬性設定的接口,我們這裡暫時隻開放了設定圖檔和文字内容的接口。

[java]  view plain copy

  1. public void setImageBitmap(Bitmap bitmap) {  
  2.     imageBitmap = bitmap;  
  3.     requestLayout();  
  4.     invalidate();  
  5. }  
  6. public void setTitleText(String text) {  
  7.     titleText = text;  
  8.     requestLayout();  
  9.     invalidate();  
  10. }  
  11. public void setSubTitleText(String text) {  
  12.     subTitleText = text;  
  13.     requestLayout();  
  14.     invalidate();  
  15. }  

    做到這一步的時候,這個自定義控件基本就算完成了,後續的工作就是一些完善和修補了。

    接下來就是自定義控件的使用了,在布局檔案中使用自定義控件的時候我們需要額外做一點工作,如下:

[java]  view plain copy

  1. <RelativeLayout   
  2.     xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     xmlns:carrey="http://schemas.android.com/apk/res/com.carrey.customview"  
  4.     xmlns:tools="http://schemas.android.com/tools"  
  5.     android:layout_width="match_parent"  
  6.     android:layout_height="match_parent"  
  7.     tools:context=".MainActivity" >  
  8.     <com.carrey.customview.customview.CustomView   
  9.         android:id="@+id/customview"  
  10.         android:layout_width="200dp"  
  11.         android:layout_height="200dp"  
  12.         android:layout_centerInParent="true"  
  13.         android:background="#FFD700"  
  14.         carrey:imageSrc="@drawable/clock"  
  15.         carrey:imageAspectRatio="1.0"  
  16.         carrey:imageAlpha="0.5"  
  17.         carrey:imagePaddingLeft="5dp"  
  18.         carrey:imagePaddingTop="5dp"  
  19.         carrey:imagePaddingRight="5dp"  
  20.         carrey:imagePaddingBottom="5dp"  
  21.         carrey:imageScaleType="center"  
  22.         carrey:titleText="這是一級标題"  
  23.         carrey:titleTextSize="30sp"  
  24.         carrey:titleTextColor="#1E90FF"  
  25.         carrey:titlePaddingLeft="4dp"  
  26.         carrey:titlePaddingTop="4dp"  
  27.         carrey:titlePaddingRight="4dp"  
  28.         carrey:titlePaddingBottom="4dp"  
  29.         carrey:subTitleText="這是二級子标題"  
  30.         carrey:subTitleTextSize="20sp"  
  31.         carrey:subTitleTextColor="#00FF7F"  
  32.         carrey:subTitlePaddingLeft="3dp"  
  33.         carrey:subTitlePaddingTop="3dp"  
  34.         carrey:subTitlePaddingRight="3dp"  
  35.         carrey:subTitlePaddingBottom="3dp"/>  
  36.     <Button   
  37.         android:id="@+id/button"  
  38.         android:layout_width="match_parent"  
  39.         android:layout_height="wrap_content"  
  40.         android:text="next page"/>  
  41. </RelativeLayout>  

我們需要添加一行xmlns:carrey="http://schemas.android.com/apk/res/com.carrey.customview",其中carrey是一個字首,你可以随意設定,com.carrey.customview是我們的應用的包名,如果拿不準的可以打開Manifest檔案,在<manifest>節點中找到package屬性值即可。

對于在AdapterView中的使用方法就和我們正常使用一個常用控件的方法是一樣的,這裡就不贅述了,如果說到了這裡還有一些不明白的地方,可以下載下傳我下面提供的源碼,然後對照着部落格的思路來看,或者給我留言進行交流。

源碼下載下傳

繼續閱讀