Android 自定义带拼音的TextView

主要功能:

1. 汉字带拼音

2. 关键词不同颜色显示

示例:

Note:此代码为作者为特定项目而使用的,如果想要复用请根据实际情况修改! 

 Java 自定义TextView代码:

package com.example.test.pinyin;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;

import com.example.test.R;

import java.util.ArrayList;

public class PinyinTextView extends androidx.appcompat.widget.AppCompatTextView {


    private int fontSize;
    private String[] pinyin;

    private String[] hanzi;

    private int color;
    private int keyWordColor = Color.rgb(0, 0, 255);
    private String keyWord;
    private int keyWordIndex = -1;
    private TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    //`TextPaint`是Android中的一个类,它继承自`Paint`类,并提供了一些额外的方法和属性,专门用于在绘制文本时进行设置和处理。
    //
    //`TextPaint`类提供了一些与文本相关的功能,例如:
    //
    //1. 文本大小和样式:`TextPaint`提供了`setTextSize()`方法来设置文本的大小,`setTypeface()`方法来设置文本的字体样式(如粗体、斜体等)。
    //
    //2. 文本颜色和效果:`TextPaint`提供了`setColor()`方法来设置文本的颜色,`setUnderlineText()`和`setStrikeThruText()`方法来设置文本的下划线和删除线效果。
    //
    //3. 文本测量和布局:`TextPaint`提供了`measureText()`方法来测量文本的宽度,`breakText()`方法来根据给定的宽度截断文本,以及一些其他的文本布局相关方法。
    private Paint.FontMetrics fontMetrics;
    //`Paint.FontMetrics`是Android中的一个类,它用于描述文本绘制时的字体度量信息。通过`Paint`类的`getFontMetrics()`方法,可以获取一个`Paint.FontMetrics`对象,该对象包含了关于字体的各种度量信息。
    //
    //`Paint.FontMetrics`类提供了以下属性:
    //
    //1. `ascent`:文本基线以上的最大距离,包括文本的上坡度和上方的空间。
    //
    //2. `descent`:文本基线以下的最大距离,包括文本的下坡度和下方的空间。
    //
    //3. `top`:文本的最高点到基线的距离,包括上坡度和上方的空间。
    //
    //4. `bottom`:文本的最低点到基线的距离,包括下坡度和下方的空间。
    //
    //5. `leading`:推荐的行间距,即行与行之间的额外空间。
    //
    //这些度量信息可以用于调整和布局文本绘制的位置和间距。例如,可以使用`ascent`和`descent`的差值来计算文本的高度,或者使用`leading`来设置行间距。
    private final int paddingTop = 0;
    private final int minHeight = 144;

    private ArrayList<Integer> indexList = new ArrayList<>();    // 存储每行首个String位置
    int line = 1;
    float density;


    public PinyinTextView(Context context) {
        this(context, null);
    }

    public PinyinTextView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PinyinTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PinyinTextView);
        color = typedArray.getColor(R.styleable.PinyinTextView_customColor, getCurrentTextColor());
        fontSize = (int) typedArray.getDimension(R.styleable.PinyinTextView_customTextSize, getTextSize());
        //回收资源
        typedArray.recycle();

        initTextPaint();
    }

    public void initTextPaint() {
        textPaint.setColor(color);
        density = getResources().getDisplayMetrics().density;//返回dpi单位
        textPaint.setStrokeWidth(density * 2);
        textPaint.setTextSize(fontSize);
        fontMetrics = textPaint.getFontMetrics();

        //1. `textPaint.getFontMetrics()`返回的是`FontMetrics`对象,其中包含了一些浮点数值,表示文本绘制
        // 时的字体度量信息。`FontMetrics`的字段包括`top`、`ascent`、`descent`、`bottom`和`leading`,
        // 它们分别表示字体的顶部、上坡度、下坡度、底部和行间距。
        //
        //2. `textPaint.getFontMetricsInt()`返回的是`FontMetricsInt`对象,其中包含了一些整型数值,
        // 表示文本绘制时的字体度量信息。`FontMetricsInt`的字段包括`top`、`ascent`、`descent`、`bottom`和
        // `leading`,它们的精度相对较高,可以直接用于像素级别的计算。

    }


    public void setPinyin(String[] pinyin) {
        this.pinyin = pinyin;
    }

    public void setHanzi(String[] hanzi) {
        this.hanzi = hanzi;
    }

    public void setColor(int color) {
        this.color = color;
        if (textPaint != null) {
            textPaint.setColor(color);
        }
    }

    public void setFontSize(int size) {
        this.fontSize = size;
        if (textPaint != null) {
            textPaint.setTextSize(size);
        }
    }

    public void setKeyWordColor(int color) {
        this.keyWordColor = color;
    }

    public void setKeyWord(String keyWord) {
        this.keyWord = keyWord;

    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //`onMeasure(int widthMeasureSpec, int heightMeasureSpec)`是`TextView`类中的一个方法,用于测量`TextView`的尺寸。
        //
        //在Android中,测量视图的尺寸是在视图绘制过程中的一个重要步骤。当`TextView`的尺寸需要确定时,系统会调用`onMeasure()`方法来测量其宽度和高度。
        //
        //`onMeasure()`方法接收两个参数:`widthMeasureSpec`和`heightMeasureSpec`,它们是32位的整数值,包含了测量模式和测量尺寸的信息。
        //
        //测量模式有三种:
        //- `MeasureSpec.EXACTLY`:表示精确的尺寸要求,通常是使用固定的数值或`match_parent`属性指定的尺寸。
        //- `MeasureSpec.AT_MOST`:表示最大尺寸限制,通常是使用`wrap_content`属性指定的尺寸。
        //- `MeasureSpec.UNSPECIFIED`:表示没有限制,通常在父容器不对子视图施加限制时使用。
        //
        //测量尺寸是一个32位整数值,可以通过`MeasureSpec.getSize()`方法获取实际的尺寸值。
        //
        //在`onMeasure()`方法中,开发者需要根据测量模式和测量尺寸来计算并设置`TextView`的宽度和高度。通常,可以使用`MeasureSpec.getMode()`方法获取测量模式,然后根据不同的模式进行相应的处理。
        //
        //例如,当测量模式为`MeasureSpec.EXACTLY`时,可以直接使用`MeasureSpec.getSize()`来设置`TextView`的尺寸;当测量模式为`MeasureSpec.AT_MOST`时,可以根据内容的大小来动态计算尺寸;当测量模式为`MeasureSpec.UNSPECIFIED`时,可以根据自身的需要来设置尺寸。
        //
        //最后,在`onMeasure()`方法中,使用`setMeasuredDimension()`方法来设置测量后的宽度和高度,以便系统可以根据这些尺寸来绘制和布局`TextView`。

        // 需要根据文本测量高度
        int widthMode, heightMode;
        int width = 0, height = 0;
        indexList.clear();
        widthMode = MeasureSpec.getMode(widthMeasureSpec);
        heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //计算宽度
        if (widthMode == MeasureSpec.EXACTLY) {
            width = MeasureSpec.getSize(widthMeasureSpec);
            Log.e("MyTest", "exactly " + width);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            width = 700;
            Log.e("MyTest", "at_most " + width);
        } else {
            width = 700;
            Log.e("MyTest", "unspecified " + width);
        }
        //计算高度
        if (heightMode == MeasureSpec.EXACTLY) {
            height = MeasureSpec.getSize(heightMeasureSpec);
        } else if (heightMode == MeasureSpec.AT_MOST) {
            if (textPaint != null) {
                if (pinyin != null && pinyin.length != 0) {
                    height = (int) ((pinyin.length / 10 + 1) * 1.6 * (fontMetrics.bottom - fontMetrics.top) + paddingTop);
                    //设置height为行数*行高+paddingTop
                } else if (hanzi != null) {
                    height = (int) ((fontMetrics.bottom - fontMetrics.top) + paddingTop);
                    //设置height为行高+paddingTop
                }
            }
        } else {
            //如果为 MeasureSpec.UNSPECIFIED
            if (textPaint != null) {
                if (pinyin != null && pinyin.length != 0) {
                    float pinyinWidth = 0;
                    int lineCount = 1;
                    for (int index = 0; index < pinyin.length; index++) {
                        if (TextUtils.equals(pinyin[index], "null")) {
                            pinyinWidth = pinyinWidth + textPaint.measureText(hanzi[index]);
                        } else {
                            pinyinWidth = pinyinWidth + textPaint.measureText(pinyin[index]);
                        }
                        if (pinyinWidth > width) {
                            indexList.add(index);
                            lineCount++;
                            pinyinWidth = textPaint.measureText(pinyin[index]);
                        }
                    }
                    height = (int) Math.ceil((lineCount * 2) * (textPaint.getFontSpacing() + density * 1));
                    //向上取整 getFontSpacing() 返回以像素为单位的行间距
                } else if (hanzi != null) {
                    height = (int) textPaint.getFontSpacing();
                }
            }
        }
        height = Math.max(height, minHeight);
        setMeasuredDimension(width, height);
        //`onMeasure()` 方法是用来测量视图的宽度和高度的。在自定义 `TextView` 中,您可以根据自己的需求重写 `onMeasure()` 方法,并在其中调用 `setMeasuredDimension(width, height)` 来设置测量后的宽度和高度。

    }

    private float pinyinWidth = 0;
    private boolean isCenterHorizontal = false;

    @Override
    protected void onDraw(Canvas canvas) {
        //keyword
        keyWordIndex = getKeyWordIndex(keyWord, hanzi);
        float widthMeasure = 0f;
        if (indexList.isEmpty()) {
            // 单行数据处理
            if (isCenterHorizontal) {
                if (pinyin != null && pinyin.length > 0) {
                    widthMeasure = (getWidth() - textPaint.measureText(combinePinyinEnd(0, pinyin.length))) / 2;
                    //得到剩余宽度的一半
                    Log.e("MyTest", "widthMeasure1: " + widthMeasure);
                } else if (hanzi != null && hanzi.length > 0) {
                    widthMeasure = (getWidth() - textPaint.measureText(combineHanziEnd(0, hanzi.length))) / 2;
                }
            }

        }
        pinyinWidth = 0;
        line = 1;
        int trimIndex = 0;
        if (pinyin != null && pinyin.length > 0) {
            for (int index = 0; index < pinyin.length; index++) {
                if (!TextUtils.equals(pinyin[index], "null") && !TextUtils.equals(pinyin[index], " ")) {
                    pinyinWidth = widthMeasure + textPaint.measureText(pinyin[index]);
                    if (pinyinWidth > getWidth()) {
                        line++;
                        widthMeasure = 0;
                        trimIndex = index;
                    }
                    Log.e("MyTest", "widthMeasure2: " + index + widthMeasure);
                    if (keyWordIndex != -1 && keyWord != null) {
                        if (keyWordIndex <= index && index <= keyWordIndex + keyWord.length() - 1) {
                            textPaint.setColor(keyWordColor);
                        } else {
                            textPaint.setColor(color);
                        }
                    }

                    if (index == trimIndex) {
                        String trimmed = pinyin[index].replaceFirst("^\\s+", "");
                        canvas.drawText(trimmed, widthMeasure, (line * 2 - 1) * (textPaint.getFontSpacing()), textPaint);
                        canvas.drawText(hanzi[index], widthMeasure + (textPaint.measureText(trimmed) - textPaint.measureText(hanzi[index])) / 2 - moveHalfIfNeed(trimmed, textPaint), (line * 2) * (textPaint.getFontSpacing()), textPaint);
                        widthMeasure = widthMeasure + textPaint.measureText(trimmed);
                    } else {
                        canvas.drawText(pinyin[index], widthMeasure, (line * 2 - 1) * (textPaint.getFontSpacing()), textPaint);
                        canvas.drawText(hanzi[index], widthMeasure + (textPaint.measureText(pinyin[index]) - textPaint.measureText(hanzi[index])) / 2 - moveHalfIfNeed(pinyin[index], textPaint), (line * 2) * (textPaint.getFontSpacing()), textPaint);
                        widthMeasure = widthMeasure + textPaint.measureText(pinyin[index]);
                    }


                } else if (TextUtils.equals(pinyin[index], "null")) {

                    float hanziWidth = widthMeasure + textPaint.measureText(hanzi[index]);
                    if (hanziWidth > getWidth()) {
                        line++;
                        widthMeasure = 0;
                    }
                    canvas.drawText(hanzi[index], widthMeasure, (line * 2) * textPaint.getFontSpacing(), textPaint);
                    widthMeasure = widthMeasure + textPaint.measureText(hanzi[index]);

                }
            }
        }
        super.onDraw(canvas);
    }

    private float moveHalfIfNeed(String pinyinUnit, TextPaint paint) {

        if (pinyinUnit.trim().length() % 2 == 0) {
            return paint.measureText(" ") / 2;
        } else {
            return 0;
        }
    }

    private String combinePinyinEnd(int index, int length) {
        StringBuilder sb = new StringBuilder();
        for (int subIndex = index; subIndex < length; subIndex++) {
            String pendString = pinyin[subIndex];
            sb.append(pendString);
        }
        return sb.toString();
    }

    private String combineHanziEnd(int index, int length) {
        StringBuilder sb = new StringBuilder();
        for (int subIndex = index; subIndex < length; subIndex++) {
            sb.append(hanzi[subIndex]);
        }
        return sb.toString();
    }


    public static String[] splitHanziString(String str) {

        ArrayList<String> arrayList = new ArrayList<>();

        int i = 0;
        while (i < str.length()) {
            char c = str.charAt(i);
            if (Character.isDigit(c) || (Character.isLetter(c) && Character.UnicodeBlock.of(c) == Character.UnicodeBlock.BASIC_LATIN)) {
                StringBuilder sb = new StringBuilder();
                sb.append(c);
                while (i + 1 < str.length()) {
                    char nextChar = str.charAt(i + 1);
                    if ((Character.isDigit(c) && Character.isDigit(nextChar)) || ((Character.isLetter(c) && Character.UnicodeBlock.of(c) == Character.UnicodeBlock.BASIC_LATIN) && (Character.isLetter(nextChar) && Character.UnicodeBlock.of(nextChar) == Character.UnicodeBlock.BASIC_LATIN))) {
                        sb.append(nextChar);
                        i++;
                    } else {
                        break;
                    }
                }
                arrayList.add(sb.toString());
            } else {
                arrayList.add(String.valueOf(c));
            }
            i++;
        }

        return arrayList.toArray(new String[0]);
    }

    public static String[] splitPinyin(String str) {
        String[] s = str.trim().split(" ");
        ArrayList<String> arrayList = new ArrayList<>();
        for (String s1 : s) {
            if (!s1.equals(" ") && !s1.equals("")) {
                arrayList.add(s1);
            }
        }

        return arrayList.toArray(new String[0]);
    }

    public static String[] getFormattedPinyin(String pinyin, String[] hanzi) {
        String[] splitPinyin = splitPinyin(pinyin);
        ArrayList<String> newPinyin = new ArrayList<>();
        ArrayList<String> formattedPinyin = new ArrayList<>();
        int i = 0;
        for (String item : hanzi) {
            if (isChineseCharacter(item)) {
                newPinyin.add(splitPinyin[i]);
                i++;

            } else {
                newPinyin.add("null");
            }
        }
        for (String s : newPinyin) {
            if (!s.equals("null")) {
                formattedPinyin.add(formatUnit(s));
            } else {
                formattedPinyin.add("null");
            }

        }
        return formattedPinyin.toArray(new String[0]);
    }

    private static boolean isChineseCharacter(String str) {
        if (str.length() == 1) {
            char c = str.charAt(0);
            return isChineseChar(c);
        }
        return false;
    }

    private static boolean isChineseChar(char c) {
        Character.UnicodeBlock block = Character.UnicodeBlock.of(c);
        return block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
                || block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A
                || block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B
                || block == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
                || block == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT;
    }

    private static String formatUnit(String unit) {
        String result = unit;
        switch (unit.length()) {
            case 1:
                result = "  " + result + "   ";
                break;
            case 2:
                result = "  " + result + "  ";
                break;
            case 3:
                result = " " + result + "  ";
                break;
            case 4:
                result = " " + result + " ";
                break;
            case 5:
                result = result + " ";
                break;
        }
        return result;
    }

    private static int getKeyWordIndex(String keyword, String[] hanzi) {
        for (int i = 0; i < hanzi.length; i++) {
            int flag = 0;
            for (int j = 0; j < keyword.length(); j++) {
                if (i + j < hanzi.length) {
                    if (hanzi[i + j].equals(String.valueOf(keyword.charAt(j)))) {
                        flag++;
                    }
                }

            }
            if (flag == keyword.length()) {
                return i;
            }
        }
        return -1;
    }
}

java 设置代码

String text = "一年有365天,我在IT行业工作,8乘以8等于64。";
String[] hanzi=PinyinTextView.splitHanziString(text);
String[] pinyin=PinyinTextView.getFormattedPinyin("yī nián yǒu tiān wǒ zài háng yè gōng zuò chéng yǐ děng yú",hanzi);

textView.setHanzi(hanzi);
textView.setColor(Color.rgb(0,255,0));
textView.setFontSize(35);
textView.setPinyin(pinyin);
textView.setKeyWord("行业");
textView.setKeyWordColor(Color.rgb(205,5,255));

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值