- 這一篇主要是對View的onDraw方法中對各種繪制參數(shù)的選擇哗蜈,進(jìn)行細(xì)節(jié)的學(xué)習(xí)前标。
主要會涉及以下幾個內(nèi)容:- 1.獲取尺寸參數(shù)時,父類在onLayout()中對于子View的布局尺寸的干涉
- 2.getWidth() ,getMeasureWidth()本質(zhì)上的區(qū)別和如何選擇
- 3.繪制文字時距潘,文字居中位置的算法
- 我們最終實(shí)現(xiàn)一個類似于TextView的SloganView,效果圖:
上面我們看到這個View帶邊框,中間文本居中音比,這樣一個View會很方便我們做驗證俭尖。
開始之前,這里提一下自定義View的實(shí)現(xiàn)步驟。
→確認(rèn)自定義屬性
→在res/attrs 文件中聲明 自定義屬性
→在View的構(gòu)造器中獲取自定義屬性并保存
→在onMeasure中確認(rèn)AT_MOST模式時的View的測量大小
→在onDraw中根據(jù)保存的自定義屬性和獲取的View尺寸進(jìn)行繪制
- 下面我們來看看實(shí)現(xiàn)中的細(xì)節(jié)
1.構(gòu)造器中Dimension值的獲取
(具體獲取所有屬性直接參照TextView的源碼即可稽犁,如果你懶你怎么告別伸手黨)
獲取自定義的Dimension參數(shù)和默認(rèn)值的設(shè)置焰望,經(jīng)常會用到下面三個方法:
getDimension()
看完源碼具體計算,再加以我蹩腳的英語理解為 合成一個像素值,直接舍去小數(shù)
getDimensionPixelOffset()
和上面一樣返回一個像素值缭付,但是對小數(shù)會進(jìn)行四舍五入 差別不大
getDimensionPixelSize()
在TypedArray和Resources中都有這三個函數(shù)柿估,功能類似循未,TypedArray中的函數(shù)是獲取自定義屬性的陷猫,Resources中的函數(shù)是獲取android預(yù)置屬性的
注:Resources 最里面還是用的TypedArray 來查找和獲取 Res 屬性值的。
2.onMeasure根據(jù)測量模式進(jìn)行處理
設(shè)置寬高默認(rèn)值
//默認(rèn)尺寸大小 單位
pxprivate int normalWidth = 260;
private int normalHeight = 140;
將上面的兩個默認(rèn)值作為AT_MOST模式的測量參數(shù)進(jìn)行賦值的妖,對這里有疑問的同學(xué)可以看我的上一篇文字自定義View-(1)先搞懂測量的所有細(xì)節(jié),再回來這里你就清楚了绣檬。
private int mWidthSize;
private int mHeightSize;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
L.printD(TAG, "onMeasure");
L.printD(TAG,"getMeasuredWidth="+getMeasuredWidth());
L.printD(TAG,"getMeasuredHeight="+getMeasuredHeight());
mWidthSize = MeasureSpec.getSize(widthMeasureSpec);
mHeightSize = MeasureSpec.getSize(heightMeasureSpec);
int widMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widMode == MeasureSpec.AT_MOST) {
mWidthSize = normalWidth;
}
if (heightMode == MeasureSpec.AT_MOST) {
mHeightSize = normalHeight;
}
setMeasuredDimension(mWidthSize, mHeightSize);
}
3.onDraw根據(jù)屬性和尺寸參數(shù)進(jìn)行繪制
可以看到再邏輯代碼中有很多的打印日志,是稍后我們驗證的時候需要用到的嫂粟。
個人非常喜歡用日志娇未,無論是輸入輸出日志,邏輯轉(zhuǎn)折或者交互的開始結(jié)束星虹,我覺得打上日志零抬,檢查時清晰快捷,錯誤疏漏一目了然宽涌。工作開發(fā)中幾乎沒用過debug,如果需要用到debug那我一定是被逼上絕路了平夜。
當(dāng)然你最好自己寫個日志打印類,提供開啟關(guān)閉卸亮,也減少代碼量忽妒。不然整個項目的日志沒有關(guān)閉,會增加不必要的緩存和降低代碼執(zhí)行效率兼贸。
我們先為View繪制一個邊框,使用自定是屬性對畫筆進(jìn)行設(shè)置
paint = new Paint();
paint.setColor(strokeColor);
paint.setStrokeWidth(strokeWidth);
paint.setStyle(Paint.Style.STROKE);
Canvas為我們提供了一個直接可以繪制矩形的方法
public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint)
用個四個參數(shù)來確定繪制起點(diǎn)和繪制大小段直,前兩個我們在不考慮有偏移量時一般都是0,0 就是從屏幕左上角那個點(diǎn)溶诞。
后面兩個用來計算繪制大小的參數(shù)我們有兩個選擇鸯檬,就是上文我們提到的getWidth()和getMeasureWidth(),那我們該如何選擇呢?他們的實(shí)際效果又是什么樣呢螺垢?
第一遍進(jìn)去看完源碼我得到如下解釋(如果你想知道為什么喧务,那就去查源碼)
getWidth()返回mRight - mLeft,他們是在layout()中調(diào)用setFrame()賦值.
計算的是View在父窗口內(nèi)被顯示的尺寸,對padding甩苛、margin及其他布局參數(shù)處理蹂楣。
最后保存為子View布局大小參數(shù),確定子View顯示尺寸大小讯蒲。
getMeasureWidth() 則是在setMeasuredDimension()調(diào)用后賦值痊土,用于確認(rèn)View繪制的實(shí)際大小。
在layout()中 我們還會發(fā)現(xiàn) onMeasure 比 onLayout先調(diào)用(有疑問請直接進(jìn)View的Layout()方法中查看)墨林。
根據(jù)上面的理解我們只能知道赁酝,getWidth獲取的是View再屏幕上顯示的寬度犯祠,getHeight同理。
兩中方式獲取的尺寸存在本質(zhì)上是不同的酌呆,但是如果單純在onDraw中用日志輸出衡载,會發(fā)現(xiàn)他們總是相等的,view的顯示尺寸總是等于繪制尺寸隙袁,這里我外層是LinearLayout痰娱,他在layout方法中將測量大小直接賦值給了布局尺寸(點(diǎn)進(jìn)源碼一看便知),這里我們反向驗證菩收。如何讓getWidth() 和getMeasureWidth()獲取的值不相等?
為了更直觀的了解 梨睁,我們寫一個父布局容器來重寫onLayout方法,對子類的布局大小進(jìn)行設(shè)置娜饵,最后日志輸出兩種方法的返回值坡贺。
- 自定義一個布局容器 CustomViewGroup
代碼如下:
public class CustomViewGroup1 extends ViewGroup {
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
//我們這里什么都不做,就強(qiáng)行設(shè)置子View的寬高為 300箱舞,300
for(int i = 0;i<count;i++){
View childView = getChildAt(i);
L.printD(TAG,"childView - me width=="+childView.getMeasuredWidth());
L.printD(TAG,"cheildView - me Height = "+childView.getMeasuredHeight());
childView.layout(0,0,300,300);
}
}
在xml中使用我們的布局容器
...
<com.zsw.testmodel.ui.act.customview.CustomViewGroup1
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/testmodelblue"
android:id="@+id/customViewGroup1">
<com.zsw.testmodel.ui.act.customview.CustomView1
android:layout_width="wrap_content"
android:layout_height="wrap_content"
custom:cv1_text="D-B-A-A"
custom:cv1_textColor="@color/red"
custom:cv1_strokeColor="@color/red"
custom:cv1_textSize="30sp"
custom:cv1_strokeWidth="5dp"
android:id="@+id/customView1" />
</com.zsw.testmodel.ui.act.customview.CustomViewGroup1>
...
android studio 給我們提供了很好的自定義view xml預(yù)覽 我們看看UI最終的繪制效果
為了區(qū)分我給View加了背景顏色
深藍(lán)色是我們的CustomViewGroup
灰色區(qū)是CustomView顯示區(qū)域
紅色邊框是我們在CustomView上繪制的圖形區(qū)域
可以看到紅色邊框比View小很多遍坟,當(dāng)然如果我們在onMeasure中將默認(rèn)值normalWidth 改為大于300 ,紅色邊框的繪制區(qū)域會超出View
再看看日志打印
//父類測量子類傳入onMeasure的尺寸
CustomView1-->>onMeasure
CustomView1-->>getMeasuredWidth=1280
CustomView1-->>getMeasuredHeight=2108
父類 onLayout方法中獲取子類自己重寫測量后的尺寸晴股,并設(shè)置布局尺寸為300愿伴,300
CustomViewGroup1-->>childView - me width==260
CustomViewGroup1-->>cheildView - me Height = 140
CustomView1-->>onLayout
CustomView1-->>onDraw----
//最終用于繪制的布局尺寸和測量尺寸
CustomView1-->>getWidth=300
CustomView1-->>getHeight=300
CustomView1-->>getMeasuredWidth=260
CustomView1-->>getMeasuredHeight=140
由此,得知這里繪制應(yīng)該使用getMeasureWidth()獲取的尺寸來計算队魏,保證圖形和文字能夠完整繪制出來公般。如果getWidth 小于 getMeasureWidth()的大小,那么應(yīng)該是xml中我們給的布局尺寸不夠胡桨。
舉個小例子官帘,在xml中寫了一個TextView 字體很大,行數(shù)很多昧谊,屏幕上只顯示了半行字刽虹。
- 回到我們繪制矩形的步驟,使用Measure的尺寸來繪制:
canvas.drawRect( 0, 0 , getMeasuredWidth() , getMeasuredHeight(), paint);
上面邊框我們已經(jīng)搞定呢诬,接下來開始繪制文字
canvas為我們提供了快速繪制文字的方法
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint)
還提供了一個獲取text文本大小的方法
public void getTextBounds(String text, int start, int end, Rect bounds)
注釋是這樣解釋的
* @param text 繪制的文本
* @param x x軸繪制起點(diǎn)坐標(biāo)
* @param y 基準(zhǔn)線baseline的起點(diǎn)坐標(biāo)(下面會解釋baseline是什么)
* @param paint 畫筆
這里的X起點(diǎn)很好計算涌哲,(getMeasuredWidth()-rect.width())/2 左右邊距相同即水平居中。
首先要記咨辛: baseline 是基準(zhǔn)線阀圾!基準(zhǔn)線!下面的驗證都是圍繞它來進(jìn)行的狗唉。
可以理解為在單獨(dú)繪制文字時,根據(jù)局部文本尺寸建立新的坐標(biāo)系 baseLineY = 0;
下面是對中心y坐標(biāo)的計算初烘,我也是翻了很多老司機(jī)的博客,最后根據(jù)自己實(shí)際測試代碼來學(xué)習(xí)的.
這里給上最一篇我認(rèn)為分析【字體繪制位置的計算】的最清晰的博客 http://blog.csdn.net/wan778899/article/details/51460849
當(dāng)然你也可以像筆者一樣把FontMetrics中的幾個邊界線繪制一遍,再推敲計算公式
上面邊線的繪制是參照TextView源碼中的BoringLayout中對文字繪制的算法肾筐。
你也可以直接參考下圖
圖片地址:http://blog.csdn.net/u014702653/article/details/51985821
最后我得到的算法如下:
先取得View繪制高度的一半哆料,減去文本得top到view的邊距,再下降文本高度得一半 得到baseline
int baseLine = getMeasuredHeight()/2 - ~(fontMetrics.bottom - fontMetrics.top)+rect.height()/2;
- 可以看到上面做了非常多的驗證和分析吗铐,最終我們得到正確的baseline的值东亦,下面繪制Text
paint.reset();
paint.setColor(textColor);
paint.setTextSize(textSize);
bounds = new Rect();
paint.getTextBounds(text,0,text.length(),bounds);
Paint.FontMetricsInt fontMetrics = new Paint.FontMetricsInt();
int baseLine = getMeasuredHeight()/2 - ~(fontMetrics.bottom - fontMetrics.top)+bounds.height()/2;
canvas.drawText(text,(getMeasuredWidth()-bounds.width())/2,baseLine,paint);
最終的效果圖:
慣例附上源碼地址:
https://github.com/HarkBen/RainBowLibrary/blob/master/simpleDemo/src/main/java/com/zsw/testmodel/ui/act/customview/SloganView1.java
這一段自定義View的學(xué)習(xí)就暫時結(jié)束了,我們來總結(jié)一下唬渗,梳理一下重要內(nèi)容典阵。
- 1.onMeasure,View繪制尺寸測量
- 2.onLayout谣妻,父類對子類顯示尺寸的設(shè)置
- 3.onDraw,根據(jù)對尺寸的選擇 + 屬性的獲取萄喳,進(jìn)行繪制
- 4.有關(guān)文本繪制時卒稳,對文本位置的計算
到這里蹋半,有關(guān)自定義為最重要的三個方法,onMeasure,onLayout,onDraw的深度分析和學(xué)習(xí)就結(jié)束了充坑。
如果能對大家有所幫助那我真的是太高興了减江,,同處開源大環(huán)境下捻爷,我也是看著知名博主們的文章長大的辈灼,哈哈。如果有疑問或者是發(fā)現(xiàn)筆者本文的紕漏和錯誤也榄,歡迎大家在下方評論巡莹。