在平时我们做项目中,或许有要对一张图片或者某一个东西进行文字和图片说明,这时候要求排版美观,所以会出现文字和图片混排的情况,如图:
这种情况就是上下两个文字说明是连续在一起的,这就要求我们计算上面的文字说明怎么和下面的文字说明连贯结合在一起呢,这就要求我们进行计算了,下面给出代码,代码中也有详细的注释,原理也很简单。
因为算是比较简单,直接就在activity中去计算了:
package com.example.test;
import android.app.Activity;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.TextView;
public class MainActivity extends Activity {
boolean imageMeasured = false;
TextView tv_right;
TextView tv_bottom;
static final String text = "叶凡:小说主角,与众老同学在泰山聚会时一同被九龙拉棺带离地球," +
"进入北斗星域,得知自己是荒古圣叶凡 叶凡体。历险禁地,习得源术,斗圣地世家,战太古生物," +
"重组天庭,叶凡辗转四方得到许多际遇和挑战,功力激增,眼界也渐渐开阔。一个浩大的仙侠世界," +
"就以他的视角在读者面前展开。姬紫月:姬家小姐,出场年龄十七岁。被叶凡劫持一同经历青铜古殿历险," +
"依靠碎裂的神光遁符解除禁制,反过来挟持叶凡一同进入太玄派寻找秘术。" +
"在叶凡逃离太玄后姬紫月在孔雀王之乱中被华云飞追杀,又与叶凡[2]相遇,被叶凡护送回姬家" +
",渐渐对叶凡产生微妙感情。后成为叶凡的妻子,千载后于飞仙星成仙,在叶凡也进入仙路后再见庞博:" +
"叶凡大学时最好的朋友,壮硕魁伟,直率义气。到达北斗星域后因服用了圣果被灵墟洞天作为仙苗," +
"在青帝坟墓处为青帝十九代孙附体离去,肉身被锤炼至四极境界。后叶凡与黑皇镇压老妖神识," +
"庞博重新掌控自己身躯,取得妖帝古经和老妖本体祭炼成的青莲法宝,习得妖帝九斩和天妖八式," +
"但仍伪装成老妖留在妖族。出关后找上叶凡,多次与他共进退。星空古路开启后由此离开北斗," +
"被叶凡从妖皇墓中救出,得叶凡授予者字秘、一气化三清,与叶凡同闯试炼古路,一起建设天庭";
// 屏幕的高度
int screen;
// 总共可以放多少个字
int count = 0;
// textView全部字符的宽度
float textTotal.0f;
// textView一个字的宽度
float text.0f;
Paint paint = new Paint();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv_right = (TextView) findViewById(R.id.test_tv_right);
tv_bottom = (TextView) findViewById(R.id.test_tv_bottom);
final ImageView imageView = (ImageView) findViewById(R.id.test_image);
imageView.setImageResource(R.drawable.ee);
screen1.0" encoding="utf-8"?>
android:layout_
android:orientation="vertical" >
android:layout_ >
android:layout_
android:layout_
android:scaleType="fitXY" />
android:layout_
android:layout_
android:layout_toRightOf="@id/test_image"
android:gravity="fill_horizontal"
android:paddingLeft="7dp"
android:textSize="16sp" />
android:layout_
android:layout_
android:layout_below="@id/test_image"
android:gravity="fill_horizontal"
android:textSize="16sp" />
代码很少,原理也很简单,后来发现这种做法在大部分手机运行是完美的,但是少部分手机还是有点问题。是什么问题呢,是在我们测量textView的长度的是,因为是我们刚刚进行setText,然后马上进行测量,这样得到的结果是不正确的,所以大家可以优化一下。温馨提示,当我们setText之后,可以延时一些时间再去测量,这样获取的值就是挣钱的了,当然那个延迟的时间很短50毫秒就可以了,因为我们要相信textView的绘制速度还是很快的。
Android 文字环绕 图文混排 支持Span折叠
先直接上效果图
上图为实现目标,实现了Android图文混排,文字环绕,支持Span的识别,表情的嵌入,支持文字substr的设置等。
由于项目中需要用到图文混排技术,在此稍微研究了两天,出来一个效果还算不错的东西
图文混排技术,在不少Android应用中都已经实现,说穿了其实就是两个TextView加一个ImageView的布局罢了,代码里面实现下String的剪切就可以了,不过我这里的这个除了要实现混排效果外,还要支持Span,支持表情等,这就有点麻烦了。下面慢慢分解。先贴出RichTextImageView的布局。
android:layout_
android:orientation="vertical"
android:id="@+id/richview" >
android:layout_
android:layout_ >
android:layout_
android:layout_
android:layout_weight="0"
android:visibility="gone" >
android:layout_
android:layout_
android:background="@drawable/preview_back"
android:cropToPadding="true"
android:scaleType="centerCrop"
android:src="@drawable/preview_back_small" />
android:layout_
android:layout_
android:layout_alignBottom="@+id/preimage_statues"
android:layout_alignRight="@+id/preimage_statues"
android:layout_marginBottom="9dip"
android:layout_marginRight="7dip" />
android:layout_
android:layout_
android:layout_weight="1"/>
android:layout_
android:layout_
android:layout_weight="0"
android:visibility="gone" >
android:layout_
android:layout_
android:background="@drawable/preview_back"
android:cropToPadding="true"
android:scaleType="centerCrop"
android:src="@drawable/preview_back_small" />
android:layout_
android:layout_
android:layout_alignBottom="@+id/preimage_statues"
android:layout_alignRight="@+id/preimage_statues"
android:layout_marginBottom="9dip"
android:layout_marginRight="7dip" />
android:layout_
android:layout_/>
布局中的RichTextView是另外封装的一个实现Span的TextView,就是实现效果图中表情啊,@啊,#话题# 之类的,了解Span的都懂的,就不细说了。读者研究这个图文混排的时候,可以直接用TextView替代。
RichTextImageView 即本文中的图文混排的View。继承于LinearLayout,该类实现的关键代码如下:
public void setText(String t) {
mTopText.setText("");
mBottomText.setText("");
mTopText.setVisibility(View.INVISIBLE);
mBottomText.setVisibility(View.GONE);
bRequestBottomLayout = true;
if (t == null)
t = "";
// Log.e("setText", t);
mTextContent = t;
mTopText.setText(mTextContent);
字体大小Layout();
}
public void setImage(int id) {
mImageContent = id;
mPreviewImage.setImageResource(id);
mPreview.setVisibility(View.VISIBLE);
if (!Util.isStringEmpty(mTextContent))
setText(mTextContent);
}
private void iniViews() {
mLinearLayout = (LinearLayout) findViewById(R.id.linearLayout1);
mTopText = (RichTextView) findViewById(R.id.lefttext);
mBottomText = (RichTextView) findViewById(R.id.bottomtext);
if(IMAGE_LOCATION == IMAGE_LOCATION_RIGHT){
mPreview = (RelativeLayout) findViewById(R.id.layout_preimage_isgif_right);
mPreviewImage = (ImageView) findViewById(R.id.preimage_statues_right);
mPreviewImageIsGif = (ImageView) findViewById(R.id.preimage_isgif_right);
}else if(IMAGE_LOCATION == IMAGE_LOCATION_LEFT){
mPreview = (RelativeLayout) findViewById(R.id.layout_preimage_isgif_left);
mPreviewImage = (ImageView) findViewById(R.id.preimage_statues_left);
mPreviewImageIsGif = (ImageView) findViewById(R.id.preimage_isgif_left);
}
}
public void setImageLocation(int l){
if(l != IMAGE_LOCATION_RIGHT && l != IMAGE_LOCATION_LEFT)
return;
else{
IMAGE_LOCATION = l;
mPreview.setVisibility(View.GONE);
iniViews();
setImage(mImageContent);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// TODO Auto-generated method stub
super.onLayout(changed, l, t, r, b);
if (Util.isStringEmpty(mTextContent))
return;
// Log.e("top text 宽 高",
// mTopText.getWidth() + " " + mTopText.getHeight());
// Log.e("top text 单行高度", "" + mTopText.getLineHeight());
// Rect rect = getStringRect(mTextContent, mTopText.getPaint());
// Log.e("文本的宽 高", rect.width() + " " + rect.height());
// toptext最多能显示的行数
int topTextMaxRows = mTopText.getHeight() / mTopText.getLineHeight();
// 显示这些内容真实占用的行数
int textRows = mTopText.getLineCount();
// Log.e("top text最多能显示的行数", "" + topTextMaxRows);
// Log.e("当前文本实际占用的行数", "" + textRows);
//由于文本可能带有表情,重新计算显示行数,以保证显示正确
Rect lineR = new Rect();
int realH = 0; //toptext真实高度
int i = 0;
for(i = 0; i < topTextMaxRows; i++){
try{
mTopText.getLineBounds(i, lineR);
}request(IndexOutOfBoundsException e){
break;
}
realH += lineR.height();
if(realH >= mTopText.getHeight())
break;
}
// Log.e("当前view实际能显示的行数为", "" + i);
topTextMaxRows = i;
//如果toptext显示不下的话,显示到bottomtext里面去
if (textRows >= topTextMaxRows && bRequestBottomLayout) {
// toptext最后一个可见字符的位置
int lastindex = mTopText.getLayout().getLineVisibleEnd(
topTextMaxRows - 1);
ClickableSpan[] cs = mTopText.getSpans();
int spanstart = 0;
int spanend = 0;
spanstring = "";
STATE = 0; // 1网页 2人名 3话题
for (ClickableSpan c : cs) {
spanstart = mTopText.getSpanStart(c);
spanend = mTopText.getSpanEnd(c);
if (spanstart <= lastindex && spanend > lastindex) {
if (c instanceof LinkClickableSpan) {
// Log.e("转角span类型", "网页");
spanstring = ((LinkClickableSpan) c).getLink();
STATE = 1;
}
if (c instanceof NameClickableSpan) {
// Log.e("转角span类型", "人名");
spanstring = ((NameClickableSpan) c).getName();
STATE = 2;
}
if (c instanceof TopicClickableSpan) {
// Log.e("转角span类型", "话题");
spanstring = ((TopicClickableSpan) c).getTopic();
STATE = 3;
}
break;
}
}
mTopText.setText(mTextContent.substring(0, lastindex));
mTopText.setVisibility(View.VISIBLE);
// 当行数不是整数时,调整内容会有下面半行没遮住,所以干脆都处理完以后再显示出来
mBottomText.setText(mTextContent.substring(lastindex,
mTextContent.length()) + mBottomText.getText().toString());
if (mBottomText.getText().length() > 0 && bRequestBottomLayout){
mBottomText.setVisibility(View.VISIBLE);
mHandler.post(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
mBottomText.requestLayout();
bRequestBottomLayout = false;
}
});
}
if (STATE != 0 || !bRequestBottomLayout) {
Log.e("spanstring", spanstring);
// Log.e("", "移除转角span");
mTopText.getSpannable().removeSpan(mTopText.getSpans()[mTopText.getSpans().length - 1]);
switch (STATE) {
case 1:
mTopText.getSpannable().setSpan(
mTopText.new LinkClickableSpan(spanstring),
spanstart,
lastindex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
mBottomText.getSpannable().setSpan(
mBottomText.new LinkClickableSpan(spanstring),
0,
spanend - lastindex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
// Log.e("", "网页");
break;
case 2:
mTopText.getSpannable().setSpan(
mTopText.new NameClickableSpan(spanstring),
spanstart,
lastindex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
mBottomText.getSpannable().setSpan(
mBottomText.new NameClickableSpan(spanstring),
0,
spanend - lastindex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
// Log.e("", "人名");
break;
case 3:
mTopText.getSpannable().setSpan(
mTopText.new TopicClickableSpan(spanstring),
spanstart,
lastindex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
mBottomText.getSpannable().setSpan(
mBottomText.new TopicClickableSpan(spanstring),
0,
spanend - lastindex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
// Log.e("", "话题");
break;
}
mTopText.setText(mTopText.getSpannable(), BufferType.SPANNABLE);
mBottomText.setText(mBottomText.getSpannable(), BufferType.SPANNABLE);
}
}
}
说明下难点:
第一,转角处可能存在Span,比如一个话题 #话题话题话题话题话题话题话题话题话题#,可能一半内容在mTopText里面的最后一行显示,而另一半显示不下了,这就需要剪切剩余的String显示到mBottomText里面去,可是Span的内容就会出错,这就需要重新设置下转角处的Span效果了
第二,由于存在图片Span,会导致行高不正确,所以需要在获取到mTopText能显示的最多行数以后,重新判断一次是否正确,如果不正确的话,需要重新调整,不然mTopText会有一部分内容显示不下。
该View实现完成后,在布局中使用catch条用,代码中find出来就可以直接使用了
不考虑Span的话,直接将代码中转角处理Span部分删除即可使用,我注释的挺清楚了