自定義View-(2)必須搞懂繪制時用到的每個參數(shù)

  • 這一篇主要是對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,效果圖:
帶邊框炼列,文本居中.png

上面我們看到這個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最終的繪制效果

自定義布局容器.png

為了區(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中對文字繪制的算法肾筐。
你也可以直接參考下圖

text繪制邊界.png

圖片地址: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);

最終的效果圖:

SloganView.png

慣例附上源碼地址:
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)筆者本文的紕漏和錯誤也榄,歡迎大家在下方評論巡莹。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市甜紫,隨后出現(xiàn)的幾起案子降宅,更是在濱河造成了極大的恐慌,老刑警劉巖囚霸,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腰根,死亡現(xiàn)場離奇詭異,居然都是意外死亡拓型,警方通過查閱死者的電腦和手機(jī)额嘿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來劣挫,“玉大人册养,你說我怎么就攤上這事⊙构蹋” “怎么了球拦?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我刘莹,道長阎毅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任点弯,我火速辦了婚禮扇调,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘抢肛。我一直安慰自己狼钮,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布捡絮。 她就那樣靜靜地躺著熬芜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪福稳。 梳的紋絲不亂的頭發(fā)上涎拉,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天,我揣著相機(jī)與錄音的圆,去河邊找鬼鼓拧。 笑死,一個胖子當(dāng)著我的面吹牛越妈,可吹牛的內(nèi)容都是我干的季俩。 我是一名探鬼主播,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼梅掠,長吁一口氣:“原來是場噩夢啊……” “哼酌住!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起阎抒,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤酪我,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后挠蛉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體祭示,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年谴古,在試婚紗的時候發(fā)現(xiàn)自己被綠了质涛。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡掰担,死狀恐怖汇陆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情带饱,我是刑警寧澤毡代,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布阅羹,位于F島的核電站,受9級特大地震影響教寂,放射性物質(zhì)發(fā)生泄漏捏鱼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一酪耕、第九天 我趴在偏房一處隱蔽的房頂上張望导梆。 院中可真熱鬧,春花似錦迂烁、人聲如沸看尼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽藏斩。三九已至,卻和暖如春却盘,著一層夾襖步出監(jiān)牢的瞬間狰域,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工谷炸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留北专,地道東北人。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓旬陡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親语婴。 傳聞我的和親對象是個殘疾皇子描孟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內(nèi)容