之前由于產品需求變更,需要實現帶拼音的文本框的功能,下面將整個實現過程簡單做一下總結:
我們先來看下效果圖:
要實現這樣的功能對于初學者來說,可能有一定的難度。甚至對于工作好幾年的人來說,也可能沒那么容易。下面我簡單做一下梳理:
1.下載與引用:
這里主要使用到了一個漢語轉拼音的jar包,當前版本為2.5.0,下載地址:http://download.csdn.net/download/lmj623565791/7161713,當完成拼音的下載時,在build.gradle文件中進行jar文件的引用:
compile files('libs/pinyin4j-2.5.0.jar')
- 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”時)。首先我們需要在循環中對拼音數組進行逐個繪制,考慮到漢字位于拼音中間的問題,繪制過程為以每個拼音單元為基準進行繪制,首先進行拼音的繪制,然后繪制音調,音調位于拼音的聲母正上位置(這個時候要熟悉拼音的標法,幼兒園基礎),最后繪制漢字,漢字位于拼音的正下位置,需要對拼音單元進行測量。當完成整個遍歷時,即完成我們的整個繪制過程。如果當前行不能夠充滿寬度時,需要居中顯示。
其中細節比較多,需要讀者細細品味。