自定义UI 简易图文混排

Posted Notzuonotdied

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义UI 简易图文混排相关的知识,希望对你有一定的参考价值。

系列文章目录

  1. 自定义UI 基础知识
  2. 自定义UI 绘制饼图
  3. 自定义UI 圆形头像
  4. 自定义UI 自制表盘
  5. 自定义UI 简易图文混排
  6. 自定义UI 使用Camera做三维变换
  7. 自定义UI 属性动画
  8. 自定义UI 自定义布局

文章目录


前言

这系列的文章主要是基于扔物线的HenCoderPlus课程的源码来分析学习。



这一篇文章主要介绍的是文字的测量,更多的内容可以参考:HenCoder Android 开发进阶:自定义 View 1-3 drawText() 文字的绘制

创建绘制对象

我们需要创建一个画笔🖌Paint来绘制我们的头像的边框,也需要提前加载我们的头像到内存(Bitmap)中。

public class ImageTextView extends View 
	// 图片的宽度
    private static final float IMAGE_WIDTH = Utils.dp2px(100);
    // 图片的偏移位置
    private static final float IMAGE_OFFSET = Utils.dp2px(80);

    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    // 头像
    Bitmap bitmap;

    public ImageTextView(Context context, @Nullable AttributeSet attrs) 
        super(context, attrs);
    

    
    	// 记载图片的代码请见下一小节
        bitmap = getAvatar((int) IMAGE_WIDTH);
    


加载图片

为避免出现java.lang.OutOfMemory异常,请先检查位图的尺寸,然后再对其进行解码,除非您绝对信任该来源可为您提供大小可预测的图片数据,以轻松适应可用的内存。——引用自Android官方文档:高效加载大型位图

下述源码的分析请见官方的说明:将按比例缩小的版本加载到内存中

public class ImageTextView extends View 

    // 头像
    Bitmap bitmap;

    Bitmap getAvatar(int width) 
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(getResources(), R.drawable.avatar_rengwuxian, options);
        options.inJustDecodeBounds = false;
        options.inDensity = options.outWidth;
        options.inTargetDensity = width;
        return BitmapFactory.decodeResource(getResources(), R.drawable.avatar_rengwuxian, options);
    

自定义绘制内容

先上一下我们绘制简易图文混排的效果图。

绘制图片

public class ImageTextView extends View 
    // 图片的宽度
    private static final float IMAGE_WIDTH = Utils.dp2px(100);
    // 图片的偏移位置
    private static final float IMAGE_OFFSET = Utils.dp2px(80);

    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    // 头像
    Bitmap bitmap;

    @Override
    protected void onDraw(Canvas canvas) 
        super.onDraw(canvas);

        canvas.drawBitmap(bitmap, getWidth() - IMAGE_WIDTH, IMAGE_OFFSET, paint);
    

绘制文字

绘制文字是图文混排的重头戏。我们需要在确保不会和图片重合的前提下,将文字分布在图片的周围。同时,我们还需要让文字和合适的地方自动换行,确保不会绘制到屏幕外边去。

如上图所示,我们需要将文字绘制在红色区域。那么我们开始绘制吧。


绘制中,有几个关键的问题

  1. 如何计算/获取将要绘制的文本的宽高?
  2. 如何计算第N行的将要绘制文本的起始位置?
  3. 如何计算将要绘制文本的位置所允许的最大宽度和字符数量?

让我们带着问题去学习吧。

文本宽高获取

文本的宽高的获取可以通过android.graphics.Paint.FontMetrics来实现。


FontMetrics是个相对专业的工具类,可以通过android.graphics.Paint#getFontMetrics来获取。FontMetrics提供了几个文字排印方面的数值:ascentdescenttopbottom,leading。

图片引用自:HenCoder Android 开发进阶:自定义 View 1-3 drawText() 文字的绘制
字段解释说明
ascent / descent限制普通字符的顶部和底部范围。
baseline作为文字显示的基准线
top / bottom限制所有字形( glyph )的顶部和底部范围。
leading行的额外间距,即对于上下相邻的两行,上行的 bottom 线和下行的 top 线的距离。
即上图中 第一行的红线第二行的蓝线 的距离。

/**
 * The Paint class holds the style and color information about how to draw
 * geometries, text and bitmaps.
 */
public class Paint 
	/**
     * Class that describes the various metrics for a font at a given text size.
     * Remember, Y values increase going down, so those values will be positive,
     * and values that measure distances going up will be negative. This class
     * is returned by getFontMetrics().
     */
    public static class FontMetrics 
        /**
         * The maximum distance above the baseline for the tallest glyph in
         * the font at a given text size.
         */
        public float   top;
        /**
         * The recommended distance above the baseline for singled spaced text.
         */
        public float   ascent;
        /**
         * The recommended distance below the baseline for singled spaced text.
         */
        public float   descent;
        /**
         * The maximum distance below the baseline for the lowest glyph in
         * the font at a given text size.
         */
        public float   bottom;
        /**
         * The recommended additional space to add between lines of text.
         */
        public float   leading;
    

测量文字宽度

具体可以使用android.graphics.Paint#breakText方法:

/**
 * The Paint class holds the style and color information about how to draw
 * geometries, text and bitmaps.
 */
public class Paint 
	/**
     * Measure the text, stopping early if the measured width exceeds maxWidth.
     * Return the number of chars that were measured, and if measuredWidth is
     * not null, return in it the actual width measured.
     *
     * @param text  The text to measure. Cannot be null.
     * @param start The offset into text to begin measuring at
     * @param end   The end of the text slice to measure.
     * @param measureForwards If true, measure forwards, starting at start.
     *                        Otherwise, measure backwards, starting with end.
     * @param maxWidth The maximum width to accumulate.
     * @param measuredWidth Optional. If not null, returns the actual width
     *                     measured.
     * @return The number of chars that were measured. Will always be <=
     *         abs(end - start).
     */
    public int breakText(CharSequence text, int start, int end,
                         boolean measureForwards,
                         float maxWidth, float[] measuredWidth) 
        if (text == null) 
            throw new IllegalArgumentException("text cannot be null");
        
        if ((start | end | (end - start) | (text.length() - end)) < 0) 
            throw new IndexOutOfBoundsException();
        

        if (text.length() == 0 || start == end) 
            return 0;
        
        if (start == 0 && text instanceof String && end == text.length()) 
            return breakText((String) text, measureForwards, maxWidth,
                             measuredWidth);
        

        char[] buf = TemporaryBuffer.obtain(end - start);
        int result;

        TextUtils.getChars(text, start, end, buf, 0);

        if (measureForwards) 
            result = breakText(buf, 0, end - start, maxWidth, measuredWidth);
         else 
            result = breakText(buf, 0, -(end - start), maxWidth, measuredWidth);
        

        TemporaryBuffer.recycle(buf);
        return result;
    

文本绘制的位置

具体使用android.graphics.Paint#getFontSpacing方法:

  • HenCoder Android 开发进阶:自定义 View 1-3 drawText() 文字的绘制「获取推荐的行距,即推荐的两行文字的 baseline 的距离。这个值是系统根据文字的字体和字号自动计算的。它的作用是当你要手动绘制多行文字(而不是使用 StaticLayout )的时候,可以在换行的时候给 y 坐标加上这个值来下移文字。」
图片引用自:HenCoder Android 开发进阶:自定义 View 2.2.1 float getFontSpacing()

由于“简易图文混排”中包含多行,我们仅需要使用 Paint#getFontSpacing 就可以将第 N 行的在垂直方向上的距离计算出来。具体计算公式为: V = ( N − 1 ) × F V = (N - 1) \\times F V=(N1)×F,其中, F F F表示 Paint#getFontSpacing 的值、 V V V表示第 N N N 行的垂直偏移量。水平方向上,默认从原点开始即可。


简单总结下:

  • 水平(x轴)方向上, H = 0 H = 0 H=0。其中, H H H表示第 N N N 行的水平偏移量。
  • 垂直(y轴)方向上, V = ( N − 1 ) × F V = (N - 1) \\times F V=(N1)×F F F F表示 Paint#getFontSpacing 的值、 V V V表示第 N N N 行的垂直偏移量

/**
 * The Paint class holds the style and color information about how to draw
 * geometries, text and bitmaps.
 */
public class Paint 
    /**
     * Return the recommend line spacing based on the current typeface and
     * text size.
     *
     * <p>Note that this is the value for the main typeface, and actual text rendered may need a
     * larger value because fallback fonts may get used in rendering the text.
     *
     * @return  recommend line spacing based on the current typeface and
     *          text size.
     */
    public float getFontSpacing() 
        return getFontMetrics(null);
    

    /**
     * Return the font's recommended interline spacing, given the Paint's
     * settings for typeface, textSize, etc. If metrics is not null, return the
     * fontmetric values in it.
     *
     * <p>Note that these are the values for the main typeface, and actual text rendered may need a
     * larger set of values because fallback fonts may get used in rendering the text.
     *
     * @param metrics If this object is not null, its fields are filled with
     *                the appropriate values given the paint's text attributes.
     * @return the font's recommended interline spacing.
     */
    public float getFontMetrics(FontMetrics metrics) 
        return nGetFontMetrics(mNativePaint, metrics);
    

实现代码

public class ImageTextView extends View 
    // 图片的宽度
    private static final float IMAGE_WIDTH = Utils.dp2px(100);
    // 图片的偏移位置
    private static final float IMAGE_OFFSET = Utils.dp2px(80);

    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    // 头像
    Bitmap bitmap;
    // 计算文字框的宽高
    Paint.FontMetrics fontMetrics = new Paint.FontMetrics();

	@Override
    protected void onDraw(Canvas canvas) 
        super.onDraw(canvas);

        canvas.drawBitmap(bitmap, getWidth() - IMAGE_WIDTH, IMAGE_OFFSET, paint);
        // 计算文本能绘制的最长长度
        int maxWidth;
        float textTop, textBottom, fontSpacing = paint.getFontSpacing();
        // 文本的长度
        int length = text.length();
        // 图片的底部位置
        float imgBottom = IMAGE_OFFSET + IMAGE_WIDTH;
        // 文字显示的垂直偏移量
        float verticalOffset = -fontMetrics.top;
        // 开始绘制文本
        for (int start = 0; start < length; ) 
            textTop = verticalOffset + fontMetrics.top;
            textBottom = verticalOffset + fontMetrics.bottom;
            maxWidth = textTop > imgBottom || textBottom < IMAGE_OFFSET
                    // 文字和图片不在同一行
                    ? getWidth()
                    // 文字和图片在同一行(图片和文字靠得太近,减去8dp)
                    : (int) (getWidth() - IMAGE_WIDTH - Utils.dp2px(8));
            // 计算能够绘制的文字的数量
            int count = paint.breakText(text, start, length, true, maxWidth, cutWidth);
            // 绘制文本
            canvas.drawText(text,
                    // 绘制的起始字符:text中的第N个字符
                    start,
                    // 绘制的终止字符:text中的第N+count个字符
                    start + count,
                    // x轴方向上,文字绘制的起始位置
                    0,
                    // y轴方向上,文字绘制的起始位置
                    verticalOffset, paint);
            // 累加绘制的文字数量
            start += count;
            // 推荐的两行文字的 baseline 的距离。这个值是系统根据文字的字体和字号自动计算的。
            verticalOffset += fontSpacing;
        
    

最终效果图:

  • 这一块还是需要自行跑一下才能好好理解,可以在附录后面获取所有源码╮(╯▽╰)╭。

附录

源码

public class ImageTextView extends View 
    // 图片的宽度
    private static final float IMAGE_WIDTH = Utils.dp2px(100);
    // 图片的偏移位置
    private static final float IMAGE_OFFSET = Utils.dp2px(80);

    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    // 头像
    Bitmap bitmap;
    // 计算文字框的宽高
    Paint.FontMetrics fontMetrics = new Paint.FontMetrics();

    float[] cutWidth = new float[1];
    String text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean justo sem, sollicitudin in maximus a, vulputate id magna. Nulla non quam a massa sollicitudin commodo fermentum et est. Suspendisse potenti. Praesent dolor dui, dignissim quis tellus tincidunt, porttitor vulputate nisl. Aenean tempus lobortis finibus. Quisque nec nisl laoreet, placerat metus sit amet, consectetur est. Donec nec quam tortor. Aenean aliquet dui in enim venenatis, sed luctus ipsum maximus. Nam feugiat nisi rhoncus lacus facilisis pellentesque nec vitae lorem. Donec et risus eu ligula dapibus lobortis vel vulputate turpis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; In porttitor, risus aliquam rutrum finibus, ex mi ultricies arcu, quis ornare lectus tortor nec metus. Donec ultricies metus at magna cursus congue. Nam eu sem eget enim pretium venenatis. Duis nibh ligula, lacinia ac nisi vestibulum, vulputate lacinia tortor.";

    public ImageTextView(Context context, @Nullable AttributeSet attrs) 
        super(context, attrs);
    

    
        bitmap = getAvatar((int) IMAGE_WIDTH);
        paint.setTextSize(Utils.dp2px(16));
        paint.getFontMetrics(fontMetrics);
    

    @Override
    protected void onDraw(Canvas canvas) 
        super.onDraw(canvas);

        canvas.drawBitmap(bitmap, getWidth() - IMAGE_WIDTH, IMAGE_OFFSET, paint);
        // 计算文本能绘制的最长长度
        int maxWidth;
        float textTop, textBottom, fontSpacing = paint.getFontSpacing();
        // 文本的长度
        int length = text.length();
        // 图片的底部位置
        float imgBottom = IMAGE_OFFSET + IMAGE_WIDTH;
        // 文字显示的垂直偏移量
        float verticalOffset = -fontMetrics.top;
        // 开始绘制文本
        for (int start = 0; start < length; ) 
            textTop = verticalOffset + fontMetrics.top;
            textBottom = verticalOffset + fontMetrics.bottom;
            maxWidth = textTop > imgBottom || textBottom < IMAGE_OFFSET
                    // 文字和图片不在同一行
                    ? getWidth()
                    // 文字和图片在同一行(图片和文字靠得太近,减去8dp)
                    : (int) (getWidth() - IMAGE_WIDTH - Utils.dp2px(8));
            // 计算能够绘制的文字的数量
            int count = paint.breakText(text, start, length, true, maxWidth, cutWidth);
            // 绘制文本
            canvas.drawText(text,
                    // 绘制的起始字符:text中的第N个字符
                    start,
                    // 绘制的终止字符:text中的第N+count个字符
                    start + count,
                    // x轴方向上,文字绘制的起始位置
                    0,
                    以上是关于自定义UI 简易图文混排的主要内容,如果未能解决你的问题,请参考以下文章

自定义UI 简易图文混排

自定义UI 自定义布局

自定义UI 自定义布局

自定义UI 自定义布局

自定义UI 自定义布局

自定义UI 属性动画