這個(gè)標(biāo)題有點(diǎn)長(zhǎng),乍一看這么個(gè)標(biāo)題你可能沒明白啥意思,且聽我慢慢道來(lái)室叉。
公司的項(xiàng)目中新增了一個(gè)“心動(dòng)” 的功能,用戶初次使用時(shí)需要給一個(gè)引導(dǎo)頁(yè)硫惕,就是下面圖中的這個(gè)樣子(這就是做完之后的效果了)茧痕。
在上圖中整體實(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)圖文混排有如下方式:
- 使用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)方式
效果圖如下:
實(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,代碼省略格仲。
- 在上面的代碼中押袍,我們通過(guò)ImageSpan的構(gòu)造方法得到了一個(gè)ImageSpan對(duì)象。該構(gòu)造方法中傳入的兩個(gè)參數(shù)分別是上下文和圖片的id凯肋。(imageSpan的構(gòu)造方法還有很多)
- 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ì)齊方式:
- 一種是 ALIGN_BOTTOM , 表示與文字內(nèi)容的底部對(duì)齊,如果在構(gòu)造ImageSpan時(shí)沒有傳入對(duì)齊方式再姑,那么默認(rèn)就是這種底部對(duì)齊萌抵。
- 另一中就是 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è)字段:
(2). BaseLine 基線到底是啥遇革?
上圖中這5個(gè)字段除了leading 外卿闹,其他四個(gè)都是相對(duì)于 基線BaseLine來(lái)確定的揭糕,那么,到底啥是基線锻霎?著角?先來(lái)看一張圖:
如上圖,標(biāo)準(zhǔn)的英文書寫是基于四線三格旋恼,其中吏口,我們書寫英文的時(shí)候,都是以第三條線為基準(zhǔn)冰更,也就是說(shuō)产徊,基線就是這個(gè)四線三格中的第三條線!蜀细!
(3). Paint.FontMetrics中字段的含義及示意圖
官方文檔中對(duì)這幾個(gè)字段的解釋很簡(jiǎn)單囚痴,但理解起來(lái)挺費(fèi)勁,直接上圖审葬,圖中的標(biāo)注都是跑代碼之后確定的,如果有不準(zhǔn)確的地方奕谭,歡迎指正:
根據(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┩浴!如下圖:
參考了那么多了柴信,終于該給出我的終極方案了L灼 !
根據(jù)上面鏈接中兩位前輩的分析随常,其實(shí)我們自定義的時(shí)候潜沦,需要做的事情是 獲取文本內(nèi)容的中間線以及圖片的中間線,然后獲取兩者差值绪氛,然后在draw方法中繪制圖片時(shí)將差值作為canvas.translate(x, transY) 中的transY唆鸡;同時(shí)要重寫 getSize( )。這樣最終實(shí)現(xiàn)的效果是枣察,不論是否設(shè)置行間距喇闸,不論圖片大于文本還是文本大于圖片,都能實(shí)現(xiàn)文本和圖片的居中對(duì)齊询件!
看最終效果圖:
上代碼:
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