Bootstrap

【安卓笔记】实现自定义TextView两边对齐,适配多语言方案(kotlin)

一、背景

安卓源生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"
        />
三、翻译文本微处理原则
1.遇到换行要添加换行符“\n”,加到句首。

在这里插入图片描述

2.遇到单引号或双引号要加斜杠“\”,否则是无法在ui显示中看到这两种符号的。如有特殊符号无法显示时也可用加斜杠的方式解决。
3.空格的表达在android studio中尽量不要有[NBSP]的文本字样。具体的可以搜一下non-break space是做什么的,如下图所示:

在这里插入图片描述

有的时候从Excel或word中粘贴过来的文案会用[NBSP]代替空格,有这样的空格会导致文本换行时从单词的中间开始换行
四、注意项
1.文本处理流程:

在这里插入图片描述

2.新增多语言翻译时,要在判断语言组成类型的两个方法(languageChineseType, languageArabicType)中看是否要添加新的判断
;