FontMetrics以及自定義ImageSpan實(shí)現(xiàn)TextView中文圖混排時(shí)文圖的居中對(duì)齊

這個(gè)標(biāo)題有點(diǎn)長(zhǎng),乍一看這么個(gè)標(biāo)題你可能沒明白啥意思,且聽我慢慢道來(lái)室叉。

公司的項(xiàng)目中新增了一個(gè)“心動(dòng)” 的功能,用戶初次使用時(shí)需要給一個(gè)引導(dǎo)頁(yè)硫惕,就是下面圖中的這個(gè)樣子(這就是做完之后的效果了)茧痕。


Paste_Image.png

在上圖中整體實(shí)現(xiàn)的時(shí)候使用的是popUpWindow。該popupWindow整體使用相對(duì)布局恼除,里面再用一個(gè)相對(duì)布局布局嵌套了三個(gè)TextView:"啊哦踪旷。。。令野。pass" 用一個(gè)TextView舀患,中間灰色的上傳頭像的提示用了一個(gè)TextView,底部“我知道了” 也是一個(gè)TextView气破。上面的左劃示意圖使用above 放在 包含TextView的相對(duì)布局上方构舟,并通過(guò)負(fù)的margin值將它下移并覆蓋在包含TextView相對(duì)布局上。

這個(gè)界面并沒有什么難度堵幽,這里重點(diǎn)說(shuō)的是第一個(gè)TextView中的圖文混排,并讓圖片的橫向中間線與該行文字的橫向中間線對(duì)齊弹澎,也就是說(shuō)朴下,讓文字與那個(gè)?? 圖片的中間在水平方向?qū)R。

1. 圖文混排的方式有哪些苦蒿?

通常我們向TextView中插入圖片實(shí)現(xiàn)圖文混排有如下方式:

  1. 使用drawableLeft等屬性設(shè)置殴胧,這種方式對(duì)應(yīng)的java方法是 setCompoundDrawablesWithIntrinsicBounds(leftDrawble,topDrawable,rightDrawable,bottomDrawable);
  • 使用 SpannableString ,先將圖片轉(zhuǎn)成ImageSpan對(duì)象,然后通過(guò)setSpan插入到SpannableString 中佩迟,最后再將SpannableString通過(guò)setText設(shè)置給TextView团滥。(SpannableString 繼承自CharSquence)
  • 此外,還有一種利用Html.ImageGetter格式化圖片的方式报强。(截止目前為止灸姊,我沒用過(guò)這種方式,如果想了解的話秉溉,可以參考http://wangleyiang.iteye.com/blog/1771439中的第二點(diǎn))

2. 使用SpannableString+ImageSpan怎么實(shí)現(xiàn)圖文混排力惯?

(1). 基本實(shí)現(xiàn)方式

效果圖如下:

Paste_Image.png

實(shí)現(xiàn)方式很簡(jiǎn)單,我們只需要在xml布局文件中定義一個(gè)TextView召嘶,然后在代碼中獲取該TextView并創(chuàng)建一個(gè)含有圖片的SpannableString,并將該SpannableString通過(guò)setText( )設(shè)置給TextView即可父晶。代碼如下:

public class SpannableStringAndImageSpanActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_spannbalestring_imagespan);
        init();
    }

    private void init() {

        TextView tv_test = (TextView) findViewById(R.id.tv_test);
        SpannableString spannableString = new SpannableString("點(diǎn)擊 按鈕有驚喜");

        ImageSpan imageSpan = new ImageSpan(this, R.mipmap.ic_launcher);

        //setSpan插入內(nèi)容的時(shí)候,起始位置不替換弄跌,會(huì)替換起始位置到終止位置間的內(nèi)容甲喝,含終止位置。
        //Spanned.SPAN_EXCLUSIVE_EXCLUSIVE模式用來(lái)控制是否同步設(shè)置新插入的內(nèi)容與start/end 位置的字體樣式铛只,此處沒設(shè)置具體字體埠胖,所以可以隨意設(shè)置
        spannableString.setSpan(imageSpan, 2, 3, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        tv_test.setText(spannableString);
    }
}

xml布局文件中只給了一個(gè)普通的TextView,代碼省略格仲。

  1. 在上面的代碼中押袍,我們通過(guò)ImageSpan的構(gòu)造方法得到了一個(gè)ImageSpan對(duì)象。該構(gòu)造方法中傳入的兩個(gè)參數(shù)分別是上下文和圖片的id凯肋。(imageSpan的構(gòu)造方法還有很多)
  2. SpannbaleString的setSpan方法中谊惭,傳入的四個(gè)參數(shù)分別是 ImageSpan對(duì)象、將ImageSpan插入到的起始位置(start)、將ImageSpan插入到的終點(diǎn)位置(end)圈盔、是否應(yīng)用字體樣式豹芯。具體將ImageSpan對(duì)象插入到哪個(gè)位置,由第二個(gè)和第三個(gè)參數(shù)確定驱敲,插入的時(shí)候會(huì)覆蓋從 start 位置開始(不包含該位置)到終止位置間的內(nèi)容(包含該位置)铁蹈。第四個(gè)參數(shù)是在你插入文本的時(shí)候使用的,用來(lái)控制新插入的文本與已有文本內(nèi)容的字體樣式是否一致的如果你插入的是圖片众眨,這里就可以隨便選擇一種模式握牧。

經(jīng)過(guò)上面雖然實(shí)現(xiàn)了圖文混排,但是娩梨,細(xì)心的你可能發(fā)現(xiàn)了沿腰,這時(shí)候的文字和圖片是基于底部對(duì)齊的(由于圖片的原因,圖片底部與邊框有一點(diǎn)點(diǎn)的間距)狈定。那么如果我想更改對(duì)齊方式怎么辦呢颂龙?

(2). 更改圖片與文本的對(duì)齊方式--ALIGN_BASELINE對(duì)齊

設(shè)置對(duì)齊方式的方法很簡(jiǎn)單,在構(gòu)造ImageSpan對(duì)象的時(shí)候纽什,傳入第三個(gè)參數(shù)ALIGN_BASELINE 即可措嵌,代碼如下:

ImageSpan imageSpan = new ImageSpan(this, R.mipmap.ic_launcher, DynamicDrawableSpan.ALIGN_BASELINE);

設(shè)置對(duì)齊方式為ALIGN_BASELINE后的效果圖:


咦,看著跟上面的圖沒啥區(qū)別奥帧企巢?那么我再把上面沒設(shè)置對(duì)齊方式的圖拉下來(lái)看下:



仔細(xì)對(duì)比下,我們發(fā)現(xiàn)让蕾,設(shè)置對(duì)齊方式之后包斑,圖往上跑了一點(diǎn)點(diǎn)。

其實(shí)涕俗,在ImageSpan 中罗丰,官方只給出了兩中對(duì)齊方式:

  1. 一種是 ALIGN_BOTTOM , 表示與文字內(nèi)容的底部對(duì)齊,如果在構(gòu)造ImageSpan時(shí)沒有傳入對(duì)齊方式再姑,那么默認(rèn)就是這種底部對(duì)齊萌抵。
  2. 另一中就是 ALIGN_BASELINE, 表示與文字內(nèi)容的基線對(duì)齊。那么元镀,你可能會(huì)問我基線是啥绍填?請(qǐng)繼續(xù)往下看:

3. Paint.FontMetrics 是啥?

(1). Paint.FontMetrics基本介紹

要說(shuō)基線呢栖疑,我們先了解這個(gè)Paint.FontMetircs, 官方對(duì)該類的解釋是:Class that describes the various metrics for a font at a given text size., 意思是說(shuō)讨永,這玩意兒是繪制文本內(nèi)容時(shí)存儲(chǔ)該文本內(nèi)容位置信息的一個(gè)類。這個(gè)類中有如下五個(gè)字段:

Paste_Image.png

(2). BaseLine 基線到底是啥遇革?

上圖中這5個(gè)字段除了leading 外卿闹,其他四個(gè)都是相對(duì)于 基線BaseLine來(lái)確定的揭糕,那么,到底啥是基線锻霎?著角?先來(lái)看一張圖:

Paste_Image.png

如上圖,標(biāo)準(zhǔn)的英文書寫是基于四線三格旋恼,其中吏口,我們書寫英文的時(shí)候,都是以第三條線為基準(zhǔn)冰更,也就是說(shuō)产徊,基線就是這個(gè)四線三格中的第三條線!蜀细!

(3). Paint.FontMetrics中字段的含義及示意圖

官方文檔中對(duì)這幾個(gè)字段的解釋很簡(jiǎn)單囚痴,但理解起來(lái)挺費(fèi)勁,直接上圖审葬,圖中的標(biāo)注都是跑代碼之后確定的,如果有不準(zhǔn)確的地方奕谭,歡迎指正:

Paste_Image.png

根據(jù)上圖可知:

  • ascent
    文字內(nèi)容的頂部到基線的距離涣觉。即 ascent=文字內(nèi)容頂部Y坐標(biāo) - 基線Y坐標(biāo)。由于android中坐標(biāo)系是 右下為正血柳,所以得到的ascent實(shí)際是一個(gè)負(fù)數(shù)官册。

  • descent
    文字內(nèi)容的底部到基線的距離。即 descent=文字內(nèi)容底部Y坐標(biāo) - 基線Y坐標(biāo)难捌。

  • ** 基線 **
    在圖中膝宁,基線的坐標(biāo)用Y表示,在ImageSpan父類的 draw( ) 中根吁,會(huì)傳入一個(gè) float Y ,就是這個(gè)基線的坐標(biāo)员淫。實(shí)際上,基線的Y坐標(biāo)=文字內(nèi)容中間線Y坐標(biāo)+1/2 (文字內(nèi)容高度)

  • top
    對(duì)應(yīng)圖中 文字所在行的top 坐標(biāo)

  • bottom
    對(duì)應(yīng)圖中 文字所在行的bottom 坐標(biāo)
    需要注意:如果設(shè)置了行間距击敌,且文本內(nèi)容產(chǎn)生了換行介返,那么這個(gè)bottom 也會(huì)將行間距包裹。所以沃斤, 圖中藍(lán)色的文字內(nèi)容中間線的Y軸坐標(biāo)并不一定等于 (bottom+top)/2

4 自定義ImageSpan實(shí)現(xiàn)文字與圖片居中對(duì)齊

好了圣蝎,前面說(shuō)了那么多,終于進(jìn)入正題了衡瓶。徘公。。
在上面的2 SpannableString+ImageSpan實(shí)現(xiàn)圖文混排中哮针,我們已經(jīng)知道官方并沒有給出文字與圖片居中對(duì)齊的模式,所以需要我們自定義关面。

關(guān)于自定義ImageSpan的分析坦袍,已經(jīng)有前輩講解過(guò)了,此處不再贅述缭裆,請(qǐng)參考http://blog.csdn.net/gaoyucindy/article/details/39473135键闺。但是,按照該文章中的代碼實(shí)現(xiàn)的時(shí)候澈驼,有個(gè)問題就是:如果給TextView設(shè)置了行間距辛燥,且文本產(chǎn)生了換行,那么就無(wú)法對(duì)齊了7炱洹挎塌!

那么,設(shè)置了行間距之后内边,該如何實(shí)現(xiàn)文本和圖片的居中對(duì)齊呢榴都?也有前輩分析過(guò)了,請(qǐng)看:http://www.cnblogs.com/withwind318/p/5541267.html , 但是漠其,這篇文章中的實(shí)現(xiàn)方式?jīng)]有重寫 getSize( ) 方法嘴高,所以也有一個(gè)問題:文本和圖片并不是在TextView的居中位置,而且如果圖片高于文本的話和屎,圖片會(huì)顯示不全K┩浴!如下圖:

Paste_Image.png

參考了那么多了柴信,終于該給出我的終極方案了L灼 !
根據(jù)上面鏈接中兩位前輩的分析随常,其實(shí)我們自定義的時(shí)候潜沦,需要做的事情是 獲取文本內(nèi)容的中間線以及圖片的中間線,然后獲取兩者差值绪氛,然后在draw方法中繪制圖片時(shí)將差值作為canvas.translate(x, transY) 中的transY唆鸡;同時(shí)要重寫 getSize( )。這樣最終實(shí)現(xiàn)的效果是枣察,不論是否設(shè)置行間距喇闸,不論圖片大于文本還是文本大于圖片,都能實(shí)現(xiàn)文本和圖片的居中對(duì)齊询件!

看最終效果圖:

Paste_Image.png

上代碼:

public class SpannableStringAndImageSpanActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_spannbalestring_imagespan);
        init();
    }

    private void init() {

        TextView tv_test = (TextView) findViewById(R.id.tv_test);
        SpannableString spannableString = new SpannableString("點(diǎn)擊 按鈕有驚喜");

        //調(diào)用自定義的imageSpan,實(shí)現(xiàn)文字與圖片的橫向居中對(duì)齊
        CustomImageSpan imageSpan = new CustomImageSpan(this, R.mipmap.ic_launcher, 2);

        //setSpan插入內(nèi)容的時(shí)候燃乍,起始位置不替換,會(huì)替換起始位置到終止位置間的內(nèi)容宛琅,含終止位置刻蟹。
        //Spanned.SPAN_EXCLUSIVE_EXCLUSIVE模式用來(lái)控制是否同步設(shè)置新插入的內(nèi)容與start/end 位置的字體樣式,此處沒設(shè)置具體字體嘿辟,所以可以隨意設(shè)置
        spannableString.setSpan(imageSpan, 2, 3, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        tv_test.setText(spannableString);
    }

    /**
     * 自定義imageSpan實(shí)現(xiàn)圖片與文字的居中對(duì)齊
     */
    class CustomImageSpan extends ImageSpan {

        //自定義對(duì)齊方式--與文字中間線對(duì)齊
        private int ALIGN_FONTCENTER = 2;

        public CustomImageSpan(Context context, int resourceId) {
            super(context, resourceId);
        }

        public CustomImageSpan(Context context, int resourceId, int verticalAlignment) {
            super(context, resourceId, verticalAlignment);
        }

        @Override
        public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom,
                         Paint paint) {

            //draw 方法是重寫的ImageSpan父類 DynamicDrawableSpan中的方法舆瘪,在DynamicDrawableSpan類中片效,雖有g(shù)etCachedDrawable(),
            // 但是私有的英古,不能被調(diào)用淀衣,所以調(diào)用ImageSpan中的getrawable()方法,該方法中 會(huì)根據(jù)傳入的drawable ID 召调,獲取該id對(duì)應(yīng)的
            // drawable的流對(duì)象膨桥,并最終獲取drawable對(duì)象
            Drawable drawable = getDrawable(); //調(diào)用imageSpan中的方法獲取drawable對(duì)象
            canvas.save();

            //獲取畫筆的文字繪制時(shí)的具體測(cè)量數(shù)據(jù)
            Paint.FontMetricsInt fm = paint.getFontMetricsInt();

            //系統(tǒng)原有方法,默認(rèn)是Bottom模式)
            int transY = bottom - drawable.getBounds().bottom;
            if (mVerticalAlignment == ALIGN_BASELINE) {
                transY -= fm.descent;
            } else if (mVerticalAlignment == ALIGN_FONTCENTER) {    //此處加入判斷唠叛, 如果是自定義的居中對(duì)齊
                //與文字的中間線對(duì)齊(這種方式不論是否設(shè)置行間距都能保障文字的中間線和圖片的中間線是對(duì)齊的)
                // y+ascent得到文字內(nèi)容的頂部坐標(biāo)只嚣,y+descent得到文字的底部坐標(biāo),(頂部坐標(biāo)+底部坐標(biāo))/2=文字內(nèi)容中間線坐標(biāo)
                transY = ((y + fm.descent) + (y + fm.ascent)) / 2 - drawable.getBounds().bottom / 2;
            }

            canvas.translate(x, transY);
            drawable.draw(canvas);
            canvas.restore();
        }

        /**
         * 重寫getSize方法艺沼,只有重寫該方法后册舞,才能保證不論是圖片大于文字還是文字大于圖片,都能實(shí)現(xiàn)中間對(duì)齊
         */
        public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
            Drawable d = getDrawable();
            Rect rect = d.getBounds();
            if (fm != null) {
                Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
                int fontHeight = fmPaint.bottom - fmPaint.top;
                int drHeight = rect.bottom - rect.top;

                int top = drHeight / 2 - fontHeight / 4;
                int bottom = drHeight / 2 + fontHeight / 4;

                fm.ascent = -bottom;
                fm.top = -bottom;
                fm.bottom = top;
                fm.descent = top;
            }
            return rect.right;
        }
    }
}

xml布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

    <TextView
        android:id="@+id/tv_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#fffaa3"
        android:lineSpacingExtra="@dimen/dp100"
        android:textSize="16sp"/>

</LinearLayout>

上面的已經(jīng)是完整代碼了障般,如果想直接下載運(yùn)行调鲸,請(qǐng)到gitHub下載:https://github.com/CnPeng/CrazyAndroid。該倉(cāng)庫(kù)中的b_01_spannableString_ImageSpan 對(duì)應(yīng)該文中的內(nèi)容


寫在最后挽荡,最近項(xiàng)目太緊了藐石,過(guò)了年一直在加班。這次的總結(jié)也很倉(cāng)促徐伐,本來(lái)想寫的更細(xì)一些,并且也想把SpannableString的使用完整總結(jié)募狂,but 時(shí)間太緊了办素,先這樣吧,后面時(shí)間充足了再修正吧祸穷!

歡迎各位指正文中錯(cuò)誤的地方性穿,一起交流,一起進(jìn)步雷滚!

參考鏈接:
http://wangleyiang.iteye.com/blog/1771439
http://blog.csdn.net/gaoyucindy/article/details/39473135
http://www.cnblogs.com/withwind318/p/5541267.html
http://stackoverflow.com/questions/27631736/meaning-of-top-ascent-baseline-descent-bottom-and-leading-in-androids-font
https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B99%5DDrawText.md

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末需曾,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子祈远,更是在濱河造成了極大的恐慌呆万,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,576評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件车份,死亡現(xiàn)場(chǎng)離奇詭異谋减,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)扫沼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門出爹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)庄吼,“玉大人,你說(shuō)我怎么就攤上這事严就∽苎埃” “怎么了?”我有些...
    開封第一講書人閱讀 168,017評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵梢为,是天一觀的道長(zhǎng)渐行。 經(jīng)常有香客問我,道長(zhǎng)抖誉,這世上最難降的妖魔是什么殊轴? 我笑而不...
    開封第一講書人閱讀 59,626評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮袒炉,結(jié)果婚禮上旁理,老公的妹妹穿的比我還像新娘。我一直安慰自己我磁,他們只是感情好孽文,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著夺艰,像睡著了一般芋哭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上郁副,一...
    開封第一講書人閱讀 52,255評(píng)論 1 308
  • 那天减牺,我揣著相機(jī)與錄音,去河邊找鬼存谎。 笑死拔疚,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的既荚。 我是一名探鬼主播稚失,決...
    沈念sama閱讀 40,825評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼恰聘!你這毒婦竟也來(lái)了句各?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,729評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤晴叨,失蹤者是張志新(化名)和其女友劉穎凿宾,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體兼蕊,經(jīng)...
    沈念sama閱讀 46,271評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡菌湃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了遍略。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惧所。...
    茶點(diǎn)故事閱讀 40,498評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡骤坐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出下愈,到底是詐尸還是另有隱情纽绍,我是刑警寧澤,帶...
    沈念sama閱讀 36,183評(píng)論 5 350
  • 正文 年R本政府宣布势似,位于F島的核電站拌夏,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏履因。R本人自食惡果不足惜障簿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望栅迄。 院中可真熱鬧站故,春花似錦、人聲如沸毅舆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)憋活。三九已至岂津,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間悦即,已是汗流浹背吮成。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留辜梳,地道東北人粱甫。 一個(gè)月前我還...
    沈念sama閱讀 48,906評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像冗美,于是被迫代替她去往敵國(guó)和親魔种。 傳聞我的和親對(duì)象是個(gè)殘疾皇子析二,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評(píng)論 2 359

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