android實(shí)現(xiàn)帶拼音的自定義TextView

之前由于產(chǎn)品需求變更,需要實(shí)現(xiàn)帶拼音的文本框的功能,下面將整個(gè)實(shí)現(xiàn)過程簡單做一下總結(jié):

我們先來看下效果圖:

單行顯示.jpg
多行顯示.jpg

要實(shí)現(xiàn)這樣的功能對(duì)于初學(xué)者來說,可能有一定的難度。甚至對(duì)于工作好幾年的人來說,也可能沒那么容易。下面我簡單做一下梳理:

1.下載與引用:

這里主要使用到了一個(gè)漢語轉(zhuǎn)拼音的jar包,當(dāng)前版本為2.5.0,下載地址:http://download.csdn.net/download/lmj623565791/7161713,當(dāng)完成拼音的下載時(shí),在build.gradle文件中進(jìn)行jar文件的引用:

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

pinyin4j.jar的使用過程也比較簡單,當(dāng)我們輸入一個(gè)漢字時(shí),會(huì)給我們輸出一個(gè)拼音的字符串?dāng)?shù)組,而數(shù)組的長度代表該漢字有多少個(gè)多音字,會(huì)默認(rèn)根據(jù)使用頻率進(jìn)行數(shù)組排序,實(shí)現(xiàn)如下:

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";  // 非漢字字符,如標(biāo)點(diǎn)符號(hào)
                    continue;
                } else {
                    pinyin[index] = formatCenterUnit(pinyinUnit[0].substring(0, pinyinUnit[0].length() - 1)) +
                            pinyinUnit[0].charAt(pinyinUnit[0].length() - 1);  // 帶音調(diào)且長度固定為7個(gè)字符長度,,拼音居中,末尾優(yōu)先
                    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);

分別表示返回的拼音字母為小寫,并帶有聲調(diào),聲調(diào)用數(shù)字表示。

該段代碼主要功能實(shí)現(xiàn)為將漢字字符串轉(zhuǎn)化成拼音的功能,首先會(huì)遍歷漢字中的每個(gè)字符,當(dāng)字符不為漢字時(shí)(如標(biāo)點(diǎn)符號(hào)),這個(gè)時(shí)候會(huì)返回null,當(dāng)返回結(jié)果為null時(shí),我們使用"null"字符串來標(biāo)記它,表示一個(gè)不帶拼音的字符;當(dāng)字符為漢字時(shí),我們使用它的第一個(gè)拼音單元來表示,這里會(huì)固定拼音的長度為7個(gè)字符長度(最大拼音長度 + 拼音與拼音之間的空格),最后一個(gè)字符表示它的音調(diào)。返回結(jié)果即為格式化后的拼音數(shù)組。

格式化拼音代碼如下:

// 每個(gè)拼音單元長度以7個(gè)字符長度為標(biāo)準(zhǔn),拼音居中,末尾優(yōu)先
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;
}

另外,為了防止?jié)h字為空以及與拼音對(duì)應(yīng),我們同時(shí)也對(duì)漢字做格式化處理如下:

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;
    }
}

而在使用時(shí),我們只需要將格式化后的拼音與漢字傳給我們自己定義的TextView即可:

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

這里傳進(jìn)去的參數(shù)即為文本信息。

3.我們接下來看自定義TextView中的實(shí)現(xiàn):

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<>(); // 統(tǒng)計(jì)標(biāo)點(diǎn)長度

private ArrayList<Integer> indexList = new ArrayList<>();    // 存儲(chǔ)每行首個(gè)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) {
    // 需要根據(jù)文本測量高度
    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()) {
        // 單行數(shù)據(jù)處理
        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;  // 去掉數(shù)字和空格符
                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同時(shí)存在規(guī)則
                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) {
                    // 沒有聲母存在時(shí),stateIndex一直為-1 ('嗯' 轉(zhuǎn)成拼音后變成 ng,導(dǎo)致沒有聲母存在,stateIndex一直為-1,數(shù)組越界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);  // 由于拼音長度固定,采用居中顯示策略,計(jì)算拼音實(shí)際長度不需要去掉拼音后面空格
                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));    // 下個(gè)字符為拼音
                }
                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();
}
}

整個(gè)PinyinTextView使用起來很簡單,但它的實(shí)現(xiàn)還是有點(diǎn)復(fù)雜的,因?yàn)椴粌H涉及到我們的拼音問題,還增加了根據(jù)朗讀的速度實(shí)現(xiàn)字體變色與自動(dòng)滾動(dòng)的邏輯,這部分邏輯并不影響我們帶拼音的文本顯示,我并沒有剔除掉這部分邏輯,因?yàn)樵陂_發(fā)中你也許同樣會(huì)遇到這種不按套路出牌的產(chǎn)品經(jīng)理,這里我簡單理一下主要邏輯處理。

首先我們會(huì)根據(jù)文本內(nèi)容的高度完成對(duì)文本的寬高的測量,由于每個(gè)拼音的長度固定為6個(gè)字符(不包含拼音之間的間隔),所以拼音的長度一定是大于漢字的長度的,所以我們以拼音的寬度為基準(zhǔn)進(jìn)行測量,當(dāng)當(dāng)前拼音的總長度加上間隔在加上下一個(gè)拼音的長度大于PinyinTextView的width時(shí)(測量值,也是最終值),這個(gè)時(shí)候會(huì)換行,高度增加兩行文本的高度再加上行間距,即高度增加固定高度,通過這種方式即可得到文本框的高度。

draw過程繪制為三部分,分別為音調(diào)的繪制,拼音的繪制與漢字的繪制(包含標(biāo)點(diǎn)符號(hào)或無拼音文本的處理,即拼音為“null”時(shí))。首先我們需要在循環(huán)中對(duì)拼音數(shù)組進(jìn)行逐個(gè)繪制,考慮到漢字位于拼音中間的問題,繪制過程為以每個(gè)拼音單元為基準(zhǔn)進(jìn)行繪制,首先進(jìn)行拼音的繪制,然后繪制音調(diào),音調(diào)位于拼音的聲母正上位置(這個(gè)時(shí)候要熟悉拼音的標(biāo)法,幼兒園基礎(chǔ)),最后繪制漢字,漢字位于拼音的正下位置,需要對(duì)拼音單元進(jìn)行測量。當(dāng)完成整個(gè)遍歷時(shí),即完成我們的整個(gè)繪制過程。如果當(dāng)前行不能夠充滿寬度時(shí),需要居中顯示。

其中細(xì)節(jié)比較多,需要讀者細(xì)細(xì)品味。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容