[Android] 안드로이드 개발 레벨업 교과서 정리 #2 커스텀뷰


출처 : 안드로이드 개발 레벨업 교과서 41~50p

몰랐던 부분 정리하는 포스트!


커스텀 뷰 만들기

1. 뷰를 이해하자

  • 뷰란 UI를 구성하는 바탕이 되는 컴포넌트로서 네모난 그리기 영역을 가진다
  • 패딩으로 지정된 간격은 배경색으로 칠해지고, 마진으로 지정된 간격은 공백이 된다. 패딩은 뷰 크기에 포함되지만, 마진은 포함되지 않는다.


2. 커스텀 뷰 만들기

  • 기존 뷰를 조합한 커스텀뷰 만들기는 아래 4단계로 진행된다.
    1. 커스텀뷰의 레이아웃을 결정한다.
    2. 레이아웃 XML로 설정할 수 있는 항목을 attrs.xml에 기재한다.
    3. 커스텀 뷰 클래스를 만든다.
    4. 메인 앱의 레이아웃에 삽입해서 확인한다.


① 커스텀뷰의 레이아웃을 결정한다.

  • 가장 먼저 xml 로 레이아웃을 만든다.
five_airplane_indicator.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">

<ImageView
android:id="@+id/airplane1"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/black_plane_yellow" />

<ImageView
android:id="@+id/airplane2"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/black_plane_base" />

<ImageView
android:id="@+id/airplane3"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/black_plane_base" />

<ImageView
android:id="@+id/airplane4"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/black_plane_base" />

<ImageView
android:id="@+id/airplane5"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/black_plane_base" />
</merge>
  • 주의할 점은 루트 태그를 merge로 해야한다는 것이다. 이유는 커스텀 뷰가 LinearLayout을 상속한 클래스이므로 LinearLayout의 불필요한 중첩을 피하기 위함이다.


② 레이아웃 xml로 설정할 수 있는 항목을 attrs.xml에 기재한다.

  • 커스텀뷰의 xml로 속성을 변경할 수 있도록 준비한다. xml 로 몇번째 비행기가 선택되었는지 설정할 수 있도록 selected 속성을 추가했다.

values/attrs.xml

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyCustomView">
<attr name="selected" format="integer" />
</declare-styleable>
</resources>


③ 커스텀 뷰 클래스를 만든다.

  • 커스텀 뷰를 만들 때는 View를 상속할 필요가 있다. 이번 예제에서는 LinearLayout을 사용한다.

  • 염두에 둘 것은 3가지

    레이아웃 xml

    스타일 반영

    외부 클래스

    로서, 예를 들어 액티비티로 조작할 수 있게 메서드를 구현한다.

MyCustomView.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package com.onedelay.myapplication

import android.annotation.TargetApi
import android.content.Context
import android.content.res.TypedArray
import android.os.Build
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.LinearLayout

class MyCustomView : LinearLayout {
private lateinit var mAirplane1: ImageView
private lateinit var mAirplane2: ImageView
private lateinit var mAirplane3: ImageView
private lateinit var mAirplane4: ImageView
private lateinit var mAirplane5: ImageView
var mSelected = 0
private set(value) {
field = value
}

constructor(context: Context?) : super(context) {
initializeViews(context, null)
}

constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
initializeViews(context, attrs)
}

constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initializeViews(context, attrs)
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
initializeViews(context, attrs)
}

/**
* 레이아웃 초기화
*/
private fun initializeViews(context: Context?, attrs: AttributeSet?) {
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
// 1. 레이아웃 전개
inflater.inflate(R.layout.five_airplane_indicator, this)
// 2. attrs.xml 에 정의한 스타일을 가져온다
if (attrs != null) {
val a: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.MyCustomView)
mSelected = a.getInteger(0, 0)
a.recycle() // 이용이 끝났으면 recycle()을 호출한다
}
}

/**
* inflate 가 완료되는 시점에서 콜백된다
*/
override fun onFinishInflate() {
super.onFinishInflate()
mAirplane1 = findViewById(R.id.airplane1)
mAirplane2 = findViewById(R.id.airplane2)
mAirplane3 = findViewById(R.id.airplane3)
mAirplane4 = findViewById(R.id.airplane4)
mAirplane5 = findViewById(R.id.airplane5)
// 처음에만 xml 의 지정을 반영하고자 2번째 인수인 force 를 true 로 한다
setSelected(mSelected, true)
}

fun setSelected(select: Int) {
setSelected(select, false)
}

/**
* 지정된 번호로 선택한다(내부용)
*
* @param select 지정할 번호(0이 가장 왼쪽)
* @param force: 지정을 강제로 반영한다
*/
private fun setSelected(select: Int, force: Boolean) {
if (force || mSelected != select)
if (4 < mSelected || mSelected < 0) return
mSelected = select
when (mSelected) {
1 -> {
mAirplane1.setImageResource(R.drawable.black_plane_base)
mAirplane2.setImageResource(R.drawable.black_plane_yellow)
mAirplane3.setImageResource(R.drawable.black_plane_base)
mAirplane4.setImageResource(R.drawable.black_plane_base)
mAirplane5.setImageResource(R.drawable.black_plane_base)
}

2 -> {
mAirplane1.setImageResource(R.drawable.black_plane_base)
mAirplane2.setImageResource(R.drawable.black_plane_base)
mAirplane3.setImageResource(R.drawable.black_plane_yellow)
mAirplane4.setImageResource(R.drawable.black_plane_base)
mAirplane5.setImageResource(R.drawable.black_plane_base)
}

3 -> {
mAirplane1.setImageResource(R.drawable.black_plane_base)
mAirplane2.setImageResource(R.drawable.black_plane_base)
mAirplane3.setImageResource(R.drawable.black_plane_base)
mAirplane4.setImageResource(R.drawable.black_plane_yellow)
mAirplane5.setImageResource(R.drawable.black_plane_base)
}

4 -> {
mAirplane1.setImageResource(R.drawable.black_plane_base)
mAirplane2.setImageResource(R.drawable.black_plane_base)
mAirplane3.setImageResource(R.drawable.black_plane_base)
mAirplane4.setImageResource(R.drawable.black_plane_base)
mAirplane5.setImageResource(R.drawable.black_plane_yellow)
}

else -> {
mAirplane1.setImageResource(R.drawable.black_plane_yellow)
mAirplane2.setImageResource(R.drawable.black_plane_base)
mAirplane3.setImageResource(R.drawable.black_plane_base)
mAirplane4.setImageResource(R.drawable.black_plane_base)
mAirplane5.setImageResource(R.drawable.black_plane_base)
}
}
}
}


④ 메인 앱의 레이아웃에 삽입해서 확인한다.

  • <패키지명.클래스명> 태그로 xml에 뷰를 추가한다.
  • 이름공간(app)이 부여되어있는데, attrs.xml 에서 지정한 정의를 이용하기 위해 필요하다. app이라는 이름공간은 xmlns:app="http://schemas.android.com/apk/res-auto"가 된다. 이 이름공간을 이용하면 자동으로 attrs.xml에서 정의한 내용을 연결할 수 있다.

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="15dp">

<com.onedelay.myapplication.MyCustomView
android:id="@+id/indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:selected="1" />

<Button
android:id="@+id/button"
android:text="@string/str_click"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

MainActivity.kt

1
2
3
4
5
6
7
8
9
10
11
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

button.setOnClickListener {
indicator.setSelected((indicator.mSelected + 1) % 5)
}
}
}


결과화면

Share