編輯:關於Android編程
UI作為用戶看得到的東西,已經成為吸引用戶的最重要因素了。在android中提供了大量的widget以及主題和屬性,加上各種動畫,已經可以實現非常多很絢麗的控件了。但是很多情況下,僅僅使用系統提供給我們的控件,總是有那麼點缺憾。即每個控件的存在都有自身的特定功能,當我們卻是需要這些功能的時候,無疑是很好的選擇,但如果我們不需要這些功能,但卻需要其中的某些特性呢?這個時候,就需要我們自定義一個新的控件了。關於自定義控件,按照復雜程度,可以分成三大類,
①、僅僅繼承某個已存在的widget重寫部分方法。
②、組合多個已存在的widget組合成一個復合控件。
③、完全自定義控件,包括外觀。
上面的最復雜的就是第三種,也是本文的重點內容。在了解如何自定義控件之前,我們首先得明白View是個什麼玩意,它的實現機制是什麼,有什麼特點,才能更好的去掌握它。接下來就先了解一下View。
View是所有的layout和widget的基礎,它是以矩形的形式在屏幕中占領著一定的空間,並且負責處理各種事件以及界面渲染。也就是說,無論是控件還是布局,它都是以矩形的形式在屏幕中出現的,但是通過改變形狀,是可以修改控件可見的形狀,但記得,它本質上還是矩形。而布局則是看不見的VIew,本質上也是一塊矩形。為什麼強調它是矩形,因為後面需要用的這個特征。
View根據用途,比如展示圖片,展示文字,展示滾動內容.....可以分為很多種類,但是無論是什麼類型的View,都有兩種方式可以使用它們,一種是通過代碼的形式,另外一種是通過布局文件的形式。因為布局文件的形式寫法比較符合邏輯與視圖的分離模式,並且用起來自由度也高,所以是比較推薦這種方式的。記住,無論你是通過哪種途徑產生的View,它們最終都會被添加到視圖樹裡面。這樣有助於系統進行事件的攔截處理以及統一管理視圖。
一旦我們建立了View,通常會對它進行一些基本的屬性設置,監聽器設置,或者是焦點處理。但是對於一個需要完全自定義View來說,這些不是重點,重點的是measure,layout,draw這三個方法。如果沒有需要的話,前往不要調用這些方法,否則會破壞系統本身對視圖的渲染。但如果確實又需要,就需要我們好好理解這些方法之間的聯系,處理好界面渲染的關系。所以下面會重點講解這些。
如果要自定義View,那麼接下來的關於View的知識都是需要了解清楚的。雖然關於自定義View的知識很多,但是我們沒必要都重寫所有的有關自定義View的方法。比如,你可以單單重寫onDraw方法,就可以實現一個自己繪圖的View。接下來,了解關於會影響自定義View的方方面面。
View的創建有兩種形式,一種是代碼形式,另外一種是xml布局文件形式,對於XML文件填充的View還需要處理xml裡邊寫的相關屬性。對於View的構造函數有以下幾個:
1.public View(Context context)
2.public View(Context context, @Nullable AttributeSet attrs)
3.public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr)
4.public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)第一種:主要用於使用代碼創建View,context是View的上下文環境,通過它可以獲取主題和資源文件。
第二種:用於通過Xml文件填充的View,AttributeSet是一個屬性集,它包含了你在xml文件中指定的屬性集合,記得在調用次構造函數的時候,必須調用父類的對應的構造函數,以便系統初始化視圖的本身的樣式風格。如果,我們自己寫了屬性集,就需要在這裡獲取到我們的TypeArray的對象,根據自己定義的stylable名稱去獲取屬性值,以便後續處理。默認情況下,即你沒有自定義屬性的情況下,使用的是系統提供的主題和屬性集。填充完View之後,會調用onFinishInflate()方法。
第三種:也是用於通過xml文件填充的View,但是這個可以指定一個主題風格。
第四種:在第三種的基礎上,可以指定一種主題風格並指定它的主題資源。
通常我們只需要重寫前面兩個構造函數就ok的了。
View的布局過程,主要有onMeasure,onlayout,ondraw,onsizechanger這幾個方法,其中涉及到測量,布局,繪畫各方面。所以很經常自定義View所花的時間就在這上面。同時這也是自定義View的重點所在。接下來我們了解一下這些方法。
這個方法會在View通過視圖樹去遍歷它包括它的所有子View的尺寸要求的時候被調用。在了解onMeasure之前,我們先了解MeasureSpec。
MesusreSpec是當前View的父容器傳給它的關於父容器對它的尺寸的要求,這個要求是根據父容器的layoutParam和當前View的寬高綜合給出的測量規格。每個MesureSpec都包含兩方面的內容,一個是mode,一個是size。其中mode表示當前父容器對view的限制模式,size表示父容器給出的建議尺寸。
view的限制模式有三種:
①、UNSPECIFIED:即父容器不對當前View做任何限制,因此View可以使用任何尺寸。一般用於設置默認的尺寸。
②、EXACTLY:即父容器給當前View指定了一個確定的尺寸,無論你給View設置了什麼值,view都將會是指定的尺寸。
③、AT_MOST:即父容器給出了一個最大的尺寸,view設置的尺寸大小不能超過這個范圍。
根據上面三種描述,總結一下,假設現在有一個默認尺寸,一個父容器建議的尺寸。第一種情況,可以設置為任何你想要的值,第二種情況只能使用建議的尺寸,第三種情況只能使用小於建議尺寸的值。由於我們聲明一個View的時候,一般都會指定寬高的大小,所以我們遇到的情況只會是第二第三種。第二種發生在在布局中指定了確定大小或者match_parent的情況,第三種發生在指定的大小是Wrap_content的情況。標准的使用方法如下:
public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; }
OnMeasure方法在View需要確認它的內容和子View所占據的測量空間的時候調用。一般是measure方法的回調,所以,如果View有重寫這個回調方法的話,一定要提供一個有效的測量值。即在根據測量的mode和size綜合得出的測量高度和寬度的時候,一定要調用setMeasureDimensions方法將測量的寬高穿進去,以便父容器知道當前View的要求的寬高。此回調方法在View測量的過程中可能會被回調多次,因為有時候所有子View給的測量寬高的綜合,當前View並不能滿足,所以又需要重新測量一次。
當View需要給所有的子View分配空間和大小的時候調用。一般我們把這個過程交給系統去做就好,layout分為兩個過程,分別是measure過程和layout過程,前者用於遍歷視圖樹中子View的所有尺寸要求。注意measure過程可能會不止調用一遍,因為view可以第一次用不確定的值去處理子View的尺寸要求,然後確定了所有View的要求之後,在第二次遍歷得到一個確定的值。一旦onMeausre方法被回調之後,就可以獲取測量的高度和寬度了。layout過程就是根據測量的尺寸要求,給所有的View包括他的子View進行尺寸分配以及位置的確定。
如果需要在布局過程中判斷View的尺寸是否發生變化,那麼就可以重寫這個方法了。這個方法包含四個形參,分別是舊的寬高和現在的寬高。
當View進行內容的渲染的時候,就會調用此方法。此方法會將View的內容和它的子View在給定的Canvas對象裡面進行渲染。注意,調用此方法之前布局必須先確定下來。
前面提到draw的時候,是在canvas上進行繪畫的,那麼現在來了解一下canvas是什麼,可以干什麼。
canvas是來承受繪畫的內容的,所有在View中呈現的內容,都是先渲染到canvas中,然後再由canvas寫進bitmap裡面,然後就呈現出來了。因此canvas含有大量的繪制圖形,文本的方法,並且還可以進行圖形的旋轉,移動,放大縮小......即所有相關的繪制方法都包含在這裡面了。
public void setBitmap(@Nullable Bitmap bitmap)
public void setBitmap(@Nullable Bitmap bitmap)
public void setBitmap(@Nullable Bitmap bitmap)
public int getDensity()
public void setDensity(int density)
public int save()
public void restore()
移動
public void translate(float dx, float dy)
縮放比例
public void scale(float sx, float sy)
public final void scale(float sx, float sy, float px, float py)
旋轉
public void rotate(float degrees)
public final void rotate(float degrees, float px, float py)
傾斜
public void skew(float sx, float sy)
public void drawRGB(int r, int g, int b)
public void drawARGB(int a, int r, int g, int b)
public void drawColor(@ColorInt int color)
public void drawPaint(@NonNull Paint paint)上面四種方法都可以用於給畫布填充顏色,r,g,b分別表示紅綠藍的比例,a是透明度,也可以直接用Color的常量填充,或者使用畫筆paint直接對畫布噴油漆。
public void drawPoints(@Size(multiple=2) float[] pts, int offset, int count, @NonNull Paint paint)
public void drawPoints(@Size(multiple=2) @NonNull float[] pts, @NonNull Paint paint)
public void drawPoint(float x, float y, @NonNull Paint paint)
public void drawLine(float startX, float startY, float stopX, float stopY, @NonNull Paint paint)
public void drawLines(@Size(min=4,multiple=2) float[] pts, int offset, int count, Paint paint)
public void drawLines(@Size(min=4,multiple=2) @NonNull float[] pts, @NonNull Paint paint)
1、public void drawRect(@NonNull RectF rect, @NonNull Paint paint)
2、public void drawRect(@NonNull Rect r, @NonNull Paint paint)
3、public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint)
4、public void drawOval(@NonNull RectF oval, @NonNull Paint paint)
5、public void drawOval(float left, float top, float right, float bottom, @NonNull Paint paint)
6、public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint)
7、public void drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint)
8、public void drawText(@NonNull char[] text, int index, int count, float x, float y, @NonNull Paint paint)
9、public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint)
10、public void drawText(@NonNull String text, int start, int end, float x, float y, @NonNull Paint paint)
事實上View有兩種Size類型,一種是測量尺寸,一種是布局尺寸。測量尺寸表達的是當前View所期望的尺寸,布局尺寸是父容器結合實際給出的具體尺寸,所以測量尺寸和布局尺寸不一定會相等。獲取測量尺寸的方法,getMeasuredWidth和getMeasuredHeight。獲取布局尺寸的方法:getWidth,getHeight。
padding表示的是間距,即當前View的內容距離邊緣的距離。比如padding=2,表示當前View的內容距離View的邊緣有2個像素點。要注意的是,如果是我們自定義View過程需要Draw渲染內容,一定要處理padding,否則即使你給View設置了padding而不去處理它,會導致View根本起不到padding的效果。獲取padding值得方法,getPaddingLeft,getPaddingRight,getPaddingTop,getPaddingBottom。
Margin表示的是間隔,即當前View相對於父容器的距離,此特征會有父容器進行處理,所以我們自定義View不必關注這個屬性。
首先是attr_xml文件定義自定義屬性:
然後是自定義View
package cn.com.chinaweal.customview; import android.content.Context; import android.content.res.TypedArray; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.view.View; /** * 自定義View注意這裡繼承的是View所以除了系統處理的事件之外任何我們要的東西都要自己寫 * Created by Myy on 2016/8/20. */ public class CustomerView extends View { float strokeWidth=1; int paintColor=Color.RED; Paint paint; Context context; /** * 次構造函數有代碼生成的View調用 * @param context */ public CustomerView(Context context) { super(context); this.context=context; init(); } /** * 此構造函數一般有xml填充的View調用 */ public CustomerView(Context context, AttributeSet attrs) { super(context, attrs); this.context=context; //獲取自定義的主題的相關信息 TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.CustomerView); strokeWidth=typedArray.getDimension(R.styleable.CustomerView_strokeWidth,1); paintColor=typedArray.getColor(R.styleable.CustomerView_paintColor,Color.RED); init(); } private void init() { paint=new Paint(); paint.setColor(paintColor); paint.setStrokeWidth(strokeWidth); } /** * 重寫onMeasure用於得出一個合適的測量尺寸,因為我們繼承的是View,所以這些方法必須自己實現 * @param widthMeasureSpec * @param heightMeasureSpec */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width=getBetterSize(widthMeasureSpec); int height=getBetterSize(heightMeasureSpec); int size=width>height?height:width;//用最小的尺寸作為view的尺寸 setMeasuredDimension(size,size);//一定要調用此方法設置測量尺寸 } /** * 根據測量規則獲取最佳尺寸 * @param measureSpec * @return */ private int getBetterSize(int measureSpec) { int size=200; int mode=MeasureSpec.getMode(measureSpec); int requestSize=MeasureSpec.getSize(measureSpec); switch (mode) { case MeasureSpec.UNSPECIFIED:size=200;break; case MeasureSpec.AT_MOST:size=requestSize;break; case MeasureSpec.EXACTLY:size=requestSize;break; } return size; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //繪畫基本圖形 //記得處理padding int left=getPaddingLeft(); int right=getPaddingRight(); int bottom=getPaddingBottom(); int top=getPaddingTop(); int width=getMeasuredWidth(); int height=getMeasuredHeight(); //繪制圓形 paint.setFlags(Paint.ANTI_ALIAS_FLAG);//這樣畫出來的不是實心的 int radius=Math.min(width-left-right,height-bottom-top)/2;//如果不處理padding會導致padding屬性失效 canvas.drawCircle(width/2,height/2,radius,paint); //繪制直線 canvas.drawLine(left,top,width-left-right,height-bottom-top,paint); //移動畫布 canvas.save(); //將畫布繞著0,0旋轉10度 canvas.rotate(10); //在旋轉後的畫布上畫圖,會發現圖也被選擇了10度 canvas.drawBitmap(BitmapFactory.decodeResource(context.getResources(),R.mipmap.ic_launcher),20,20,paint); paint.setColor(Color.GREEN); //可以發現線也被旋轉了十度 canvas.drawLine(left,top,width-left-right,height-bottom-top,paint); canvas.restore();//恢復狀態 /** * 此時要注意畫筆的顏色的改變,在save和restore之間畫的圖像和線段會隨著resotre的調用而被排除在繪制棧外 * 當調用resotre的時候,需要重新繪制save之前繪畫的東西,所以當系統需要再次刷新View的時候,級下面調用drawRect的時候 * 系統會重新恢復之前的繪畫圖像,由於在resotre之後我將paint的顏色修改了,所以之前的繪畫圖像都會用此paint進行繪制。導致 * 顏色被修改 */ paint.setColor(Color.YELLOW); //此時畫矩形,發現矩形並沒有被旋轉10度,因為resotre將畫布狀態恢復了 //此時會發現之前繪制的灰色的圖像變成了黃色 canvas.drawRect(50,50,100,100,paint); } }
運行效果:
具體解釋請看我的注釋。
不得不說,當不了解一件事情的時候,就會像當然的認為,其很神秘。但是當真正的接觸到了這些神秘的item,就不會有這種感覺了。作為一個android開發新手的我,剛接觸到了V
我們還是用一個小例子來看看自定義View和自定義屬性的使用,帶大家來自己定義一個帶進度的圓形進度條,我們還是先看一下效果吧從上面可以看出,我們可以自定義圓環的顏色,圓環進
最近我家的Wi-Fi好像接入很多不明來歷的Android設備,可是進入路由器查看後,都是類似“androidXXXXX”的設備顯示的
??最近一段時間比較忙,都沒有時間更新博客,今天公司的事情忙完得空,繼續為我的自定義控件系列博客添磚加瓦。本篇博客講解的是標簽自動換行的布局容器,正好前一陣子有個項目中需