android实现带拼音的自定义TextView

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

我们先来看下效果图:

单行显示.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”时)。首先我们需要在循环中对拼音数组进行逐个绘制,考虑到汉字位于拼音中间的问题,绘制过程为以每个拼音单元为基准进行绘制,首先进行拼音的绘制,然后绘制音调,音调位于拼音的声母正上位置(这个时候要熟悉拼音的标法,幼儿园基础),最后绘制汉字,汉字位于拼音的正下位置,需要对拼音单元进行测量。当完成整个遍历时,即完成我们的整个绘制过程。如果当前行不能够充满宽度时,需要居中显示。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容