Android Kotlin 自定義View的一些研究(一)

前言

閑來無事窗轩,來學(xué)習(xí)下自定義View的一些知識,發(fā)現(xiàn)了不少的坑,在此做下筆記給大家分享下

自定義View的分類

自定義View的實現(xiàn)方法分挺多種的崔兴,這個簡單做一下分類

這幾種自定義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>

效果圖


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市儒飒,隨后出現(xiàn)的幾起案子谬莹,更是在濱河造成了極大的恐慌,老刑警劉巖桩了,帶你破解...
    沈念sama閱讀 222,729評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件附帽,死亡現(xiàn)場離奇詭異,居然都是意外死亡井誉,警方通過查閱死者的電腦和手機士葫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來送悔,“玉大人,你說我怎么就攤上這事爪模∏菲。” “怎么了?”我有些...
    開封第一講書人閱讀 169,461評論 0 362
  • 文/不壞的土叔 我叫張陵屋灌,是天一觀的道長洁段。 經(jīng)常有香客問我,道長共郭,這世上最難降的妖魔是什么祠丝? 我笑而不...
    開封第一講書人閱讀 60,135評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮除嘹,結(jié)果婚禮上写半,老公的妹妹穿的比我還像新娘。我一直安慰自己尉咕,他們只是感情好叠蝇,可當(dāng)我...
    茶點故事閱讀 69,130評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著年缎,像睡著了一般悔捶。 火紅的嫁衣襯著肌膚如雪铃慷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,736評論 1 312
  • 那天蜕该,我揣著相機與錄音犁柜,去河邊找鬼。 笑死堂淡,一個胖子當(dāng)著我的面吹牛馋缅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播淤齐,決...
    沈念sama閱讀 41,179評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼股囊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了更啄?” 一聲冷哼從身側(cè)響起稚疹,我...
    開封第一講書人閱讀 40,124評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎祭务,沒想到半個月后内狗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,657評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡义锥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,723評論 3 342
  • 正文 我和宋清朗相戀三年柳沙,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拌倍。...
    茶點故事閱讀 40,872評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡赂鲤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出柱恤,到底是詐尸還是另有隱情数初,我是刑警寧澤,帶...
    沈念sama閱讀 36,533評論 5 351
  • 正文 年R本政府宣布梗顺,位于F島的核電站泡孩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏寺谤。R本人自食惡果不足惜仑鸥,卻給世界環(huán)境...
    茶點故事閱讀 42,213評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望变屁。 院中可真熱鬧眼俊,春花似錦、人聲如沸粟关。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至获列,卻和暖如春谷市,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背击孩。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評論 1 274
  • 我被黑心中介騙來泰國打工迫悠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人巩梢。 一個月前我還...
    沈念sama閱讀 49,304評論 3 379
  • 正文 我出身青樓创泄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親括蝠。 傳聞我的和親對象是個殘疾皇子鞠抑,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,876評論 2 361

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