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”時)。首先我們需要在循環中對拼音數組進行逐個繪制,考慮到漢字位于拼音中間的問題,繪制過程為以每個拼音單元為基準進行繪制,首先進行拼音的繪制,然后繪制音調,音調位于拼音的聲母正上位置(這個時候要熟悉拼音的標法,幼兒園基礎),最后繪制漢字,漢字位于拼音的正下位置,需要對拼音單元進行測量。當完成整個遍歷時,即完成我們的整個繪制過程。如果當前行不能夠充滿寬度時,需要居中顯示。

其中細節比較多,需要讀者細細品味。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容