android实现带拼音的自定义TextView

作者: 骑着毛驴追宝马 | 来源:发表于2017-10-07 02:50 被阅读692次

    之前由于产品需求变更,需要实现带拼音的文本框的功能,下面将整个实现过程简单做一下总结:

    我们先来看下效果图:

    单行显示.jpg 多行显示.jpg

    要实现这样的功能对于初学者来说,可能有一定的难度。甚至对于工作好几年的人来说,也可能没那么容易。下面我简单做一下梳理:

    1.下载与引用:

    这里主要使用到了一个汉语转拼音的jar包,当前版本为2.5.0,下载地址:http://download.csdn.net/download/lmj623565791/7161713,当完成拼音的下载时,在build.gradle文件中进行jar文件的引用:

    compile files('libs/pinyin4j-2.5.0.jar')
    
    1. pinyin4j的使用:

    pinyin4j.jar的使用过程也比较简单,当我们输入一个汉字时,会给我们输出一个拼音的字符串数组,而数组的长度代表该汉字有多少个多音字,会默认根据使用频率进行数组排序,实现如下:

    public static String[] getPinyinString(String hanzi) {
        if (hanzi != null && hanzi.length() > 0) {
            String[] pinyin = new String[hanzi.length()];
            HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
            format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
            format.setToneType(HanyuPinyinToneType.WITH_TONE_NUMBER);
            for (int index = 0; index < hanzi.length(); index++) {
                char c = hanzi.charAt(index);
                try {
                    String[] pinyinUnit = PinyinHelper.toHanyuPinyinStringArray(c, format);
                    if (pinyinUnit == null) {
                        pinyin[index] = "null";  // 非汉字字符,如标点符号
                        continue;
                    } else {
                        pinyin[index] = formatCenterUnit(pinyinUnit[0].substring(0, pinyinUnit[0].length() - 1)) +
                                pinyinUnit[0].charAt(pinyinUnit[0].length() - 1);  // 带音调且长度固定为7个字符长度,,拼音居中,末尾优先
                        Log.e("pinyin", pinyin[index]);
                    }
                } catch (BadHanyuPinyinOutputFormatCombination badHanyuPinyinOutputFormatCombination) {
                    badHanyuPinyinOutputFormatCombination.printStackTrace();
                }
    
            }
            return pinyin;
        } else {
            return null;
        }
    }
    

    其中:

    format.setCaseType(HanyuPinyinCaseType.LOWERCASE);      
    format.setToneType(HanyuPinyinToneType.WITH_TONE_NUMBER);
    

    分别表示返回的拼音字母为小写,并带有声调,声调用数字表示。

    该段代码主要功能实现为将汉字字符串转化成拼音的功能,首先会遍历汉字中的每个字符,当字符不为汉字时(如标点符号),这个时候会返回null,当返回结果为null时,我们使用"null"字符串来标记它,表示一个不带拼音的字符;当字符为汉字时,我们使用它的第一个拼音单元来表示,这里会固定拼音的长度为7个字符长度(最大拼音长度 + 拼音与拼音之间的空格),最后一个字符表示它的音调。返回结果即为格式化后的拼音数组。

    格式化拼音代码如下:

    // 每个拼音单元长度以7个字符长度为标准,拼音居中,末尾优先
    private static String formatCenterUnit(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;
            case 6:
                result = result + " ";
                break;
        }
        return result;
    }
    

    另外,为了防止汉字为空以及与拼音对应,我们同时也对汉字做格式化处理如下:

    public static String[] getFormatHanzi(String hanzi) {
        if (hanzi != null && hanzi.length() > 0) {
            char[] c = hanzi.toCharArray();
            String[] result = new String[c.length];
            for (int index = 0; index < c.length; index++) {
                result[index] = c[index] + "";
            }
            return result;
        } else {
            return null;
        }
    }
    

    而在使用时,我们只需要将格式化后的拼音与汉字传给我们自己定义的TextView即可:

    pinyinTv.setPinyin(PinyinUtils.getPinyinString(pages.get(position - 1).getText()));
    pinyinTv.setHanzi(PinyinUtils.getFormatHanzi(pages.get(position - 1).getText()));
    

    这里传进去的参数即为文本信息。

    3.我们接下来看自定义TextView中的实现:

    public class PinyinTextView extends TextView {
    
    
    private final int fontSize = 72;  
    private String[] pinyin;
    
    private String[] hanzi;
    
    private int color = Color.rgb(99, 99, 99);
    
    private int[] colors = new int[]{Color.rgb(0x3d, 0xb1, 0x69), Color.rgb(99, 99, 99)};
    private TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    
    private Paint.FontMetrics fontMetrics;
    private final int paddingTop = 20;
    private final int lestHeight = 141;
    private int snot = 0;
    private ScrollView scrollView;
    private ArrayList<String> dots = new ArrayList<>(); // 统计标点长度
    
    private ArrayList<Integer> indexList = new ArrayList<>();    // 存储每行首个String位置
    int comlum = 1;
    float density;
    
    private TemplateItem item;
    
    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_textColor, Color.BLACK);
        
        typedArray.recycle();
    
        initTextPaint();
    }
    
    public void initTextPaint() {
        textPaint.setColor(color);
        float denity = getResources().getDisplayMetrics().density;
        textPaint.setStrokeWidth(denity * 2);
        if (item != null) {
            textPaint.setTextSize(item.getPageTextFontSize());
        }
        fontMetrics = textPaint.getFontMetrics();
        fontMetricsInt = textPaint.getFontMetricsInt();
    
        density = getResources().getDisplayMetrics().density;
    }
    
    public void setTemplateItem(TemplateItem item) {
        this.item = item;
        if (item != null) {
            initTextPaint();
        }
    }
    
    public void setPinyin(String[] pinyin) {
        this.pinyin = pinyin;
    }
    
    public void setHanzi(String[] hanzi) {
        this.hanzi = hanzi;
    }
    
    public void setColor(int color) {
        this.color = color;
        snot = 0;
        if (textPaint != null) {
            textPaint.setColor(color);
        }
    }
    
    public void setScrollEnable(boolean isScrollEnable) {
    
        Log.e("jacky", "isScrollEnable == " + isScrollEnable);
        this.isScrollEnable = isScrollEnable;
        if (isScrollEnable) {
            setMovementMethod(ScrollingMovementMethod.getInstance());
        } else {
            setMovementMethod(null);
        }
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 需要根据文本测量高度
        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);
        }
        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) * 2 * (fontMetrics.bottom - fontMetrics.top) + paddingTop);
                } else if (hanzi != null) {
                    height = (int) ((fontMetrics.bottom - fontMetrics.top) + paddingTop);
                }
            }
        } else if (height == MeasureSpec.UNSPECIFIED) {
            if (textPaint != null) {
                if (pinyin != null && pinyin.length != 0) {
                    float pinyinWidth = 0;
                    int comlumTotal = 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].substring(0, pinyin[index].length() - 1));
                        }
                        if (pinyinWidth > width) {
                            indexList.add(index);
                            comlumTotal++;
                            pinyinWidth = (TextUtils.equals(pinyin[index], "null") ?
                                    textPaint.measureText(pinyin[index]) : textPaint.measureText(pinyin[index].substring(0, pinyin[index].length() - 1)));
                        }
                    }
                    height = (int) Math.ceil((comlumTotal * 2) * (textPaint.getFontSpacing() + density * 1));
                } else if (hanzi != null) {
                    height = (int) textPaint.getFontSpacing();
                }
            }
        }
        height = height < lestHeight ? lestHeight : height;
        setMeasuredDimension(width, height);
    }
    
    private int snotMark = 0;
    
    private void scrollByUser(int snot, boolean isByUser) {
        if (snotMark != snot && !isByUser && scrollView != null) {
            scrollView.smoothScrollBy(0, (int) ((fontMetrics.bottom - fontMetrics.top) * 2) + 10);
            dots.clear();
        }
        this.snotMark = snot;
    }
    
    public void startScrolling(int snot) {
        if (snotMark != snot && scrollView != null) {
            scrollView.smoothScrollTo(0, 0);
            snot = 0;
            dots.clear();
        }
        this.snotMark = snot;
    }
    
    private int snotDrawMark = 0;
    private float pinyinWidth = 0;
    
    @Override
    protected void onDraw(Canvas canvas) {
        float widthMesure = 0f;
        if (indexList.isEmpty()) {
            // 单行数据处理
            if (pinyin != null && pinyin.length > 0) {
                widthMesure = (getWidth() - textPaint.measureText(combinePinEnd(0, pinyin.length))) / 2;
                Log.e("jacky", "widthMesure1 === " + widthMesure);
            } else if (hanzi != null && hanzi.length > 0) {
                widthMesure = (getWidth() - textPaint.measureText(combineHanziEnd(0, hanzi.length))) / 2;
            }
        }
        int count = 0;
        pinyinWidth = 0;
        comlum = 1;
        if (pinyin != null && pinyin.length > 0) {
            for (int index = 0; index < pinyin.length; index++) {
                if (snot != 0 && snot >= index) {
                    textPaint.setColor(colors[0]);
                    if (indexList.contains(snot)) {
                        scrollByUser(snot, false);
                    }
                } else {
                    textPaint.setColor(colors[1]);
                }
                if (!TextUtils.equals(pinyin[index], "null") && !TextUtils.equals(pinyin[index], " ")) {
                    pinyinWidth = widthMesure + textPaint.measureText(pinyin[index].substring(0, pinyin[index].length() - 1));
                    if (pinyinWidth > getWidth()) {
                        comlum++;
                        widthMesure = 0;
                        // 多行考虑最后一行居中问题
                        if (indexList.size() > 1 && indexList.get(indexList.size() - 1) == index) {
                            // 最后一行
                            widthMesure = (getWidth() - textPaint.measureText(combinePinEnd(index, pinyin.length))) / 2;
                        }
                    }
                    Log.e("jacky", "widthmeasure2 === " + widthMesure);
                    canvas.drawText(pinyin[index].substring(0, pinyin[index].length() - 1), widthMesure, (comlum * 2 - 1) * (textPaint.getFontSpacing()), textPaint);
                    String tone = " ";
                    switch (pinyin[index].charAt(pinyin[index].length() - 1)) {
                        case '1':
                            tone = "ˉ";
                            break;
                        case '2':
                            tone = "ˊ";
                            break;
                        case '3':
                            tone = "ˇ";
                            break;
                        case '4':
                            tone = "ˋ";
                            break;
                    }
                    int toneIndex = pinyin[index].length() - 3;  // 去掉数字和空格符
                    int stateIndex = -1;
                    for (; toneIndex >= 0; toneIndex--) {
                        if (pinyin[index].charAt(toneIndex) == 'a' || pinyin[index].charAt(toneIndex) == 'e'
                                || pinyin[index].charAt(toneIndex) == 'i' || pinyin[index].charAt(toneIndex) == 'o'
                                || pinyin[index].charAt(toneIndex) == 'u' || pinyin[index].charAt(toneIndex) == 'v') {
                            if (stateIndex == -1 || pinyin[index].charAt(toneIndex) < pinyin[index].charAt(stateIndex)) {
                                stateIndex = toneIndex;
                            }
                        }
                    }
                    // iu同时存在规则
                    if (pinyin[index].contains("u") && pinyin[index].contains("i") && !pinyin[index].contains("a") && !pinyin[index].contains("o") && !pinyin[index].contains("e")) {
                        stateIndex = pinyin[index].indexOf("u") > pinyin[index].indexOf("i") ? pinyin[index].indexOf("u") : pinyin[index].indexOf("i");
                    }
                    Log.e("jacky", "stateIndex === " + stateIndex);
                    if (stateIndex != -1) {
                        // 没有声母存在时,stateIndex一直为-1 ('嗯' 转成拼音后变成 ng,导致没有声母存在,stateIndex一直为-1,数组越界crash)
                        canvas.drawText(tone, widthMesure + textPaint.measureText(pinyin[index].substring(0, stateIndex)) + (textPaint.measureText(pinyin[index].charAt(stateIndex) + "") - textPaint.measureText(tone + "")) / 2, (comlum * 2 - 1) * (textPaint.getFontSpacing()), textPaint);
                    }
                    canvas.drawText(hanzi[index], widthMesure + (textPaint.measureText(pinyin[index].substring(0, pinyin[index].length() - 1)) - textPaint.measureText(hanzi[index])) / 2 - moveHalfIfNeed(pinyin[index].substring(0, pinyin[index].length() - 1), textPaint), (comlum * 2) * (textPaint.getFontSpacing()), textPaint);  // 由于拼音长度固定,采用居中显示策略,计算拼音实际长度不需要去掉拼音后面空格
                    if (index + 1 < pinyin.length && TextUtils.equals("null", pinyin[index + 1])) {
                        widthMesure = widthMesure + textPaint.measureText(pinyin[index].substring(0, pinyin[index].length() - 1));
                    } else {
                        widthMesure = widthMesure + textPaint.measureText(pinyin[index].substring(0, pinyin[index].length() - 1));    // 下个字符为拼音
                    }
                    if (index % 10 == 0 && index >= 10 && textPaint.getColor() == colors[1]) {
                    }
                    count = count + 1; // 有效拼音
                } else if (TextUtils.equals(pinyin[index], "null")) {  //   (count / 10) * 100 + 80   之前高度
    
                    if (!dots.isEmpty()) {
                        float hanziWidth = widthMesure + textPaint.measureText(hanzi[index]);
                        if (hanziWidth > getWidth()) {
                            comlum++;
                            widthMesure = 0;
                        }
                        canvas.drawText(hanzi[index], widthMesure, (comlum * 2) * textPaint.getFontSpacing(), textPaint);
                        widthMesure = widthMesure + textPaint.measureText(hanzi[index]);
                    } else {
                        float hanziWidth = widthMesure + textPaint.measureText(hanzi[index]);
                        if (hanziWidth > getWidth()) {
                            comlum++;
                            widthMesure = 0;
                        }
                        canvas.drawText(hanzi[index], widthMesure, (comlum * 2) * textPaint.getFontSpacing(), textPaint);
                        widthMesure = widthMesure + textPaint.measureText(hanzi[index]);
                    }
                    count = count + 1;
                }
            }
        } else {
    
        }
        snotDrawMark = snot;
        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 combinePinEnd(int index, int length) {
        StringBuilder sb = new StringBuilder();
        for (int subIndex = index; subIndex < length; subIndex++) {
            String pendString = pinyin[subIndex].substring(0, pinyin[subIndex].length() - 1);
            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();
    }
    }
    

    整个PinyinTextView使用起来很简单,但它的实现还是有点复杂的,因为不仅涉及到我们的拼音问题,还增加了根据朗读的速度实现字体变色与自动滚动的逻辑,这部分逻辑并不影响我们带拼音的文本显示,我并没有剔除掉这部分逻辑,因为在开发中你也许同样会遇到这种不按套路出牌的产品经理,这里我简单理一下主要逻辑处理。

    首先我们会根据文本内容的高度完成对文本的宽高的测量,由于每个拼音的长度固定为6个字符(不包含拼音之间的间隔),所以拼音的长度一定是大于汉字的长度的,所以我们以拼音的宽度为基准进行测量,当当前拼音的总长度加上间隔在加上下一个拼音的长度大于PinyinTextView的width时(测量值,也是最终值),这个时候会换行,高度增加两行文本的高度再加上行间距,即高度增加固定高度,通过这种方式即可得到文本框的高度。

    draw过程绘制为三部分,分别为音调的绘制,拼音的绘制与汉字的绘制(包含标点符号或无拼音文本的处理,即拼音为“null”时)。首先我们需要在循环中对拼音数组进行逐个绘制,考虑到汉字位于拼音中间的问题,绘制过程为以每个拼音单元为基准进行绘制,首先进行拼音的绘制,然后绘制音调,音调位于拼音的声母正上位置(这个时候要熟悉拼音的标法,幼儿园基础),最后绘制汉字,汉字位于拼音的正下位置,需要对拼音单元进行测量。当完成整个遍历时,即完成我们的整个绘制过程。如果当前行不能够充满宽度时,需要居中显示。

    其中细节比较多,需要读者细细品味。

    相关文章

      网友评论

      • 清风一点云:楼主大人,跪求源码!
      • ZSGZ_AD:求源码
      • 丹丹无敌:有源码么?可以给个完整的源码么?我发现这个在“掠”这个拼音上有问题,还有自动滚动
      • ibo:哥们儿能不能发个源码。我正好需要 :sweat_smile:
      • vincent_leo:不错不错👍

      本文标题:android实现带拼音的自定义TextView

      本文链接:https://www.haomeiwen.com/subject/wgxdyxtx.html