前言
閑來無事窗轩,來學(xué)習(xí)下自定義View的一些知識,發(fā)現(xiàn)了不少的坑,在此做下筆記給大家分享下
自定義View的分類
自定義View的實現(xiàn)方法分挺多種的崔兴,這個簡單做一下分類
-
繼承View重寫onDraw方法
Android Kotlin 自定義View的一些研究(一) - 繼承ViewGroup派生特殊的Layout
- 繼承特定的View(比如TextView)
- 繼承特定的ViewGroup
這幾種自定義View的實現(xiàn)方式有所不同五督,自然實現(xiàn)的效果也不一樣藏否,下面我們一一研究并踩踩里面的坑唄,因為篇幅較長充包,所以會分幾篇來講
那我們開始吧
繼承View重寫onDraw方法
這種方法只要用于實現(xiàn)一些不規(guī)則的效果副签,即這種效果不方便通過布局的組合方式來達到,玩玩需要靜態(tài)或者動態(tài)的顯示一些不規(guī)則的圖形基矮。很顯然這需要通過繪制的方式來實現(xiàn)淆储,即重寫onDraw方法。
這里我們來以簡單繪制的一個圓作為demo來研究吧
class CircleView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : View(context,attrs,defStyleAttr){
private var color: Int = Color.RED
private var paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
init {
paint.color = color
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
var radius = Math.min(width,height)/2f //width和height是getWidth()和getHeight()
canvas?.drawCircle(width/2f,height/2f,radius,paint)
}
}
上面代碼實現(xiàn)了一個具有圓形效果的自定義View家浇,它會在自己的中心以寬/高的最小值為直徑繪制一個紅心的實體圓
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
tools:context=".MainActivity">
<com.example.diyview.CircleView
android:id="@+id/circle_view"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#000000"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
在布局里面使用并運行一下看效果
符合我們的預(yù)期效果本砰,一個背景顏色為黑色的紅色實體圓View
然后我們更改一下布局參數(shù),為其設(shè)置20dp的margin
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
tools:context=".MainActivity">
<com.example.diyview.CircleView
android:id="@+id/circle_view"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#000000"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_margin="20dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
符合預(yù)期钢悲,那么我們再來設(shè)置下20dp的padding
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
tools:context=".MainActivity">
<com.example.diyview.CircleView
android:id="@+id/circle_view"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#000000"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_margin="20dp"
android:padding="20dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
emmmmm,沒有任何變化点额,padding屬性失效
看來繼承View來實現(xiàn)自定義view舔株,padding是默認不生效的,需要我們手動處理一下
既然不生效咖楣,那么我們可以在繪制的時候考慮一下padding即可督笆,所以對onDraw()方法稍作修改就行了
class CircleView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : View(context,attrs,defStyleAttr){
private var color: Int = Color.RED
private var paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
init {
paint.color = color
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
var viewWidth = width - paddingLeft - paddingRight
var viewHeight = height - paddingTop - paddingBottom
var radius = Math.min(viewHeight,viewWidth)/2f
canvas?.drawCircle(paddingLeft+viewWidth/2f,paddingTop+viewHeight/2f,radius,paint)
}
}
然后繼續(xù)修改布局的屬性,我們將match_parent改成wrap_content
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
tools:context=".MainActivity">
<com.example.diyview.CircleView
android:id="@+id/circle_view"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:background="#000000"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_margin="20dp"
android:padding="20dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
emmmmm....
發(fā)現(xiàn)坑了诱贿,warp_content不生效娃肿,這其實是沒有設(shè)置warp_content的默認寬高導(dǎo)致的,這個先給出解決方案珠十,至少為什么這樣稍后再說(簡短答案料扰,系統(tǒng)的控件如TextView里的warp_content生效其實是設(shè)置了默認值)
重寫onMeasure()方法
package com.example.diyview
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
class CircleView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : View(context,attrs,defStyleAttr){
private var color: Int = Color.RED
private var paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
init {
paint.color = color
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
var viewWidth = width - paddingLeft - paddingRight
var viewHeight = height - paddingTop - paddingBottom
var radius = Math.min(viewHeight,viewWidth)/2f
canvas?.drawCircle(paddingLeft+viewWidth/2f,paddingTop+viewHeight/2f,radius,paint)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//設(shè)置warp_content默認寬高為200dp
val mWidth = 200
val mHeight = 200
var widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
var widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
var heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
var heightSpceSize = MeasureSpec.getSize(heightMeasureSpec)
MeasureSpec.AT_MOST.let {//Kotlin寫法,MeasureSpec.AT_MOST用it來表示
when(true) {
widthSpecMode == it && heightSpecMode == it -> setMeasuredDimension(mWidth, mHeight)
widthSpecMode == it -> setMeasuredDimension(mWidth, heightSpceSize)
heightSpceSize == it -> setMeasuredDimension(widthSpecSize, mHeight)
}
}
}
}
解釋為什么warp_content失效,可以跳過不看
可以看到我們的解決方案是重寫了onMeasure(),所以是什么原因?qū)е碌奈覀円罅私庖幌耾nMeasure()
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
onMeasure()的代碼非常的簡潔焙蹭,但簡潔不意味的簡單晒杈,其中setMeasuredDimension()是用來設(shè)置View的寬/高測量值的,因此我們需要看的是getDefaultSize()這個方法:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
先解釋下MeasureSpec的三個枚舉值吧
- UNSPECIFIFD 父容器不對view有任何限制孔厉,要多大給多大拯钻,這種情況一般用于系統(tǒng)內(nèi)部,表示一直測量狀態(tài)
- EXACTLY 父容器已經(jīng)檢測出View說需要的精確大小撰豺,這個時候View的最終大小就是SpecSize所指定的值粪般。它對應(yīng)LayoutParams中的match_parent和具體的數(shù)組這兩種模式。
- AT_MOST 父容器指定了一個可用大小及SpecSize污桦,View的大小不能大于這個值亩歹,具體是什么得看不同View的實現(xiàn)。它對應(yīng)LayoutParams中的warp_content
所以我們只需要看AT_MOST和EXACTLY這兩種情況就可以了凡橱。
可以很容易看出小作,getDefaultSize()這個方法,他返回的大小就是measureSpec中的specSize 稼钩,但是AT_MOST顾稀,也就是warp_content的話,是什么都不返回的变抽,所以我們繼承View的話需要手動處理warp_content础拨,即給一個默認值
自定義屬性
很多情況下,自定義View僅靠系統(tǒng)提供的屬性是不夠用的绍载,所以我們需要添加自定義屬性
1. 在Values目錄下創(chuàng)建自定義屬性的XML诡宗,如attr.xml(名字隨便取),并創(chuàng)建如下文本內(nèi)容
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color"/>
<!--用法:<CircleView app:circle_color = "#00FF00" />-->
</declare-styleable>
</resources>
在上面的XML里面击儡,聲明了一個自定義屬性集合“CircleView”,在這個集合里面可以有許多自定義屬性塔沃,但這個只定義了一個格式(format)為“color”的屬性“circle_color”,這里的格式為“color”指的是顏色阳谍,除此之外蛀柴,還有其他很多格式:
-
reference 參考某一資源ID
xml自定義屬性聲明方法同color螃概,我就不重復(fù)了
<CircleView app:cirlce_background = "@drawable/圖片ID"/>
- boolean 布爾值
<CircleView app:cirlce_focusable = "true"/>
- dimension 尺寸值
<CircleView app:cirlce_layout_width = "421dp"/>
- float 浮點值
<CircleView app:cirlce_fromAlpha = "1.0"/>
- integer 整型值
<CircleView app:cirlce_framesCount = "12"/>
- string 字符串
<CircleView app:cirlce_text = "我是文本"/>
- enum 枚舉值
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circleview_orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
</declare-styleable>
</resources>
<CircleView app:circleview_orientation = "vertical"/>
- 混合類型 屬性定義時可以指定多種類型值
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name = "circleview_background" format = "reference|color" />
</declare-styleable>
</resources>
<CircleView
app:circleview_background = "@drawable/圖片ID" />
或者:
<CircleView
app:circleview_background = "#00FF00" />
- flag 位或運算
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circleview_gravity">
<flag name="top" value="0x30" />
<flag name="bottom" value="0x50" />
<flag name="left" value="0x03" />
<flag name="right" value="0x05" />
<flag name="center_vertical" value="0x10" />
</attr>
</declare-styleable>
</resources>
<CircleView app:circleview_gravity="bottom|left"/>/>
2. 在View的構(gòu)造方法里面解析自定義屬性的值并做處理
在Kotlin里面則是在init{}代碼塊做處理(相當(dāng)于在構(gòu)造方面里面,因為init{}就是在構(gòu)造時調(diào)用的)
init {
var attrs = context.obtainStyledAttributes(attrs,R.styleable.CircleView)
var mColor = attrs.getColor(R.styleable.CircleView_circle_color,Color.RED)
paint.color = mColor
attrs .recycle()
}
首先是獲取自定義屬性集合CircleView鸽疾,然后解析CircleView屬性集合中的circle_color屬性吊洼,它的id為R.styleable.CircleView_circle_color。在這一步驟中制肮,如果使用時沒有指定circle_color這個屬性冒窍,那么就會選擇紅色作為默認的顏色值,解析完自定義屬性之后豺鼻,通過recycle()方法來釋放資源综液。
3. 在布局文件使用自定義屬性
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
tools:context=".MainActivity">
<com.example.diyview.CircleView
android:id="@+id/circle_view"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:background="#000000"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:circle_color="@color/colorPrimary"
android:layout_margin="20dp"
android:padding="20dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
效果圖