自定义换行
我们通常在自定义控件的时候会遇到不想用系统默认的文字换行,而需要根据自己的需求来实现文字的换行,原理如下:
- 使用Paint的measureText方法来测量文字的长度
- 假设设定单行的最大长度为maxWidth,那么就通过measureText来测量每一个字的长度,然后不断的累加再去对比maxWidth,如果比maxWidth大,那就追加一个换行符号,然后再把累加长度归零,继续开始计算,以此类推
图文混排实现
- TextView中有一个概念就是富文本,富文本可以实现图文混排,代码如下:
Spannable spannable = Spannable.Factory.getInstance().newSpannable(sbNewText.toString());
ImageSpan imageSpan = new ImageSpan(b);
spannable.setSpan(imageSpan, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
setText(spannable);
Spannable是根据当前TextView的文本内容创建出来的,然后可以用ImageSpan去替换Spannable中的任意一个位置
文本缩进实现
- 依然是用TextView的富文本Spannable去实现,代码如下:
Spannable spannable = Spannable.Factory.getInstance().newSpannable(sbNewText.toString());
BitmapDrawable spaceDrawable = new BitmapDrawable(resources, (Bitmap) null);
spaceDrawable.setBounds(0, 0, sapceWidth, sapceWidth);
spannable.setSpan(new ImageSpan(spaceDrawable), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
缩进其实就是用一个空的BitmapDrawable去替换掉你想替换的位置,比如我替换的是第一个位置,当然不能为了缩进把一些真正的文字内容给替换掉了,所以需要在想替换的位置中插入一些无用字符来占位,然后再去用图片或者空的图片来替换这个占位符的位置,我定义的占位符为“#”,一下代码实现了图文混排,文字缩进,自定义换行的功能:
private void formatText(Bitmap bitmap, String text) {
int sapceWidth = DensityUtils.dp2px(getContext(), 20L);
Resources resources = getContext().getResources();
BitmapDrawable b = new BitmapDrawable(resources, bitmap);
b.setBounds(0, 0, sapceWidth, sapceWidth);
TextPaint textPaint = getPaint();
textPaint.setTextSize(getTextSize());
float textWidth = textPaint.measureText(text); // 文本总长度
int viewWidth = getWidth() - getPaddingLeft() - getPaddingRight() - sapceWidth; // 控件可用长度;
if (viewWidth <= 0) {
int oneWordWidth = (int) textPaint.measureText("一");
int widthSpec = View.MeasureSpec.makeMeasureSpec(resources.getDisplayMetrics().widthPixels - oneWordWidth, MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
measure(widthSpec, heightSpec);
viewWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - sapceWidth; // 控件可用长度
}
Spannable spannable;
if (textWidth > viewWidth) { // 超出一行
float lineWidth = 0;
StringBuffer sbNewText = new StringBuffer("#");
// 通过循环累积测量每一个字符的长度来判断当前的累积字符长度之和是否已经超过一行的长度,如果超过
// 就进行换行
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
lineWidth += textPaint.measureText(String.valueOf(ch));
if (lineWidth <= viewWidth) {
sbNewText.append(ch);
} else {
sbNewText.append("\n");
sbNewText.append("#");
lineWidth = 0;
i--;
}
}
LogUtil.i("rich text:\n" + sbNewText.toString());
spannable = Spannable.Factory.getInstance().newSpannable(sbNewText.toString());
if (spaceDrawable == null) {
spaceDrawable = new BitmapDrawable(resources, (Bitmap) null);
spaceDrawable.setBounds(0, 0, sapceWidth, sapceWidth);
}
for (int j = 1;j < sbNewText.length();j++) {
if (sbNewText.charAt(j) == '#') {
int index = sbNewText.indexOf("#", j);
spannable.setSpan(new ImageSpan(spaceDrawable), index, index + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
} else {
spannable = Spannable.Factory.getInstance().newSpannable("#" + text);
}
ImageSpan imageSpan = new ImageSpan(b);
spannable.setSpan(imageSpan, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
setText(spannable);
}
这里面有一个点需要注意,在ListView或者RecycleView的Adapter加载这些View的时候有可能View可能还没有渲染所以宽度获取到为零:
int viewWidth = getWidth() - getPaddingLeft() - getPaddingRight() - sapceWidth; // 控件可用长度;
getWidth()为0,所以会导致viewWidth最后结果为负数,这时候就需要我们自己来测量控件的可用长度,我用了一个投机取巧的版本,我发现场景中控件的宽度是match_parent类型的,而且正好是match屏幕的宽度,所以就会有以下处理:
if (viewWidth <= 0) {
int oneWordWidth = (int) textPaint.measureText("一");
int widthSpec = View.MeasureSpec.makeMeasureSpec(resources.getDisplayMetrics().widthPixels - oneWordWidth, MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
measure(widthSpec, heightSpec);
viewWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - sapceWidth; // 控件可用长度
}
makeMeasureSpec是根据传入的长度以及长度测量模式来计算出一个值,这里传入的长度要用屏幕的宽度扣去一个字的长度,至于为什么要这么做是我根据我们的屏幕分辨率调出来的,不同的屏幕分辨率可能需要扣去的值也不同,如果不扣去这一个字的长度会导致测量出来的长度过长,这样计算出来的空间宽度就会过长,会导致已经达到了TextView自动换行的时机了,但是我们自定义的换行时机还没有达到,所以自定义换行的时机一定要发生在TextView自动换行时机之前这样才能保证自定义换行实现准确换行,至于如何在View还没渲染的时候首先测量View的宽度,那就去参考onMeasure方法中是怎么测量的了,measure方法就是根据转入的参数去测量得出View的测量长度和宽度,然后通过getMeasuredWidth和getMeasuredHeight方法来获取到测量的值,测量长度和宽度的核心就在于一个实际的长度和宽度,另一个就是测量模式,这两个属性结合就可以得出一个测量结果
测量View的方法
int widthSpec = View.MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
measure(widthSpec, heightSpec);
测量模式
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
- UPSPECIFIED :父容器对于子容器没有任何限制,子容器想要多大就多大,当width或者height设置为0的时候就使用这个模式去测量
- EXACTLY:父容器已经为子容器设置了尺寸,子容器应当服从这些边界,不论子容器想要多大的空间(对应match_parent)
- AT_MOST:最大不能超过当前给定的参考值(对应wrap_content)