自定義 view 練手 - 自定義可換行的 textview

項(xiàng)目初衷


自定義 view 呢我是打算寫幾個(gè)練手的,想了想拗踢,第一個(gè)自定義 view 的練手還是實(shí)現(xiàn)一個(gè)可換行的簡單 textview 為好

剛接觸自定義 view 的同學(xué)一定會(huì)頭疼于 view 的測量和繪制,繪制是個(gè)復(fù)雜的事向臀,但是測量才是初學(xué)者們首先要玩順溜的

我們來再來看看經(jīng)典的自定義 view 測量寫法:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    // 獲取寬的測量模式
    int wSpecMode = MeasureSpec.getMode(widthMeasureSpec); 
    // 獲取符控件提供的 view 寬的最大值
    int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);

    int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);

    if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(300, 300);
    } else if (wSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(300, hSpecSize);
    } else if (hSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(wSpecSize, 300);
    }
}

自定義view 測量的難點(diǎn)就是如何處理 warpConten 的情況巢墅,在面臨 warpConten 時(shí)我們?nèi)绻蛔鎏幚砟敲?view 的寬高就是 matchParent 的,但是不是所有情況我們都能這么粗暴的解決的券膀,有時(shí)我們自定義的 view 必要要能處理 warpConten

何為處理 warpConten 君纫,就是在 warpConten 時(shí)我們根據(jù)需要繪制的內(nèi)容,計(jì)算所需的寬高大小芹彬,然后返回通知該自定義view

所以我在這里準(zhǔn)備了一個(gè) 自定義的 textview 給大家找找感覺蓄髓,另外也是熟練下 canvas 繪制文字,我認(rèn)為這是練習(xí)自定義view雀监,提高熟練度最好的開始了

我的目標(biāo)是讓萌新們跟著我一起快快樂樂双吆,簡簡單單的熟悉自定義 view眨唬,希望大家多點(diǎn)贊会前,點(diǎn)個(gè)喜歡,關(guān)注匾竿,github 給個(gè) start 啥的瓦宜,多謝大家啦,么么噠 ~~

項(xiàng)目地址:BW_Libs

CustomeTextView 思路


這里我們不需要做的很復(fù)雜和 textview 一樣岭妖,我們的目標(biāo)是實(shí)現(xiàn)一個(gè)能處理 warpConten 临庇,能實(shí)現(xiàn)文字換行,正確顯示文字的 view 即可

首先說明一下昵慌,不同的字符占用的寬度是不同的假夺,a < A < 我,所以中英文混合的字符串斋攀,每行能夠顯示的文字?jǐn)?shù)量是不同的已卷,大家可以用 mPaint.measureText() 方法自己去試試

在 view 中我們?nèi)绾巫龅轿淖值淖詣?dòng)換行,這點(diǎn)其實(shí)不復(fù)雜淳蔼,我們根據(jù) view 的最大寬度侧蘸,計(jì)算出 view 每一行能容納的字符數(shù)裁眯,然后依次繪制出每一行的文字即可。以前我把這塊想的可復(fù)雜了讳癌,以為有黑科技在里面穿稳,但是試過之后才知道,不難嘛 ~ 想的太復(fù)雜可不是好事啊

那么我們正式開始講解思路啦:

1. 處理 warpContent

面臨 warpConten 晌坤,我們需要根據(jù)設(shè)置的文字逢艘,算出所需要的寬高。這里我們要借助 mPaint.measureText(text) 這個(gè) API

寬好算骤菠,我們可以拿到 view 所能獲取到的最大寬度 maxWdith埋虹,然后用 mPaint.measureText 計(jì)算出傳入文字的寬度 textWidth

  • textWidth < maxWdith
    說明文字不足一行,我們以文字所需的寬度 textWidth 為 view 的寬度即可
  • textWidth = maxWdith
    說明文字正好一行娩怎,view 的最大寬度 maxWdith 就是 view 所需的寬度
  • textWidth > maxWdith
    說明文字一行顯示不下搔课,有多行,這時(shí)view 所需的寬度就是 view 的最大寬度 maxWdith 了
    /**
     * 計(jì)算 view 所需寬度截亦,view 的寬是 warpContent 時(shí)需要處理
     */
    fun calculateWidth(width: Int): Int {
        val measureWidth = mPaint.measureText(mText)
        return if (measureWidth >= width) width else measuredWidth
    }

高度其實(shí)也好算爬泥,我們只要知道了文字繪制的行數(shù) * 每行文字的高度,就是 view 所需的高度了

    /**
     * 計(jì)算 view 總共的高度崩瓤,view 的高是 warpContent 時(shí)需要處理
     */
    fun calculateHeight(width: Int): Int {

        val measureWidth = mPaint.measureText(mText).toInt()
        if (measureWidth <= width) {
            return mLineHeight.toInt()
        }
        return (mlinesNumber * mLineHeight).toInt()
    }

整個(gè) view 的測量方法如下:

    /**
     * 計(jì)算 view 大小
     */
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        var widthSize = MeasureSpec.getSize(widthMeasureSpec)

        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        var heightSize = MeasureSpec.getSize(heightMeasureSpec)

        // 當(dāng) view 的寬高是 warpContent 時(shí)袍啡,根據(jù)文字計(jì)算 view 所需大小
        if (widthMode == MeasureSpec.AT_MOST) {
            widthSize = calculateWidth(widthSize)
        }

        if (heightMode == MeasureSpec.AT_MOST) {
            heightSize = calculateHeight(widthSize)
        }

        // 計(jì)算文字分成幾行
        calculateLines(widthSize)
        // 設(shè)置 view 的大小
        setMeasuredDimension(widthSize, heightSize)
    }
2. 繪制文字

這里的難點(diǎn)是我們把文字分割成一行一行的,上面我們知道中英文字符占用的寬度是不一樣的却桶,view 每一樣能顯示的字符數(shù)是不固定的境输,這里我們需要?jiǎng)討B(tài)計(jì)算出每一行能顯示的文字?jǐn)?shù),然后根據(jù)這個(gè)字符數(shù)截取字符串颖系,最后再一行行的去繪制

計(jì)算每一行最大字符數(shù):

    /**
     * 根據(jù)傳入的文字嗅剖,獲取一行最多能顯示的字符數(shù)
     */
    fun getSigleLineTextNumber(text: String, width: Int, centerTextNum: Int): Int {

        // 判斷是不是最后一行,最后一行返回字符串長度
        if (text.length <= centerTextNum || mPaint.measureText(text) < width) {
            return text.length
        }

        var index = centerTextNum
        while (true) {
            // 從每行文字的中間數(shù)開始嘁扼,一個(gè)字符的一個(gè)字符的增加文字測量數(shù)信粮,一直到超過或等于指定寬度時(shí),就是 view 每行能顯示文字的字?jǐn)?shù)
            val measureWidth = mPaint.measureText(text.substring(0, index) + 0.5f).toInt()
            if (measureWidth > width) {
                return index - 1
                break
            }
            if (measureWidth == width) {
                return index
                break
            }

            index++
        }
    }

分割字符串成一行一行:

    /**
     * 分割文字成一行一行的
     * 為了減少計(jì)算量趁啸,我們算下每行文字?jǐn)?shù)量的平均數(shù)强缘,從這個(gè)平均數(shù)開始比對(duì)
     */
    fun splitText() {

        var centerTextNum = mText.length / mlinesNumber
        var text: String = mText
        while (true) {
            // 先獲取每行文字的數(shù)量
            val sigleLineTextNumber = getSigleLineTextNumber(text, width, centerTextNum)
            // 然后根據(jù)這個(gè)數(shù)量裁剪文字,把這行文字取出來不傅,
            val lineText = text.substring(0, sigleLineTextNumber)
            // 把取出的每行文字存入集合
            textList.add(lineText)
            // 然后把取出的這行文字從源文字中刪除旅掂,以便接下來的計(jì)算
            text = text.substring(sigleLineTextNumber, text.length)
            if (text.isEmpty()) break
        }
    }

恩,這里大家看注釋就行访娶,分割字符串的思路可能繞一點(diǎn)商虐,但是沒啥問題,這里我的代碼沒有經(jīng)過修飾整理,看著不是非常好称龙,大家諒解

所有代碼如下:

device-2018-10-15-012012.png
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent" android:layout_height="match_parent"
    tools:context="com.bloodcrown.bw.customeview.CustomeTextviewActivity">

    <com.bloodcrown.bw.customeview.CustomeTextView
        android:layout_width="800px"
        android:layout_height="wrap_content"
        android:text="A1111111111111111B2222222222222222C333333333333333D444444444444444E55555555555555555F66666666666666"
        android:textSize="18sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

</android.support.constraint.ConstraintLayout>
    <declare-styleable name="CustomeTextView">
        <attr name="android:textSize" tools:ignore="ResourceName"></attr>
        <attr name="android:text" tools:ignore="ResourceName"></attr>
    </declare-styleable>
class CustomeTextView : View {

    var mPaint = TextPaint()
    var mText = ""
    // 行高
    var mLineHeight: Float = 0f
    // 文字拆分行數(shù)
    var mlinesNumber: Int = 0
    // 存儲(chǔ)每行文字集合
    var textList = arrayListOf<String>()

    @JvmOverloads
    constructor(context: Context, attributeSet: AttributeSet? = null, defAttrStyle: Int = 0)
            : super(context, attributeSet, defAttrStyle) {

        // 初始化畫筆
        initPaint()
        // 初始化各種自定義參數(shù)
        initAttrs(context, attributeSet, defAttrStyle)
        // 計(jì)算行高
        mLineHeight = calculateLineHeight()
    }

    /**
     * 初始化畫筆
     */
    fun initPaint() {
        mPaint.color = Color.BLACK
        mPaint.strokeWidth = 1f
        mPaint.style = Paint.Style.FILL
        mPaint.isAntiAlias = true
    }

    /**
     * 初始化各種自定義參數(shù)
     */
    private fun initAttrs(context: Context, attributeSet: AttributeSet?, defAttrStyle: Int) {

        val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.CustomeTextView)
        (0..typedArray.indexCount)
                .asSequence()
                .map { typedArray.getIndex(it) }
                .forEach {
                    when (it) {
                    // 獲取文字內(nèi)容
                        R.styleable.CustomeTextView_android_text -> {
                            mText = typedArray.getString(R.styleable.CustomeTextView_android_text)
                        }
                    // 獲取文字大小
                        R.styleable.CustomeTextView_android_textSize -> {
                            var textSize = typedArray.getDimensionPixelSize(R.styleable.CustomeTextView_android_textSize, 0).toFloat()
                            mPaint.textSize = textSize
                        }
                    }
                }
        typedArray.recycle()
    }

    /**
     * 計(jì)算 view 大小
     */
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        var widthSize = MeasureSpec.getSize(widthMeasureSpec)

        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        var heightSize = MeasureSpec.getSize(heightMeasureSpec)

        // 當(dāng) view 的寬高是 warpContent 時(shí)留拾,根據(jù)文字計(jì)算 view 所需大小
        if (widthMode == MeasureSpec.AT_MOST) {
            widthSize = calculateWidth(widthSize)
        }

        if (heightMode == MeasureSpec.AT_MOST) {
            heightSize = calculateHeight(widthSize)
        }

        // 計(jì)算文字分成幾行
        calculateLines(widthSize)
        // 設(shè)置 view 的大小
        setMeasuredDimension(widthSize, heightSize)
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        // 把傳入的文字根據(jù) view 的寬度,分割成一行一行的鲫尊,便于繪制,我們一次只能繪制一行痴柔,多行文字就是一行行繪制出來的
        splitText()

        // 第一行文字的 baseline 的起始坐標(biāo)
        var startX = 0f
        var startY = 0f - mPaint.fontMetrics.ascent

        // 遍歷儲(chǔ)存每行文字的集合,繪制每一行文字
        for ((index, text) in textList.withIndex()) {
            canvas?.drawText(text, startX, startY + index * mLineHeight, mPaint)
        }
    }

    /**
     * 計(jì)算行高
     */
    fun calculateLineHeight(): Float {
        return -mPaint.fontMetrics.ascent + mPaint.fontMetrics.bottom
    }

    /**
     * 計(jì)算文字會(huì)分割成幾行繪制疫向,由余數(shù)的話行數(shù) +1
     */
    fun calculateLines(width: Int) {
        val measureWidth = mPaint.measureText(mText).toInt()
        mlinesNumber = if (measureWidth % width != 0) measureWidth / width + 1 else measureWidth / width
    }

    /**
     * 計(jì)算 view 所需寬度咳蔚,view 的寬是 warpContent 時(shí)需要處理
     */
    fun calculateWidth(width: Int): Int {
        val measureWidth = mPaint.measureText(mText)
        return if (measureWidth >= width) width else measuredWidth
    }

    /**
     * 計(jì)算 view 總共的高度,view 的高是 warpContent 時(shí)需要處理
     */
    fun calculateHeight(width: Int): Int {

        val measureWidth = mPaint.measureText(mText).toInt()
        if (measureWidth <= width) {
            return mLineHeight.toInt()
        }
        return (mlinesNumber * mLineHeight).toInt()
    }

    /**
     * 分割文字成一行一行的
     */
    fun splitText() {

        var centerTextNum = mText.length / mlinesNumber
        var text: String = mText
        while (true) {
            // 先獲取每行文字的數(shù)量
            val sigleLineTextNumber = getSigleLineTextNumber(text, width, centerTextNum)
            // 然后根據(jù)這個(gè)數(shù)量裁剪文字搔驼,把這行文字取出來谈火,
            val lineText = text.substring(0, sigleLineTextNumber)
            // 把取出的每行文字存入集合
            textList.add(lineText)
            // 然后把取出的這行文字從源文字中刪除,以便接下來的計(jì)算
            text = text.substring(sigleLineTextNumber, text.length)
            if (text.isEmpty()) break
        }
    }

    /**
     * 根據(jù)傳入的文字舌涨,獲取一行最多能顯示的字符數(shù)
     */
    fun getSigleLineTextNumber(text: String, width: Int, centerTextNum: Int): Int {

        // 判斷是不是最后一行糯耍,最后一行返回字符串長度
        if (text.length <= centerTextNum || mPaint.measureText(text) < width) {
            return text.length
        }

        var index = centerTextNum
        while (true) {
            // 從每行文字的中間數(shù)開始,一個(gè)字符的一個(gè)字符的增加文字測量數(shù)囊嘉,一直到超過或等于指定寬度時(shí)温技,就是 view 每行能顯示文字的字?jǐn)?shù)
            val measureWidth = mPaint.measureText(text.substring(0, index) + 0.5f).toInt()
            if (measureWidth > width) {
                return index - 1
                break
            }
            if (measureWidth == width) {
                return index
                break
            }

            index++
        }
    }


}

最后


可能大家都不會(huì)看到這里,因?yàn)檎l會(huì)在一大段代碼后面接著寫文字呢

這里我們使用 StaticLayout 來繪制多行文字的話會(huì)方面很多啊扭粱,不用我們自己去算有多少行舵鳞,不用我們自己去截取每一行的文字再去繪制了,StaticLayout 都幫我們做了琢蛤,并且通過 StaticLayout 我們可以獲取文字實(shí)際會(huì)占用的寬高是多少

// 可以獲取文字在指定寬度限制下所需空間
staticLayout.width
staticLayout.height

預(yù)知 StaticLayout 的詳細(xì)請(qǐng)看:自定義 view - 繪制文字

好了蜓堕,這次真的沒了 ~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市博其,隨后出現(xiàn)的幾起案子套才,更是在濱河造成了極大的恐慌,老刑警劉巖贺奠,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件霜旧,死亡現(xiàn)場離奇詭異,居然都是意外死亡儡率,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門以清,熙熙樓的掌柜王于貴愁眉苦臉地迎上來儿普,“玉大人,你說我怎么就攤上這事掷倔∶己ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長浪汪。 經(jīng)常有香客問我巴柿,道長,這世上最難降的妖魔是什么死遭? 我笑而不...
    開封第一講書人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任广恢,我火速辦了婚禮,結(jié)果婚禮上呀潭,老公的妹妹穿的比我還像新娘钉迷。我一直安慰自己,他們只是感情好钠署,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開白布糠聪。 她就那樣靜靜地躺著,像睡著了一般谐鼎。 火紅的嫁衣襯著肌膚如雪舰蟆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評(píng)論 1 305
  • 那天狸棍,我揣著相機(jī)與錄音夭苗,去河邊找鬼。 笑死隔缀,一個(gè)胖子當(dāng)著我的面吹牛题造,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播猾瘸,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼界赔,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了牵触?” 一聲冷哼從身側(cè)響起淮悼,我...
    開封第一講書人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎揽思,沒想到半個(gè)月后袜腥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钉汗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年羹令,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片损痰。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡福侈,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出卢未,到底是詐尸還是另有隱情肪凛,我是刑警寧澤堰汉,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站伟墙,受9級(jí)特大地震影響翘鸭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜戳葵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一就乓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧譬淳,春花似錦档址、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至浦妄,卻和暖如春尼摹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背剂娄。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來泰國打工蠢涝, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人阅懦。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓和二,卻偏偏與公主長得像,于是被迫代替她去往敵國和親耳胎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子惯吕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 前面學(xué)習(xí)了Android自定義View(一) -- 初識(shí)Android自定義View(二) -- Paint詳解 ...
    T9的第三個(gè)三角閱讀 14,685評(píng)論 0 26
  • 1、通過CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫組件 SD...
    陽明先生_X自主閱讀 15,982評(píng)論 3 119
  • 參考資料 kelin//view的繪制流程,比較詳細(xì)的過源碼 有注釋* 郭霖自定義view系列 鴻洋自定義view...
    玄策閱讀 176評(píng)論 0 2
  • 系列文章之 Android中自定義View(一)系列文章之 Android中自定義View(二)系列文章之 And...
    YoungerDev閱讀 2,232評(píng)論 0 1
  • 今天早上早起怕午,期待中废登,因?yàn)橐觥豆膭?lì)咨詢》微課活動(dòng)帶導(dǎo)。 這個(gè)活動(dòng)是我?guī)?dǎo)好多次的一個(gè)活動(dòng)郁惜。讓我自己也又重溫一遍堡距,...
    小雨秋子閱讀 447評(píng)論 0 1