行业知识
Android进阶之路 - StaticLayout 绘制文本自动换行
2026-11-09 13:46:43
来源:互联网转载
最近在修改 WheelView 自定义控件,因为原始效果单行效果,现在需要支持换行效果,故接触到了 StaticLayout ,先行记录
在 Android自定义控件中,当我们调用 drawText 绘制 Text 时,假设 Text 为超长字符串(文本宽度超过屏幕宽度)也只会显示一行,超出部分会隐藏在屏幕之外…
而 Google 为了适配多行 Text 显示效果,早已提供了 StaticLayout 工具类用于处理文字换行的问题,现在开始一起来看我写个Demo吧!
- 基础认知
- 单行控件
- 多行控件
- 使用方式
- 单点技能
- 设置控件宽、高
- 根据内容动态调整控件高度
效果(Demo下载地址)
基础认知
很多人可能会想到TextView为何支持换行?嗯… 有没有可能TextView内部也使用了 StaticLayout?
在自定义控件中关于单行显示的 drawText 场景很常见,以下是一些构造方法
重点我们来看一下换行显示内容的方式,不清楚大家对于StaticLayout了解的多不多,我们直接通过其构造参数来学一些就行
- StaticLayout 相关构造参数
- 具体函数
StaticLayout 参数解释
@Deprecated public StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { super((CharSequence)null, (TextPaint)null, 0, (Layout.Alignment)null, 0.0F, 0.0F); throw new RuntimeException("Stub!"); }- CharSequence source: 需要分行的字符串
- int bufstart: 需要分行的字符串从第几的位置开始
- int bufend :需要分行的字符串到哪里结束
- TextPaint paint: 画笔对象(必须为TextPaint,不同于drawText)
- int outerwidth: layout的宽度,超出时换行
- Alignment align layout 对其方式:ALIGN_CENTER, ALIGN_NORMAL, ALIGN_OPPOSITE
位于 Layout 源码
- float spacingmult: 相对行间距,相对字体大小,1.5f表示行间距为1.5倍的字体高度。
- float spacingadd: 在基础行距上添加多少
- boolean includepad:是否有包含其他的layout(个人感觉),一般设置为false
- TextUtils.TruncateAt ellipsize: 从什么位置开始省略
- int ellipsizedWidth: 超过多少开始省略
单行控件
AloneLineTextView
package com.example.staticlayoutdemo;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.Rect;import android.text.StaticLayout;import android.util.AttributeSet;import android.util.Log;import android.view.View;import androidx.annotation.Nullable;public class AloneLineTextView extends View { private Paint paint; private String contentText; private int measureWidth; public AloneLineTextView(Context context) { super(context); init(); } public AloneLineTextView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } public AloneLineTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { paint = new Paint(); paint.setAntiAlias(true); paint.setColor(Color.RED); paint.setStrokeWidth(10f); paint.setTextSize(38f); paint.setStyle(Paint.Style.FILL); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); measureWidth = MeasureSpec.getSize(widthMeasureSpec); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (contentText == null || contentText.isEmpty()) { contentText = "AloneLineTextView 首次绘制"; } //测量文本宽度 Rect rect = new Rect(); paint.getTextBounds(contentText, 0, contentText.length(), rect); int contentSize = rect.width(); Log.e("tag", "屏幕宽度:" + measureWidth); Log.e("tag", "文本宽度:" + contentSize); canvas.drawText(contentText, 50, 200, paint); } void setContextText(String content) { contentText = content; postInvalidate(); }}多行控件
当内容宽度超过屏幕宽度时,使用 StaticLayout让 View自动换行
MoreLinesTextView
package com.example.staticlayoutdemo;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.Rect;import android.text.Layout;import android.text.StaticLayout;import android.text.TextPaint;import android.util.AttributeSet;import android.util.Log;import android.view.View;import androidx.annotation.Nullable;public class MoreLinesTextView extends View { private TextPaint textPaint; private String contentText; private int widthSize, heightSize;//控件宽、高 private int widthMode, heightMode;//控件宽、高 private int paddingLeft, paddingTop, paddingRight, paddingBottom; //内边距(通过外部的xml中设置居中,内部尚未做出具体逻辑处理) private int afterWidthSize, afterHeightSize; //减去内边距后的宽高 private int currentHeight;//控件高度 public MoreLinesTextView(Context context) { super(context); init(); } public MoreLinesTextView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } public MoreLinesTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } /** * 初始化画笔相关配置 */ private void init() { textPaint = new TextPaint(); textPaint.setAntiAlias(true); textPaint.setColor(Color.RED); textPaint.setStrokeWidth(10f); textPaint.setTextSize(38f); textPaint.setStyle(Paint.Style.FILL); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {// super.onMeasure(widthMeasureSpec, heightMeasureSpec); //原始Size widthSize = MeasureSpec.getSize(widthMeasureSpec); widthMode = MeasureSpec.getMode(widthMeasureSpec); heightSize = MeasureSpec.getSize(heightMeasureSpec); heightMode = MeasureSpec.getMode(heightMeasureSpec); //去除内边距的Size afterWidthSize = widthSize - paddingRight - paddingLeft; afterHeightSize = heightSize - paddingTop - paddingBottom; // 调试时用到的一些Log// Log.e("tag", "widthSize:" + widthSize + "--" + "afterWidthSize:" + afterWidthSize);// Log.e("tag", "heightSize:" + heightSize + "--" + "afterHeightSize:" + afterHeightSize); //告知父布局,当前View的部分属性 super.onMeasure(MeasureSpec.makeMeasureSpec(afterWidthSize, widthMode), MeasureSpec.makeMeasureSpec(afterHeightSize, heightMode)); setViewHeight(currentHeight);/* // 根据测量模式设置控件的高度 if (heightMode == MeasureSpec.EXACTLY) { // 如果测量模式为EXACTLY(精确),高度为测量大小 setMeasuredDimension(afterWidthSize, heightSize); Log.e("tag", "MeasureSpec-Height:MeasureSpec.EXACTLY"); } else if (heightMode == MeasureSpec.AT_MOST) { // 如果测量模式为AT_MOST(至多),高度为自定义的高度 int desiredHeight = 200; // 自定义高度 setMeasuredDimension(afterWidthSize, Math.min(desiredHeight, heightSize)); Log.e("tag", "MeasureSpec-Height:AT_MOST"); } else { // 如果测量模式为UNSPECIFIED(未指定),高度为自定义的高度 int desiredHeight = 200; // 自定义高度 setMeasuredDimension(afterWidthSize, desiredHeight); Log.e("tag", "MeasureSpec-Height:如果测量模式为UNSPECIFIED"); }*/// setViewHeight(200); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //给一些顶部内边距(偏移量), canvas.translate(0, 50); if (contentText == null || contentText.isEmpty()) { contentText = "MoreLinesTextView 首次绘制"; } //测量文本宽度 Rect rect = new Rect(); textPaint.getTextBounds(contentText, 0, contentText.length(), rect); /* //调试时用到的一些方法 canvas.drawColor(Color.RED); //通过设置View背景,可以更便捷看出View大小 int contentWidthSize = rect.width(); int contentHeightSize = rect.height(); Log.e("tag", "屏幕宽度:" + afterWidthSize); Log.e("tag", "屏幕高度:" + afterHeightSize); Log.e("tag", "文本宽度:" + contentWidthSize); Log.e("tag", "文本高度&canvas.getHeight():" + contentHeightSize); Log.e("tag", "StaticLayout :getWidth()&afterWidthSize()" + getWidth());*/ StaticLayout staticLayout = new StaticLayout(contentText, textPaint, getWidth(), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); staticLayout.draw(canvas); } /** * 设置该View显示内容 * 当需要换行时,动态设置控件高度 */ void setContextText(String content) { contentText = content; //测量文本宽度 Rect rect = new Rect(); textPaint.getTextBounds(contentText, 0, contentText.length(), rect); int contentWidthSize = rect.width(); int contentHeightSize = rect.height(); if (contentWidthSize > afterWidthSize) { currentHeight = contentHeightSize * 5; } else { currentHeight = afterHeightSize; } requestLayout(); postInvalidate(); } /** * 设置该View宽、高 * * @deprecated 自定义高度 * 宽度目前采用的是固定 */ void setViewHeight(int desiredHeight) { if (heightMode == MeasureSpec.EXACTLY) { // 如果测量模式为EXACTLY(精确),高度为测量大小 setMeasuredDimension(afterWidthSize, heightSize); Log.e("tag", "MeasureSpec-Height:MeasureSpec.EXACTLY"); } else if (heightMode == MeasureSpec.AT_MOST) { // 如果测量模式为AT_MOST(至多),高度为自定义的高度 setMeasuredDimension(afterWidthSize, Math.min(desiredHeight, heightSize)); Log.e("tag", "MeasureSpec-Height:AT_MOST"); } else { // 如果测量模式为UNSPECIFIED(未指定),高度为自定义的高度 setMeasuredDimension(afterWidthSize, desiredHeight); Log.e("tag", "MeasureSpec-Height:如果测量模式为UNSPECIFIED"); } } void setSelfPadding(int padding) { paddingLeft = padding; paddingTop = padding; paddingRight = padding; paddingBottom = padding; postInvalidate(); } void setSelfPadding(int left, int top, int right, int bottom) { paddingLeft = left; paddingTop = top; paddingRight = right; paddingBottom = bottom; postInvalidate(); }}使用方式
MainActivity
package com.example.staticlayoutdemoimport android.annotation.SuppressLintimport androidx.appcompat.app.AppCompatActivityimport android.os.Bundleclass MainActivity : AppCompatActivity() { @SuppressLint("MissingInflatedId") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val more = findViewById(R.id.tv_more) more.setContextText("more换行:正月里来是新年啊,大年初一头一天;家家那个团圆会啊,少的给老的拜年啊") more.setSelfPadding(50) val aLone = findViewById(R.id.tv_alone) aLone.setContextText("Alone单行:正月里来是新年啊,大年初一头一天;家家那个团圆会啊,少的给老的拜年啊") }}activity_main
单点技能
其实是我在写Demo时做的一些调试和学到的一些东西
设置控件宽、高
onMeasure 中 super.onMeasure 后调用下方设置方式即可
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 根据测量模式设置控件的高度 if (heightMode == MeasureSpec.EXACTLY) { // 如果测量模式为EXACTLY(精确),高度为测量大小 setMeasuredDimension(afterWidthSize, heightSize); Log.e("tag", "MeasureSpec-Height:MeasureSpec.EXACTLY"); } else if (heightMode == MeasureSpec.AT_MOST) { // 如果测量模式为AT_MOST(至多),高度为自定义的高度 int desiredHeight = 200; // 自定义高度 setMeasuredDimension(afterWidthSize, Math.min(desiredHeight, heightSize)); Log.e("tag", "MeasureSpec-Height:AT_MOST"); } else { // 如果测量模式为UNSPECIFIED(未指定),高度为自定义的高度 int desiredHeight = 200; // 自定义高度 setMeasuredDimension(afterWidthSize, desiredHeight); Log.e("tag", "MeasureSpec-Height:如果测量模式为UNSPECIFIED"); } }根据内容动态调整控件高度
这部分其实属于优化部分,建议结合Demo查看(虽不完美,但应该可以凑乎使用)
/** * 设置该View显示内容 * 当需要换行时,动态设置控件高度 */ void setContextText(String content) { contentText = content; //测量文本宽度 Rect rect = new Rect(); textPaint.getTextBounds(contentText, 0, contentText.length(), rect); int contentWidthSize = rect.width(); int contentHeightSize = rect.height(); if (contentWidthSize > afterWidthSize) { currentHeight = contentHeightSize * 5; } else { currentHeight = afterHeightSize; } requestLayout(); postInvalidate(); }
- 具体函数
- StaticLayout 相关构造参数
上一篇:怎么操作云服务器系统设置?
下一篇:算法-1.暴力和枚举