カスタムビュー作成2:カスタムビューの属性の定義と指定方法

投稿日:  更新日:

Android SDKは様々なViewコンポーネント(TextView, Button, ImageViewなど)を含んでいます。

これだけで、十分に見栄えのあるアプリが開発できます。

ですが、全ての人やアプリの要望に対応することは難しく、アプリ開発中に「こんなViewが欲しい!」と思える場面があります。

そのような場合はカスタムビューの作成を検討してみましょう。「なければ作ってしまえ!」という訳です。

ここでは、「カスタムビューの属性の定義と指定方法」をまとめます。

※環境:Android Studio Flamingo | 2022.2.1

スポンサーリンク

ビュー(View)の属性

図に示すように、ビュー(TextView)は様々な値によって表現されています。

ビューの属性(Attributes)

この値を「ビューの属性(Attributes)」といいます。また、属性は「属性名」を持ちます。

スポンサーリンク

指定方法

表は属性の指定方法です。大きく分けて5つあります。

優先度指定単位指定対象指定先静・動
Immediate
1
各々の属性Viewクラスのプロパティ
動的
Layout
2
レイアウトファイル
静的
Style
3
複数の属性
Theme
4
Activityマニフェストファイル
Default
5
各々の属性Viewクラスのプロパティ(初期値)
※優先度:1(高)~5(低)

※Immediate:「カスタムビュー作成1:Viewの継承と…」で取り上げています。
※Layout:本記事で取り上げています。
※Style:「カスタムビュー作成4:ビューの属性をスタイル…」で取り上げています。
※Theme:同上
※Default:Viewクラスのプロパティ初期値による動作です。

スポンサーリンク

属性のタイプ

表は使用可能な属性のタイプです。

属性タイプ内容データの型属性値の例
integerInt型の数値Int20
@integer/xxxx
floatFloat型の数値Float3.14 ※fは付けない
stringSting型の文字列String"Hello"
@string/xxxx
booleanBoolean型の真理値Booleantrue, false
@bool/xxxx
dimension寸法(単位付き可)Float240dp, 240px, 20sp
@dimen/xxxx
colorARGB(32bit)の色情報Int#FF0000
@color/red
referenceリソースDrawable
Int
String
Int
Boolean
...
@drawable/xxxx
@color/xxxx
@string/xxxx
@integer/xxxx
@bool/xxxx
...
fraction割合Float50%, 25%p
@fraction/xxxx
flagsフラグのon / offIntbit0→0000_0001b (2^0=1)
bit4→0001_0000b (2^4=16)
bit4 | bit0→0001_0001b(1+16=17)
enum整数の列挙Int{ten:10, twenty:20, thirty:30}

各タイプの詳細は「カスタムビュー作成3:ビューの属性のタイプと使用例」を参照してください。

スポンサーリンク

属性の定義

属性はattrs(values/attrs.xml)ファイルに、以下のようなフォーマットで定義します。

クラス専用の属性使いまわしの属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="クラス名A">
        <attr name="属性名1" format="属性タイプ" />   ... 属性名の定義
			:
    </declare-styleable>
	<declare-styleable name="クラス名B">
        <attr name="属性名2" format="属性タイプ" />   ... 属性名の定義
			:
    </declare-styleable>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
	<attr name="属性名" format="属性タイプ" />     ... 属性名の定義
    <declare-styleable name="クラス名A">
        <attr name="属性名" />                     ... 属性名の使い回し
			:
    </declare-styleable>
	<declare-styleable name="クラス名B">
        <attr name="属性名" />                     ... 属性名の使い回し
			:
    </declare-styleable>
</resources>
 「クラス名」 

任意の名前が付けられます。

カスタムビューのクラス名にするのが良いです。attr要素で定義された属性名とカスタムビューが関連付けられて、Android Studioに認識されます。ですので、後々、便利です。

 「属性名」 

任意の名前が付けられます。

attrsファイル内で一度定義した属性名は、重複して定義できません。単独で定義された属性名を使い回すことは可能です。

 「属性タイプ」 

integer、float、stringなどが指定できます。

以下は、attrsファイルの例です。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomView">
        <attr name="prm1" format="string" />
        <attr name="prm2" format="integer" />
    </declare-styleable>
</resources>
スポンサーリンク

属性の指定

定義された属性は“http://…/res-auto”という名前空間に集約されています。これを名前空間の定義(xmlns)で“app”という接頭辞に割り当てています。

ですので、属性の指定は「app:属性名=”値”」になります。

以下は、レイアウトファイルの例です。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".MainActivity">
	
	...
    <カスタムビューのクラス名.CustomView
        android:id="@+id/customView4"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:prm1="April"
        app:prm2="2023" />
	...

</androidx.constraintlayout.widget.ConstraintLayout>
スポンサーリンク

属性の参照

コンストラクタのattrs引数はレイアウトファイルのカスタムビューに指定された全属性に対する参照先を持ちます。

attrsからインデックス(R.styleable.CustomView)を使って、カスタムビュー階層の属性(祖先のビューの属性は含まない)を取得します。

参照は属性のタイプに適した関数(getInt, getStringなど)を使用します。

class CustomView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    private var prm1: String?
    private var prm2: Int

    init {
        context.obtainStyledAttributes(                            // 属性の取得
            attrs, R.styleable.CustomView5, defStyleAttr, defStyleRes)
            .apply {
                try {
                    prm1 = getString(R.styleable.CustomView_prm1)  // 属性の参照
                    prm2 = getInt(R.styleable.CustomView_prm2, -1) // 属性の参照
                }
                finally {
                    recycle()   // 最後にTypedArrayインスタンスを開放
                }
            }
    }
	
	...
}
スポンサーリンク

例:Moon

カスタムビューの例として、「Moon(月)」を作成してみました。

Moonは3つの属性を持ちます。age属性は月の欠け具合を、noonColor・nightColor属性は月の色合いを変更できます。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="Moon">
        <attr name="age" format="float" />
        <attr name="noonColor" format="color" />
        <attr name="nightColor" format="color" />
    </declare-styleable>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="blue_moon">#D5E1FF</color>
    <color name="blood_moon">#CC5810</color>
</resources>
class Moon @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {

    var age: Float = 0.0f
        set(value) {
            field = value
            invalidate()    // onDrawの再実行を依頼
        }
    var noonColor: Int = Color.YELLOW
        set(value) {
            field = value
            invalidate()    // onDrawの再実行を依頼
        }
    var nightColor: Int = Color.YELLOW
        set(value) {
            field = value
            invalidate()    // onDrawの再実行を依頼
        }

    init {
        context.obtainStyledAttributes(
		    attrs, R.styleable.Moon, defStyleAttr, defStyleRes)
            .apply {
                try {
                    age = getFloat(R.styleable.Moon_age, 0.0f)
                    noonColor = getColor(R.styleable.Moon_noonColor, Color.YELLOW)
                    nightColor = getColor(R.styleable.Moon_nightColor, Color.DKGRAY)
                }
                finally {
                    recycle()
                }
            }
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        // Viewの描画を行う

        if(canvas == null) return
        if((canvas.width <= 0) or (canvas.height <= 0)) return

        val _width = canvas.width.toFloat()
        val _heigjt = canvas.height.toFloat()
        val _centerX = _width / 2.0f
        val _centerY = _heigjt / 2.0f
        val _diameter = if(_width < height) _width else _heigjt
        val _radius = _diameter / 2.0f

        val _paintNoon = Paint().apply { setColor(noonColor) }
        val _paintNight = Paint().apply { setColor(nightColor) }
        val _colors = intArrayOf(0x00000000, 0x00000000, 0x20000000,)
        val _stops = floatArrayOf(0.0f, 0.6f, 1.0f)
        val _shader = RadialGradient(
            _centerX, _centerY, _radius,
            _colors, _stops, Shader.TileMode.CLAMP
        )
        val _paintEdge = Paint().apply {setShader(_shader) }

        val _fullMoon = RectF(
            _centerX - _radius, _centerY - _radius,     // top, left
            _centerX + _radius, _centerY + _radius      // bottom, right
        )
        val _ageAngle = (PI.toFloat() * 2.0f) / 30.0f * age
        val _waneWidth = abs(cos(_ageAngle) * _radius)
        val _halfMoon = RectF(
            _centerX - _waneWidth, _centerY - _radius,  // top, left
            _centerX + _waneWidth, _centerY + _radius   // bottom, right
        )

        when {
            (_ageAngle <= PI * 0.5f) -> {
                canvas.drawArc(_fullMoon, 90.0f,180.0f,false, _paintNight)  // 左半分
                canvas.drawArc(_fullMoon,270.0f,180.0f,false, _paintNoon)   // 右半分
                canvas.drawArc(_halfMoon,  0.0f,360.0f,false, _paintNight)  // 右半分
            }
            (_ageAngle <= PI) -> {
                canvas.drawArc(_fullMoon, 90.0f,180.0f,false, _paintNight)  // 左半分
                canvas.drawArc(_fullMoon,270.0f,180.0f,false, _paintNoon)   // 右半分
                canvas.drawArc(_halfMoon,  0.0f,360.0f,false, _paintNoon)   // 左半分
            }
            (_ageAngle <= PI * 1.5f) -> {
                canvas.drawArc(_fullMoon, 90.0f,180.0f,false, _paintNoon)   // 左半分
                canvas.drawArc(_fullMoon,270.0f,180.0f,false, _paintNight)  // 右半分
                canvas.drawArc(_halfMoon,  0.0f,360.0f,false, _paintNoon)   // 右半分
            }
            (_ageAngle <= PI * 2.0f) -> {
                canvas.drawArc(_fullMoon, 90.0f,180.0f,false, _paintNoon)   // 左半分
                canvas.drawArc(_fullMoon,270.0f,180.0f,false, _paintNight)  // 右半分
                canvas.drawArc(_halfMoon,  0.0f,360.0f,false, _paintNight)  // 左半分
            }
            else -> {
                canvas.drawArc(_fullMoon,  0.0f,360.0f,false, _paintNight)  // 全周
            }
        }
        canvas.drawArc(_fullMoon,0.0f,360.0f,false, _paintEdge)             // 全周
    }
}

属性を定義するだけで、Android Studioのレイアウトエディタから参照できるようになります。とても便利です。

属性が参照できる

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    ...
    tools:context=".MoonActivity">

    <カスタムビューのパッケージ名.Moon
        android:id="@+id/moon_normal"
        android:layout_width="100dp"
        android:layout_height="100dp"
		...
        app:age="5.0" />

    <カスタムビューのパッケージ名.Moon
        android:id="@+id/moon_blue"
        android:layout_width="100dp"
        android:layout_height="100dp"
		...
        app:age="7.5"
        app:noonColor="@color/blue_moon" />

    <カスタムビューのパッケージ名.Moon
        android:id="@+id/moon_blood"
        android:layout_width="100dp"
        android:layout_height="100dp"
		...
        app:age="10.0"
        app:noonColor="@color/blood_moon" />

</androidx.constraintlayout.widget.ConstraintLayout>

プログラム上の扱いは既存のViewと全く変わりません。

属性は全てレイアウトファイルへ指定し、静的な表示を行っています。ですので、カスタムビューのプロパティはプログラムで操作していません。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_moon)
    }
}

色違いの月

スポンサーリンク

関連記事:

Android SDKは様々なViewコンポーネント(TextView, Button, ImageViewなど)を含んでいます。 これだけで、十分に見栄えのあるアプリが開発できます。 ですが、全ての人やアプリの要望に対応することは難しく、アプリ開発中に「こんなViewが欲しい!」と思える場面があります。 そのような場合はカスタムビューの作成を検討してみましょう。「なければ作ってしまえ!」という訳です。 ここでは「Viewの継承とonDrawの役割」をまとめます。 ※環境:Android Studio Electric Eel | 2022.1.1 ...
Android SDKは様々なViewコンポーネント(TextView, Button, ImageViewなど)を含んでいます。 これだけで、十分に見栄えのあるアプリが開発できます。 ですが、全ての人やアプリの要望に対応することは難しく、アプリ開発中に「こんなViewが欲しい!」と思える場面があります。 そのような場合はカスタムビューの作成を検討してみましょう。「なければ作ってしまえ!」という訳です。 ここでは、「カスタムビューの属性のタイプと使用例」をまとめます。 ※環境:Android Studio Flamingo | 2022.2.1 ...
Android SDKは様々なViewコンポーネント(TextView, Button, ImageViewなど)を含んでいます。 これだけで、十分に見栄えのあるアプリが開発できます。 ですが、全ての人やアプリの要望に対応することは難しく、アプリ開発中に「こんなViewが欲しい!」と思える場面があります。 そのような場合はカスタムビューの作成を検討してみましょう。「なければ作ってしまえ!」という訳です。 ここでは、「カスタムビューの属性をスタイル・テーマで指定」をまとめます。 ※環境:Android Studio Flamingo | 2022.2.1 ...
Android SDKは様々なViewコンポーネント(TextView, Button, ImageViewなど)を含んでいます。 これだけで、十分に見栄えのあるアプリが開発できます。 ですが、全ての人やアプリの要望に対応することは難しく、アプリ開発中に「こんなViewが欲しい!」と思える場面があります。 そのような場合はカスタムビューの作成を検討してみましょう。「なければ作ってしまえ!」という訳です。 ここでは、「カスタムビューの動的な配置」をまとめます。 ※環境:Android Studio Flamingo | 2022.2.1 ...
スポンサーリンク