Android SDKは様々なViewコンポーネント(TextView, Button, ImageViewなど)を含んでいます。
これだけで、十分に見栄えのあるアプリが開発できます。
ですが、全ての人やアプリの要望に対応することは難しく、アプリ開発中に「こんなViewが欲しい!」と思える場面があります。
そのような場合はカスタムビューの作成を検討してみましょう。「なければ作ってしまえ!」という訳です。
ここでは、「カスタムビューの属性の定義と指定方法」をまとめます。
※環境:Android Studio Flamingo | 2022.2.1
ビュー(View)の属性
図に示すように、ビュー(TextView)は様々な値によって表現されています。

この値を「ビューの属性(Attributes)」といいます。また、属性は「属性名」を持ちます。
指定方法
表は属性の指定方法です。大きく分けて5つあります。
| 優先度 | 指定単位 | 指定対象 | 指定先 | 静・動 | |
|---|---|---|---|---|---|
| Immediate | 各々の属性 | View | クラスのプロパティ | ||
| Layout | レイアウトファイル | ||||
| Style | 複数の属性 | ||||
| Theme | Activity | マニフェストファイル | |||
| Default | 各々の属性 | View | クラスのプロパティ(初期値) | ||
| ※優先度:1(高)~5(低) | |||||
※Immediate:「カスタムビュー作成1:Viewの継承と…」で取り上げています。
※Layout:本記事で取り上げています。
※Style:「カスタムビュー作成4:ビューの属性をスタイル…」で取り上げています。
※Theme:同上
※Default:Viewクラスのプロパティ初期値による動作です。
属性のタイプ
表は使用可能な属性のタイプです。
| 属性タイプ | 内容 | データの型 | 属性値の例 |
|---|---|---|---|
| integer | Int型の数値 | Int | 20 @integer/xxxx |
| float | Float型の数値 | Float | 3.14 ※fは付けない |
| string | Sting型の文字列 | String | "Hello" @string/xxxx |
| boolean | Boolean型の真理値 | Boolean | true, false @bool/xxxx |
| dimension | 寸法(単位付き可) | Float | 240dp, 240px, 20sp @dimen/xxxx |
| color | ARGB(32bit)の色情報 | Int | #FF0000 @color/red |
| reference | リソース | Drawable Int String Int Boolean ... | @drawable/xxxx @color/xxxx @string/xxxx @integer/xxxx @bool/xxxx ... |
| fraction | 割合 | Float | 50%, 25%p @fraction/xxxx |
| flags | フラグのon / off | Int | bit0→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)
}
}

関連記事:
