項(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)過修飾整理,看著不是非常好称龙,大家諒解
所有代碼如下:
<?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 - 繪制文字
好了蜓堕,這次真的沒了 ~