一、背景
安卓源生TextView中只能左边对齐或右边对齐并没有两边对齐的效果,于是想到用自定义view将文本重新绘制。如果文本不仅仅是中文会怎么样呢?泰语、阿拉伯语、希伯来语、日语各种各种语言直接是否需要一个统一的方案呢?
二、实现过程
1.创建自定义TextView类:
class AlignBothTextView : AppCompatTextView {
// 是否打开左右对齐功能
private var alignBothSide = false
constructor(context: Context) : super(context) {
initAttr(context, null)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
initAttr(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
initAttr(context, attrs)
}
/**
* 初始化属性:
* 从文本框中获取自定义属性alignBothSide的值
*/
private fun initAttr(context: Context, attributeSet: AttributeSet?) {
val typedArray: TypedArray =
context.obtainStyledAttributes(attributeSet, R.styleable.AlignBothTextView)
alignBothSide = typedArray.getBoolean(R.styleable.AlignBothTextView_alignBothSide, false)
typedArray.recycle() // obtainStyledAttributes调用之后,必须要用到的
}
override fun onDraw(canvas: Canvas?) {
if (text !is String) {
super.onDraw(canvas)
} else {
paint.color = currentTextColor
for (i in 0 until layout.lineCount) { // 遍历textView中每行字符
// 记录文本框内部padding值 上左右
val lineBaseline = layout.getLineBaseline(i)
val lineStart = layout.getLineStart(i)
val lineEnd = layout.getLineEnd(i)
if (alignBothSide) { // 如果要两边对齐
// 如果i行是最后一行,无需计算直接输出
if (i == layout.lineCount - 1) {
canvas?.drawText(
text.substring(lineStart),
paddingLeft.toFloat(),
lineBaseline.toFloat(),
paint
)
break
} else {// 如果i行不是最后一行
val line = text.substring(lineStart, lineEnd)
val width = StaticLayout.getDesiredWidth(text, lineStart, lineEnd, paint)
// 2. 根据语种进行不同的绘制方法:类中文、从右往左念类阿拉伯文、类英文
if (languageChineseType()) {
drawChineseScaledText(canvas, line, lineBaseline.toFloat(), width)
} else if (languageArabicType()) {
drawArabicScaledText(canvas, line, lineBaseline.toFloat(), width)
} else {
drawScaledText(canvas, line, lineBaseline.toFloat(), width)
}
}
} else { // 如果不要两边对齐,直接绘制这一行的文本
canvas?.drawText(
text.substring(lineStart, lineEnd),
paddingLeft.toFloat(),
lineBaseline.toFloat(),
paint
)
}
}
}
}
/**
* 在canvas上绘制一行文本
* @param line 一行的字符
* @param baseLineY 基线垂直方向offset,每个字符绘制时y坐标点
* @param lineWidth 当前行的总长度
*/
private fun drawScaledText(canvas: Canvas?, line: String, baseLineY: Float, lineWidth: Float) {
if (line.isEmpty()) {
return
}
val endIndex = line.length - 1
if (line[endIndex].code == ConstCommon.ASCII_ENTER || endIndex == ConstCommon.EMPTY_LINE) { // 若本行结尾是换行符或本行无内容,则直接绘制无须处理
canvas?.drawText(line, paddingLeft.toFloat(), baseLineY, paint)
return
}
var x = paddingLeft.toFloat()
// 2.数出有多少个空格
var sum = getSpaceCount(line)
// tempLine: 用于存储接下来要开始绘制的文本主要是看最后一个是不是空格
val tempLine = if (line[endIndex].code == ConstCommon.ASCII_SPACE) { // 最后一个字符是空格,则删掉空格
sum--
line.substring(0, line.length - 1)
} else {
line
}
// 3.根据空格数量计算每个空格的宽度
val oneTextWidth = (measuredWidth - lineWidth - paddingLeft - paddingRight) / sum
// 4.每找到一个单词,打印一次,如果后面有空格也打印出来
var i = 0
while (i < tempLine.length) {
var wordStr = ""
while ((tempLine[i].code != ConstCommon.ASCII_SPACE)
&& i < tempLine.length - 1) {
wordStr += tempLine[i]
i++
}
wordStr += tempLine[i]
var dw = StaticLayout.getDesiredWidth(wordStr, this.paint)
if (tempLine[i].code == ConstCommon.ASCII_SPACE) {
dw += oneTextWidth
} // else tempLine[i]是本行最后一个字符的情况
canvas?.drawText(wordStr, x, baseLineY, this.paint)
x += dw
i++
}
}
/**
* 从左往右念,中间没空格
* 举例:中文
*/
private fun drawChineseScaledText(
canvas: Canvas?,
line: String,
baseLineY: Float,
lineWidth: Float
) {
if (line.isEmpty()) {
return
}
var x = paddingLeft.toFloat()
val endIndex = line.length - 1
if (line[endIndex].code == ConstCommon.ASCII_ENTER || endIndex == ConstCommon.EMPTY_LINE) { // 若本行结尾是换行符或本行无内容,则直接绘制无须处理
canvas?.drawText(line, x, baseLineY, paint)
return
}
val oneTextWidth = (measuredWidth - lineWidth - paddingLeft - paddingRight) / endIndex
for (element in line) {
val textStr = element.toString()
val dw = StaticLayout.getDesiredWidth(textStr, this.paint)
canvas?.drawText(textStr, x, baseLineY, this.paint)
x += dw + oneTextWidth
}
}
/**
* 从右往左念,中间有空格
* 举例:阿拉伯语
*/
private fun drawArabicScaledText(
canvas: Canvas?,
line: String,
baseLineY: Float,
lineWidth: Float
) {
if (line.isEmpty()) {
return
}
var x = measuredWidth - paddingRight.toFloat() //找到最右边的起点
val endIndex = line.length - 1
val sum = getSpaceCount(line)
val oneTextWidth = (measuredWidth - lineWidth - paddingLeft - paddingRight) / sum
var i = 0
while (i < line.length) {
var wordStr = ""
while ((!charIsSymbol(line[i])
|| (charIsFullStop(line[i]) && i + 1 < endIndex && i - 1 >= 0 && charIsNumber(line[i+1]) && charIsNumber(line[i-1])))
&& i < line.length - 1) {
wordStr += line[i]
i++
}
var dw = StaticLayout.getDesiredWidth(wordStr, this.paint)
x -= dw
canvas?.drawText(wordStr, x, baseLineY, this.paint)
if (charIsSymbol(line[i])) { // 下一次打单词之前加一个空格,不和wordStr加在一起的原因:碰到英文时他会默认空格加在左边,与阿拉伯语相反,会导致英语一边有两个空格,另一边没有空格
dw = StaticLayout.getDesiredWidth(line[i].toString(), this.paint)
x = if (line[i].code == ConstCommon.ASCII_SPACE &&
!(line[endIndex].code == ConstCommon.ASCII_ENTER || endIndex == ConstCommon.EMPTY_LINE)
) {
x - dw - oneTextWidth // 是空格且不是最后一行中的空格,需要拓宽
} else {
x - dw
}
}
canvas?.drawText(line[i].toString(), x, baseLineY, this.paint)
i++
}
}
/**
* 语言组成:从左往右念,中间没空格
* 优先级1(15)中有: 中文、日语、泰语、中文繁体
* 未来要有新语种翻译时要加新的判断
*/
private fun languageChineseType(): Boolean {
return LanguageUtil.isChineseLanguage()
|| LanguageUtil.isJapaneseLanguage()
|| LanguageUtil.isThaiLanguage()
}
/**
* 语言组成:从右往左念,中间有空格
* 优先级1(15)中有:阿拉伯语、希伯来语
*/
private fun languageArabicType(): Boolean {
return LanguageUtil.isArabicLanguage() || LanguageUtil.isHebrewLanguage()
}
/**
* 获取字符串中的空格数量
*/
private fun getSpaceCount(input: String): Int {
return input.count { it.code == ConstCommon.ASCII_SPACE }
}
/**
* 判断当前字符是一个标点符号
* 空格、双引号、单引号、逗号、句号
*/
private fun charIsSymbol(word: Char): Boolean {
return word.code == ConstCommon.ASCII_SPACE
|| word.code == ConstCommon.ASCII_DOUBLE_QUOTES
|| word.code == ConstCommon.ASCII_SINGLE_QUOTES
|| word.code == ConstCommon.ASCII_COMMA
|| word.code == ConstCommon.ASCII_FULL_STOP
}
/**
* 判断当前字符是一个数字
*/
private fun charIsNumber(word: Char): Boolean {
return word.code >= ConstCommon.ASCII_NUM_ZERO
&& word.code <= ConstCommon.ASCII_NUM_NINE
}
private fun charIsFullStop(word: Char):Boolean{
return word.code == ConstCommon.ASCII_FULL_STOP
}
}
2.自定义TextView中用到了一个自定义属性要添加在attrs.xml中
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 定义自定义textView AlignBothTextView用到的 两边对齐属性-->
<declare-styleable name="AlignBothTextView">
<attr name="alignBothSide" format="boolean" />
</declare-styleable>
</resources>
3.用到的常数放在单例类中
(1)ASCII码常量
object ConstCommon {
/**
* ASCII码值 自定义两边对齐文本框有用到
*/
const val EMPTY_LINE = 0
const val ASCII_SPACE = 32 // ASCII码:空格
const val ASCII_Enter = 10 // ASCII码:换行符
const val ASCII_FULL_STOP = 46 // ASCII码:句号
const val ASCII_COMMA = 44 // ASCII码:句号
const val ASCII_SINGLE_QUOTES = 39 // ASCII码:单引号
const val ASCII_DOUBLE_QUOTES = 34 // ASCII码:双引号
const val ASCII_NUM_ZERO = 48 // ASCII码:0
const val ASCII_NUM_NINE = 57 // ASCII码:9
}
(2)获取系统语言的工具类
object LanguageUtil {
/**
* 返回当前系统语言
* @return String
*/
fun getLanguage():String = Locale.getDefault().language
/**
* 返回当前系统国家
* @return String
*/
fun getCountry():String = Locale.getDefault().country
/**
* 当前系统语言是否为阿拉伯语
* @return Boolean
*/
fun isArabicLanguage():Boolean = getLanguage() == ConstCountryCode.LANGUAGE_AR
/**
* 当前系统语言是否为英语
* @return Boolean
*/
fun isEnglishLanguage():Boolean = getLanguage() == ConstCountryCode.LANGUAGE_EN
/**
* 当前系统语言是否为西班牙语
* @return Boolean
*/
fun isSpanishLanguage():Boolean = getLanguage() == ConstCountryCode.LANGUAGE_ES
/**
* 当前系统语言是否为法语
* @return Boolean
*/
fun isFranceLanguage():Boolean = getLanguage() == ConstCountryCode.LANGUAGE_FR
/**
* 当前系统语言是否为海地语
* @return Boolean
*/
fun isHindiLanguage():Boolean = getLanguage() == ConstCountryCode.LANGUAGE_HI
/**
* 当前系统语言是否为挪威语
* @return Boolean
*/
fun isNorwegianLanguage():Boolean = getLanguage() == ConstCountryCode.LANGUAGE_NB
/**
* 当前系统语言是否为葡萄牙语
* @return Boolean
*/
fun isPortugueseLanguage():Boolean = getLanguage() == ConstCountryCode.LANGUAGE_PT
/**
* 当前系统语言是否为泰语
* @return Boolean
*/
fun isThaiLanguage():Boolean = getLanguage() == ConstCountryCode.LANGUAGE_TH
/**
* 当前系统语言是否为乌兹别克斯坦语
* @return Boolean
*/
fun isUzbekLanguage():Boolean = getLanguage() == ConstCountryCode.LANGUAGE_UZ
/**
* 当前系统语言是否为中文
* @return Boolean
*/
fun isChineseLanguage():Boolean = getLanguage() == ConstCountryCode.LANGUAGE_ZH
/**
* 当前系统语言是否为希伯来语
* @return Boolean
*/
fun isHebrewLanguage():Boolean = getLanguage() == ConstCountryCode.LANGUAGE_IW
/**
* 当前系统语言是否为日语
* @return Boolean
*/
fun isJapaneseLanguage():Boolean = getLanguage() == ConstCountryCode.LANGUAGE_JA
}
(3)系统语言常量类
object ConstCountryCode {
/**
* 系统语言代号
*/
const val LANGUAGE_DEFAULT = "" //默认
const val LANGUAGE_EN = "en" //英语
const val LANGUAGE_AR = "ar" //阿拉伯语
const val LANGUAGE_ES = "es" //西班牙语
const val LANGUAGE_FR = "fr" //法语
const val LANGUAGE_HI = "hi" //印地语
const val LANGUAGE_NB = "nb" //挪威语
const val LANGUAGE_PT = "pt" //葡萄牙语
const val LANGUAGE_TH = "th" //泰语
const val LANGUAGE_UZ = "uz" //乌兹别克斯坦语
const val LANGUAGE_ZH = "zh" //中文
const val LANGUAGE_IW = "iw" //希伯来语
const val LANGUAGE_JA = "ja" //日语
}
4.在xml中直接引用自定义TextView
<com.xxx.alignbothtextview.AlignBothTextView
android:id="@+id/text1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:lineSpacingMultiplier="1.2"
android:text="@string/content"
app:alignBothSide="true"
/>