Android实现文本排版
在项目中有一个小功能需要实现,就是对多行文本进行排版布局,每一行的内容又分为两部分,左边为标题,右边为描述,左边内容长度不确定,右边的内容需要对齐,如有换行也需要对齐右边的文本。
一、效果图
可以看到内容分成了两部分,左边的颜色与右边不一致,右边的描述文案统一对齐。
二、实现方案
以上功能,由于输入内容输入行数不确定,并且左边的文案长度也不确定,因此不能直接在布局中实现,基于此这里主要实现了以下6种方式
方案1
采用自定义控件的方式,继承textview,重新ondraw函数,实现如下:
/** * 计算出左边最长的显示字符串maxleftwidth,之后draw每一行字符,右边的描述从maxleftwidth开始draw * 当一行显示不完全时,折行并且空出maxleftwidth的空格长度 */ public class typographyview1 extends textview { private paint leftpaint = new paint(); private paint rightpaint = new paint(); private int fullwidth; private float textsize; private jsonarray array; private int middlepadding = 0; float maxleftwidth = 0; int itemsize = 0; public typographyview1(context context) { super(context); init(); } public typographyview1(context context, attributeset attrs) { super(context, attrs); init(); } public typographyview1(context context, attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); init(); } private void init() { textsize = getresources().getdimensionpixelsize(r.dimen.text_size_13); leftpaint.setantialias(true); leftpaint.settextsize(textsize); leftpaint.setcolor(getresources().getcolor(r.color.color_black_999999)); rightpaint.setantialias(true); rightpaint.settextsize(textsize); rightpaint.setcolor(getresources().getcolor(r.color.color_black)); middlepadding = getresources().getdimensionpixelsize(r.dimen.padding_value); } @override protected void onsizechanged(int w, int h, int oldw, int oldh) { super.onsizechanged(w, h, oldw, oldh); fullwidth = getwidth();// 整个textview的宽度 } public void settext(jsonarray array) { this.array = array; if (array != null) { try { int size = itemsize = array.length(); for (int i = 0; i < size; ++i) { jsonarray o = (jsonarray) array.get(i); string key = o.getstring(0); string value = o.getstring(1); if (textutils.isempty(key) || textutils.isempty(value)) { itemsize--; continue; } float curwidth = leftpaint.measuretext(key); if (curwidth > maxleftwidth) { maxleftwidth = curwidth; } } maxleftwidth = maxleftwidth + middlepadding; invalidate(); } catch (exception e) { } } } boolean setheight = false; @override protected void ondraw(canvas canvas) { if (array == null) { return; } int linecount = 0; try { jsonarray item; float offsety; for (int i = 0; i < itemsize; ++i) { item = (jsonarray) array.get(i); offsety = (linecount + 1) * textsize; canvas.drawtext(item.getstring(0), 0, offsety, leftpaint); string value = item.getstring(1); float valuewidth = rightpaint.measuretext(value); if (valuewidth > fullwidth - maxleftwidth) {// 一行显示不完 char[] textchararray = value.tochararray(); float charwidth; float drawwidth = maxleftwidth; for (int j = 0; j < textchararray.length; j++) { charwidth = rightpaint.measuretext(textchararray, j, 1); if (fullwidth - drawwidth < charwidth) { linecount++; drawwidth = maxleftwidth; offsety += textsize; } canvas.drawtext(textchararray, j, 1, drawwidth, offsety, rightpaint); drawwidth += charwidth; } } else { canvas.drawtext(value, maxleftwidth, offsety, rightpaint); } linecount += 2; } if (!setheight) { setheight((linecount + 1) * (int) textsize); setheight = true; } } catch (jsonexception e) { e.printstacktrace(); } } }
添加了settext(jsonarray array)作为数据输入,并且在这里面测量了左边title的最大宽度,之后调用invalidate触发重绘,在onsizechanged获取整个控件的宽度,重绘会调用ondraw函数,这里不需要调用super函数,textview的ondraw函数做了非常多的操作,解析传入的数据,分别一行一行调用canvas来进行drawtext操作,当绘制描述时,先计算宽度,如果超过剩余控件说明需要换行,最后调用setheight设置高度,这个加一个判断条件,因为会触发requestlayout()进行重新布局和invalidate()进行重绘,如果不加判断会一直重绘。
方案2
方式2与方式1差不多,不同为所有计算都在ondraw函数中:
/** * 该方式与方式1很类似,只是所有的计算都放在了ondraw方法中。 */ public class typographyview2 extends textview { private paint paint1 = new paint(); private paint paint2 = new paint(); private int middlepadding = 0; int width; private float textsize; private jsonarray array; public typographyview2(context context) { super(context); init(); } public typographyview2(context context, attributeset attrs) { super(context, attrs); init(); } public typographyview2(context context, attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); init(); } private void init() { textsize = getresources().getdimensionpixelsize(r.dimen.text_size_13); paint1.setantialias(true); paint1.settextsize(textsize); paint1.setcolor(getresources().getcolor(r.color.color_black_999999)); paint2.setantialias(true); paint2.settextsize(textsize); paint2.setcolor(getresources().getcolor(r.color.color_black)); middlepadding = getresources().getdimensionpixelsize(r.dimen.padding_value); } @override protected void onsizechanged(int w, int h, int oldw, int oldh) { super.onsizechanged(w, h, oldw, oldh); width = getwidth();// 整个textview的宽度 } public void settext(jsonarray array) { this.array = array; if (array != null) { invalidate(); } } boolean setheight = false; @override protected void ondraw(canvas canvas) { // super.ondraw(canvas); int linecount = 0; int size = array.length(); float maxleftwidth = 0; float drawwidth = 0; try { for (int i = 0; i < size; ++i) { jsonarray o = (jsonarray) array.get(i); string key = o.getstring(0); float v = paint1.measuretext(key); if (v > maxleftwidth) { maxleftwidth = v; } } maxleftwidth = maxleftwidth + middlepadding; for (int i = 0; i < size; ++i) { jsonarray o = (jsonarray) array.get(i); string key = o.getstring(0); canvas.drawtext(key, 0, (linecount + 1) * textsize, paint1); string value = o.getstring(1); char[] textchararray = value.tochararray(); float charwidth; drawwidth = maxleftwidth; for (int j = 0; j < textchararray.length; j++) { charwidth = paint1.measuretext(textchararray, j, 1); if (width - drawwidth < charwidth) { linecount++; drawwidth = maxleftwidth; } canvas.drawtext(textchararray, j, 1, drawwidth, (linecount + 1) * textsize, paint2); drawwidth += charwidth; } linecount += 2; } if (!setheight) { setheight((linecount + 1) * (int) textsize + 5); setheight = true; } } catch (jsonexception e) { e.printstacktrace(); } } }
该方案的实现是不太好的,方案1也是在此基础上进行调整的,在这里放出来只是为了说明,所有的计算不要全部放在ondraw里面,因为该方法可能会反复调用多次,这样就降低了性能。
方案3
将数据源拼接成spannablestring,重写ondraw函数,根据内容draw每一个字符:
/** * 该方法,是需要显示的内容先拼接成spannablestring,在ondraw方法中获取所有的char字符,一个一个比较 * 当为分号是,表示为key与value的分隔符。 */ public class typographyview3 extends textview { private paint leftpaint = new paint(); private paint rightpaint = new paint(); int width; private string text; private float textsize; float maxleftwidth = 0; private int middlepadding = 0; public typographyview3(context context) { super(context); init(); } public typographyview3(context context, attributeset attrs) { super(context, attrs); init(); } public typographyview3(context context, attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); init(); } private void init() { textsize = getresources().getdimensionpixelsize(r.dimen.text_size_13); textsize = getresources().getdimensionpixelsize(r.dimen.text_size_13); leftpaint.setantialias(true); leftpaint.settextsize(textsize); leftpaint.setcolor(getresources().getcolor(r.color.color_black_999999)); rightpaint.setantialias(true); rightpaint.settextsize(textsize); rightpaint.setcolor(getresources().getcolor(r.color.color_black)); middlepadding = getresources().getdimensionpixelsize(r.dimen.padding_value); } @override protected void onsizechanged(int w, int h, int oldw, int oldh) { super.onsizechanged(w, h, oldw, oldh); width = getwidth();// 整个textview的宽度 } public void settext(jsonarray data) { if (data == null) { return; } try { int size = data.length(); for (int i = 0; i < size; ++i) { jsonarray o = (jsonarray) data.get(i); string key = o.getstring(0); float v = leftpaint.measuretext(key); if (v > maxleftwidth) { maxleftwidth = v; } } maxleftwidth += middlepadding; spannablestringbuilder ssb = new spannablestringbuilder(); for (int i = 0; i < size; ++i) { additem((jsonarray) data.get(i), ssb, i != 0); } settext(ssb, buffertype.spannable); } catch (exception e) { } } private void additem(jsonarray item, spannablestringbuilder ssb, boolean breakline) { try { if (item == null || item.length() == 0) { return; } string key = item.getstring(0); string value = (item.length() >= 2) ? item.getstring(1) : ""; if (textutils.isempty(key) && textutils.isempty(value)) { return; } if (breakline) {// 换行 ssb.append("\r\n"); ssb.append("\r\n"); } spannablestring span = new spannablestring(key); // span.setspan(new foregroundcolorspan(getresources().getcolor(r.color.coloraccent)), 0, key // .length(), // spanned.span_inclusive_exclusive); ssb.append(span); ssb.append(value); } catch (jsonexception e) { e.printstacktrace(); } } @override protected void ondraw(canvas canvas) { // super.ondraw(canvas); int linecount = 0; text = this.gettext().tostring(); if (text == null) return; char[] textchararray = text.tochararray(); // 已绘的宽度 float drawwidth = 0; float charwidth; paint paint = leftpaint; for (int i = 0; i < textchararray.length; i++) { charwidth = leftpaint.measuretext(textchararray, i, 1); if (textchararray[i] == '\n') { linecount++; drawwidth = 0; paint = leftpaint; continue; } if (width - drawwidth < charwidth) { linecount++; drawwidth = maxleftwidth; } if (i > 1 && textchararray[i - 1] == ':') { drawwidth = maxleftwidth; paint = rightpaint; } canvas.drawtext(textchararray, i, 1, drawwidth, (linecount + 1) * textsize, paint); drawwidth += charwidth; } //may be need set height //setheight((linecount + 1) * (int) textsize + 5); } }
这里先计算左边title的最大宽度,同时将所有的数据拼接成一个spannablestringbuilder,调用settext函数会触发重绘,在ondraw函数中进行处理,由于未重新super函数,因此spannablestring的setspan函数失效,该方案主要根据分隔符来进行分割,因此分隔符需要唯一。
方案4
采用gridlayout方式实现,但是原始控件有展示问题,因此对此进行了修改:
public class typography4activity extends baseactivity { public static void start(context context) { intent intent = new intent(); intent.setclass(context, typography4activity.class); context.startactivity(intent); } private linearlayout root; private paint leftpaint = new paint(); private float textsize; private float maxleftwidth; private int middlepadding = 0; private float maxrightwidth; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); root = (linearlayout) layoutinflater.from(this).inflate(r.layout.activity_typography4, null); setcontentview(root); initpaint(); findviews(); loaddata(); } private void initpaint() { textsize = getresources().getdimensionpixelsize(r.dimen.text_size_13); leftpaint.setantialias(true); leftpaint.settextsize(textsize); leftpaint.setcolor(getresources().getcolor(r.color.color_black_999999)); middlepadding = getresources().getdimensionpixelsize(r.dimen.padding_value); } private void findviews() { } private void loaddata() { addgridlayout(datasource.getarray()); textview view = new textview(this); view.settext("修改后的实现"); view.setgravity(gravity.center); view.setlayoutparams(new viewgroup.layoutparams(viewgroup.layoutparams.match_parent, 160)); root.addview(view); addmodifygridlayout(datasource.getarray()); } private void addgridlayout(jsonarray data) { try { gridlayout layout = creategridlayout(); int size = data.length(); for (int i = 0; i < size; ++i) { jsonarray item = (jsonarray) data.get(i); string key = item.getstring(0); string value = (item.length() >= 2) ? item.getstring(1) : ""; gridlayout.spec row = gridlayout.spec(i); gridlayout.spec col1 = gridlayout.spec(0); gridlayout.spec col2 = gridlayout.spec(1); gridlayout.layoutparams params = new gridlayout.layoutparams(row, col1); textview title = getlefttextview(key); layout.addview(title, params); params = new gridlayout.layoutparams(row, col2); textview desc = getrighttextview(value); layout.addview(desc, params); } root.addview(layout); } catch (exception e) { } } @nonnull private textview getrighttextview(string value) { textview desc = new textview(this); desc.settextsize(13); desc.settextcolor(getresources().getcolor(r.color.black)); desc.settext(value); return desc; } @nonnull private textview getlefttextview(string key) { textview title = new textview(this); title.settext(key); title.setpadding(0, middlepadding, middlepadding, 0); title.settextcolor(getresources().getcolor(r.color.color_black_999999)); title.settextsize(13); return title; } private void addmodifygridlayout(jsonarray data) { try { calculateleftmaxwidth(data); gridlayout layout = creategridlayout(); int size = data.length(); for (int i = 0; i < size; ++i) { jsonarray item = (jsonarray) data.get(i); gridlayout.spec row = gridlayout.spec(i); string key = item.getstring(0); gridlayout.spec col1 = gridlayout.spec(0); gridlayout.layoutparams params = new gridlayout.layoutparams(row, col1); textview title = getlefttextview(key); layout.addview(title, params); string value = (item.length() >= 2) ? item.getstring(1) : ""; gridlayout.spec col2 = gridlayout.spec(1); params = new gridlayout.layoutparams(row, col2); textview desc = getrighttextview(value); params.width = (int) maxrightwidth; params.height = viewgroup.layoutparams.wrap_content; layout.addview(desc, params); } root.addview(layout); } catch (exception e) { } } private void calculateleftmaxwidth(jsonarray data) { try { displayutil.init(this);// 这个可以在应用程序起来的时候init int size = data.length(); for (int i = 0; i < size; ++i) { jsonarray o = (jsonarray) data.get(i); string key = o.getstring(0); string value = o.getstring(1); if (textutils.isempty(key) || textutils.isempty(value)) { continue; } float curwidth = leftpaint.measuretext(key); if (curwidth > maxleftwidth) { maxleftwidth = curwidth; } } maxleftwidth = maxleftwidth + middlepadding; maxrightwidth = displayutil.screenwidth - displayutil.dp2px(this, 32 + 10) - maxleftwidth; } catch (exception e) { } } private gridlayout creategridlayout() { gridlayout layout = new gridlayout(this); layout.setcolumncount(2); //layout.setrowcount(5); layout.setorientation(gridlayout.horizontal); return layout; } }
如果直接创建一个gridlayout,里面添加每一项,如果描述过长都导致显示不全,这个是系统的一个bug,计算的宽度有问题,因此需要对此方案进行更改。
更改方式为先计算左边占用的最大宽度,在添加右边的项时,设置布局参数控制最大的长度。
方案5
采用每一行一个布局,手动一行一行进行添加:
public class typography5activity extends baseactivity { public static void start(context context) { intent intent = new intent(); intent.setclass(context, typography5activity.class); context.startactivity(intent); } private linearlayout root; private paint leftpaint = new paint(); private float textsize; private float maxleftwidth; private int middlepadding = 0; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); root = (linearlayout) layoutinflater.from(this).inflate(r.layout.activity_typography5, null); setcontentview(root); initpaint(); loaddata(); } private void initpaint() { textsize = getresources().getdimensionpixelsize(r.dimen.text_size_13); leftpaint.setantialias(true); leftpaint.settextsize(textsize); leftpaint.setcolor(getresources().getcolor(r.color.color_black_999999)); middlepadding = getresources().getdimensionpixelsize(r.dimen.padding_value); } private void loaddata() { jsonarray array = datasource.getarray(); calculateleftmaxwidth(array); if (array != null) { try { int size = array.length(); for (int i = 0; i < size; ++i) { jsonarray o = (jsonarray) array.get(i); string key = o.getstring(0); string value = o.getstring(1); additem(key, value); } } catch (exception e) { } } } private void calculateleftmaxwidth(jsonarray data) { try { int size = data.length(); for (int i = 0; i < size; ++i) { jsonarray o = (jsonarray) data.get(i); string key = o.getstring(0); string value = o.getstring(1); if (textutils.isempty(key) || textutils.isempty(value)) { continue; } float curwidth = leftpaint.measuretext(key); if (curwidth > maxleftwidth) { maxleftwidth = curwidth; } } maxleftwidth = maxleftwidth + middlepadding; } catch (exception e) { } } private void additem(string key, string value) { linearlayout layout = getitemlayout(); textview left = (textview) layout.findviewbyid(r.id.left); linearlayout.layoutparams params = new linearlayout.layoutparams(viewgroup.layoutparams.wrap_content, viewgroup.layoutparams.wrap_content); params.width = (int) maxleftwidth; left.setlayoutparams(params); left.settext(key); textview right = (textview) layout.findviewbyid(r.id.right); right.settext(value); root.addview(layout); } private linearlayout getitemlayout() { linearlayout layout = (linearlayout) layoutinflater.from(this).inflate(r.layout.compose_item_layout, null); return layout; } }
改方案也需要先计算左边的最大占用宽度,来设置右边占用的大小,每一项的布局如下:
<?xml version="1.0" encoding="utf-8"?> <linearlayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" android:paddingtop="@dimen/text_padding_10" tools:context=".activity.typography1activity"> <textview android:id="@+id/left" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginright="@dimen/text_padding_10" android:textcolor="@color/color_black_999999" android:textsize="@dimen/text_size_13"/> <textview android:id="@+id/right" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:textcolor="@color/black" android:textsize="@dimen/text_size_13"/> </linearlayout>
每一行有两个textview,左边宽度为自适应,右边占据剩下左右的位置,在计算出左边最大宽度后,重新设置左边每一个textview占用的宽度。
方案6
方式与1差不多,但是不在继承textview,而是直接继承view:
public class typographyview4 extends view { private paint leftpaint = new paint(); private paint rightpaint = new paint(); private int fullwidth; private float textsize; private jsonarray array; private int middlepadding = 0; float maxleftwidth = 0; int itemsize = 0; public typographyview4(context context) { super(context); init(); } public typographyview4(context context, attributeset attrs) { super(context, attrs); init(); } public typographyview4(context context, attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); init(); } private void init() { textsize = getresources().getdimensionpixelsize(r.dimen.text_size_13); leftpaint.setantialias(true); leftpaint.settextsize(textsize); leftpaint.setcolor(getresources().getcolor(r.color.color_black_999999)); rightpaint.setantialias(true); rightpaint.settextsize(textsize); rightpaint.setcolor(getresources().getcolor(r.color.color_black)); middlepadding = getresources().getdimensionpixelsize(r.dimen.padding_value); } @override protected void onsizechanged(int w, int h, int oldw, int oldh) { super.onsizechanged(w, h, oldw, oldh); fullwidth = getwidth();// 整个textview的宽度 } public void settext(jsonarray array) { this.array = array; if (array != null) { try { int size = itemsize = array.length(); for (int i = 0; i < size; ++i) { jsonarray o = (jsonarray) array.get(i); string key = o.getstring(0); string value = o.getstring(1); if (textutils.isempty(key) || textutils.isempty(value)) { itemsize--; continue; } float curwidth = leftpaint.measuretext(key); if (curwidth > maxleftwidth) { maxleftwidth = curwidth; } } maxleftwidth = maxleftwidth + middlepadding; invalidate(); } catch (exception e) { } } } @override protected void ondraw(canvas canvas) { if (array == null) { return; } int linecount = 0; try { jsonarray item; float offsety; for (int i = 0; i < itemsize; ++i) { item = (jsonarray) array.get(i); offsety = (linecount + 1) * textsize; canvas.drawtext(item.getstring(0), 0, offsety, leftpaint); string value = item.getstring(1); float valuewidth = rightpaint.measuretext(value); if (valuewidth > fullwidth - maxleftwidth) {// 一行显示不完 char[] textchararray = value.tochararray(); float charwidth; float drawwidth = maxleftwidth; for (int j = 0; j < textchararray.length; j++) { charwidth = rightpaint.measuretext(textchararray, j, 1); if (fullwidth - drawwidth < charwidth) { linecount++; drawwidth = maxleftwidth; offsety += textsize; } canvas.drawtext(textchararray, j, 1, drawwidth, offsety, rightpaint); drawwidth += charwidth; } } else { canvas.drawtext(value, maxleftwidth, offsety, rightpaint); } linecount += 2; } } catch (jsonexception e) { e.printstacktrace(); } } }
该方案主要继承自view,不再继承textview,由于在在上述方案中不在调用super,因此textview已经退化为一个view,因此直接继承view。
总结
因为左边的宽度不确定,因此所有的方案都进行了同样的一个操作,就是测量了左边显示的最大宽度,后续的操作再根据该宽度进行调整。上述的方案中1,2,3,6都只需用一个view来进行显示,4,5都需要多个view进行显示。
完整的代码可以在查看链接上进行查看。
以上就是本文的全部内容,希望对大家的学习有所帮助。