Android进阶之路 - StaticLayout 绘制文本自动换行

2024-05-30 1404阅读

最近在修改 WheelView 自定义控件,因为原始效果单行效果,现在需要支持换行效果,故接触到了 StaticLayout ,先行记录

在 Android自定义控件中,当我们调用 drawText 绘制 Text 时,假设 Text 为超长字符串(文本宽度超过屏幕宽度)也只会显示一行,超出部分会隐藏在屏幕之外…

而 Google 为了适配多行 Text 显示效果,早已提供了 StaticLayout 工具类用于处理文字换行的问题,现在开始一起来看我写个Demo吧!

    • 基础认知
    • 单行控件
    • 多行控件
    • 使用方式
    • 单点技能
      • 设置控件宽、高
      • 根据内容动态调整控件高度

        效果(Demo下载地址)

        Android进阶之路 - StaticLayout 绘制文本自动换行

        基础认知

        很多人可能会想到TextView为何支持换行?嗯… 有没有可能TextView内部也使用了 StaticLayout?

        在自定义控件中关于单行显示的 drawText 场景很常见,以下是一些构造方法

        Android进阶之路 - StaticLayout 绘制文本自动换行

        重点我们来看一下换行显示内容的方式,不清楚大家对于StaticLayout了解的多不多,我们直接通过其构造参数来学一些就行

        • StaticLayout 相关构造参数

          Android进阶之路 - StaticLayout 绘制文本自动换行

          • 具体函数

            Android进阶之路 - 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 源码

              Android进阶之路 - StaticLayout 绘制文本自动换行

              • 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.staticlayoutdemo
                import android.annotation.SuppressLint
                import androidx.appcompat.app.AppCompatActivity
                import android.os.Bundle
                class 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();
                    }
                
VPS购买请点击我

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

目录[+]