이 Codelab은 Kotlin 기반 Android 고급 교육 과정의 일부입니다. Codelab을 순서대로 진행하는 경우 학습 효과를 극대화할 수 있지만 순서를 바꿔 진행해도 괜찮습니다. 모든 교육 과정 Codelab은 Kotlin 기반 고급 Android Codelab 방문 페이지에 나열되어 있습니다.
소개
Android는 Button, TextView, EditText, ImageView, CheckBox, RadioButton 등 다양한 View 하위 클래스를 제공합니다. 이러한 서브클래스를 사용하여 사용자 상호작용을 지원하고 앱에 정보를 표시하는 UI를 구성할 수 있습니다. View 서브클래스가 요구사항을 충족하지 않는 경우 맞춤 뷰라고 하는 View 서브클래스를 만들 수 있습니다.
맞춤 뷰를 만들려면 기존 View 서브클래스 (예: Button 또는 EditText)를 확장하거나 View의 자체 서브클래스를 만들면 됩니다. View를 직접 확장하면 View의 onDraw() 메서드를 재정의하여 모든 크기와 모양의 상호작용 UI 요소를 만들어 그릴 수 있습니다.
맞춤 뷰를 만든 후 TextView 또는 Button를 추가하는 것과 같은 방식으로 활동 레이아웃에 추가할 수 있습니다.
이 과정에서는 View를 확장하여 처음부터 맞춤 뷰를 만드는 방법을 보여줍니다.
기본 요건
- 활동이 있는 앱을 만들고 Android 스튜디오를 사용하여 실행하는 방법
학습할 내용
View을 확장하여 맞춤 뷰를 만드는 방법- 원형 모양의 맞춤 뷰를 그리는 방법
- 리스너를 사용하여 맞춤 뷰와의 사용자 상호작용을 처리하는 방법
- 레이아웃에서 맞춤 뷰를 사용하는 방법
실습할 내용
CustomFanController 앱은 View 클래스를 확장하여 맞춤 뷰 하위 클래스를 만드는 방법을 보여줍니다. 새 서브클래스의 이름은 DialView입니다.
앱은 오프 (0), 로우 (1), 미디엄 (2), 하이 (3) 설정이 있는 실제 팬 컨트롤과 유사한 원형 UI 요소를 표시합니다. 사용자가 뷰를 탭하면 선택 표시기가 다음 위치(0-1-2-3)로 이동한 후 다시 0으로 돌아갑니다. 또한 선택이 1 이상이면 뷰의 원형 부분의 배경색이 회색에서 녹색으로 변경됩니다 (팬 전원이 켜져 있음을 나타냄).


뷰는 앱 UI의 기본 구성요소입니다. View 클래스는 일반적인 Android 앱의 사용자 인터페이스의 많은 요구사항을 충족하는 UI 위젯이라고 하는 많은 서브클래스를 제공합니다.
Button 및 TextView과 같은 UI 빌딩 블록은 View 클래스를 확장하는 서브클래스입니다. 시간과 개발 노력을 절약하려면 이러한 View 하위 클래스 중 하나를 확장하면 됩니다. 맞춤 뷰는 상위 뷰의 스타일과 동작을 상속하며, 변경하려는 동작이나 모양의 측면을 재정의할 수 있습니다. 예를 들어 EditText를 확장하여 맞춤 뷰를 만드는 경우 뷰는 EditText 뷰처럼 작동하지만 텍스트 입력 필드에서 텍스트를 지우는 X 버튼을 표시하도록 맞춤설정할 수도 있습니다.
EditText과 같은 View 서브클래스를 확장하여 맞춤 뷰를 가져올 수 있습니다. 원하는 결과에 가장 가까운 서브클래스를 선택하세요. 그런 다음 하나 이상의 레이아웃에서 다른 View 하위 클래스와 마찬가지로 속성이 있는 XML 요소로 맞춤 보기를 사용할 수 있습니다.
처음부터 자체 맞춤 뷰를 만들려면 View 클래스 자체를 확장하세요. 코드는 View 메서드를 재정의하여 뷰의 모양과 기능을 정의합니다. 맞춤 뷰를 만드는 데 있어 중요한 점은 크기와 모양이 다양한 전체 UI 요소를 화면에 그리는 책임이 있다는 것입니다. Button와 같은 기존 뷰를 서브클래스로 지정하면 해당 클래스에서 그리기를 처리합니다. (그리기에 관해서는 이 Codelab의 뒷부분에서 자세히 알아봅니다.)
맞춤 뷰를 만들려면 다음 일반 단계를 따르세요.
View를 확장하거나View서브클래스 (예:Button또는EditText)를 확장하는 맞춤 뷰 클래스를 만듭니다.- 기존
View하위 클래스를 확장하는 경우 변경하려는 동작이나 모양 측면만 재정의합니다. View클래스를 확장하는 경우 새 클래스에서onDraw()및onMeasure()와 같은View메서드를 재정의하여 맞춤 뷰의 모양을 그리고 모양을 제어합니다.- 사용자 상호작용에 응답하는 코드를 추가하고 필요한 경우 맞춤 뷰를 다시 그립니다.
- 활동의 XML 레이아웃에서 맞춤 뷰 클래스를 UI 위젯으로 사용합니다. 다양한 레이아웃에서 뷰를 맞춤설정할 수 있도록 뷰의 맞춤 속성을 정의할 수도 있습니다.
이 작업에서는 다음을 수행합니다.
ImageView을 맞춤 뷰의 임시 자리표시자로 사용하여 앱을 만듭니다.View를 확장하여 맞춤 뷰를 만듭니다.- 그리기 및 페인팅 값으로 맞춤 뷰를 초기화합니다.
1단계: ImageView 자리표시자가 있는 앱 만들기
- Empty Activity 템플릿을 사용하여 제목이
CustomFanController인 Kotlin 앱을 만듭니다. 패키지 이름이com.example.android.customfancontroller인지 확인합니다. - 텍스트 탭에서
activity_main.xml을 열어 XML 코드를 수정합니다. - 기존
TextView를 이 코드로 바꿉니다. 이 텍스트는 활동에서 맞춤 뷰의 라벨 역할을 합니다.
<TextView
android:id="@+id/customViewLabel"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Display3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:textColor="@android:color/black"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="24dp"
android:text="Fan Control"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>- 이
ImageView요소를 레이아웃에 추가합니다. 이 Codelab에서 만들 맞춤 뷰의 자리표시자입니다.
<ImageView
android:id="@+id/dialView"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@android:color/darker_gray"
app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp"/>- 두 UI 요소에서 문자열 및 크기 리소스를 추출합니다.
- 디자인 탭을 클릭합니다. 레이아웃은 다음과 같이 표시됩니다.

2단계. 맞춤 뷰 클래스 만들기
DialView라는 새 Kotlin 클래스를 만듭니다.- 클래스 정의를 수정하여
View를 확장합니다. 메시지가 표시되면android.view.View를 가져옵니다. View를 클릭한 다음 빨간색 전구를 클릭합니다. '@JvmOverloads'를 사용하여 Android 뷰 생성자 추가를 선택합니다. Android 스튜디오에서View클래스의 생성자를 추가합니다.@JvmOverloads주석은 Kotlin 컴파일러에 기본 매개변수 값을 대체하는 이 함수의 오버로드를 생성하도록 지시합니다.
class DialView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {DialView클래스 정의 위, 가져오기 바로 아래에 최상위enum을 추가하여 사용 가능한 팬 속도를 나타냅니다. 값이 실제 문자열이 아닌 문자열 리소스이므로 이enum는Int유형입니다. Android 스튜디오에 각 값에서 누락된 문자열 리소스에 관한 오류가 표시됩니다. 이 오류는 이후 단계에서 수정합니다.
private enum class FanSpeed(val label: Int) {
OFF(R.string.fan_off),
LOW(R.string.fan_low),
MEDIUM(R.string.fan_medium),
HIGH(R.string.fan_high);
}enum아래에 다음 상수를 추가합니다. 다이얼 표시기와 라벨을 그리는 과정에서 이를 사용합니다.
private const val RADIUS_OFFSET_LABEL = 30
private const val RADIUS_OFFSET_INDICATOR = -35DialView클래스 내에서 맞춤 뷰를 그리는 데 필요한 여러 변수를 정의합니다. 요청이 있는 경우android.graphics.PointF를 가져옵니다.
private var radius = 0.0f // Radius of the circle.
private var fanSpeed = FanSpeed.OFF // The active selection.
// position variable which will be used to draw label and indicator circle position
private val pointPosition: PointF = PointF(0.0f, 0.0f)radius은 원의 현재 반지름입니다. 이 값은 뷰가 화면에 그려질 때 설정됩니다.fanSpeed은 팬의 현재 속도이며FanSpeed열거형의 값 중 하나입니다. 기본적으로 이 값은OFF입니다.- 마지막으로
postPosition는 화면에 뷰의 여러 요소를 그리는 데 사용되는 X,Y 포인트입니다.
이러한 값은 뷰가 실제로 그려질 때가 아니라 여기에서 생성되고 초기화되어 실제 그리기 단계가 최대한 빠르게 실행되도록 합니다.
DialView클래스 정의 내부에서 몇 가지 기본 스타일을 사용하여Paint객체를 초기화합니다. 요청이 있는 경우android.graphics.Paint및android.graphics.Typeface를 가져옵니다. 이전의 변수와 마찬가지로 이러한 스타일은 그리기 단계를 빠르게 처리하기 위해 여기에서 초기화됩니다.
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
textAlign = Paint.Align.CENTER
textSize = 55.0f
typeface = Typeface.create( "", Typeface.BOLD)
}res/values/strings.xml을 열고 팬 속도에 대한 문자열 리소스를 추가합니다.
<string name="fan_off">off</string>
<string name="fan_low">1</string>
<string name="fan_medium">2</string>
<string name="fan_high">3</string>맞춤 뷰를 만든 후에는 이를 그릴 수 있어야 합니다. EditText과 같은 View 서브클래스를 확장하면 해당 서브클래스가 뷰의 모양과 속성을 정의하고 화면에 자체적으로 그려집니다. 따라서 뷰를 그리는 코드를 작성할 필요가 없습니다. 대신 상위 요소의 메서드를 재정의하여 뷰를 맞춤설정할 수 있습니다.
View를 확장하여 처음부터 자체 뷰를 만드는 경우 화면이 새로고침될 때마다 전체 뷰를 그리고 그리기를 처리하는 View 메서드를 재정의해야 합니다. View를 확장하는 맞춤 뷰를 올바르게 그리려면 다음을 충족해야 합니다.
onSizeChanged()메서드를 재정의하여 뷰가 처음 표시될 때와 뷰의 크기가 변경될 때마다 뷰의 크기를 계산합니다.Paint객체로 스타일이 지정된Canvas객체를 사용하여 맞춤 뷰를 그리는onDraw()메서드를 재정의합니다.- 뷰가 그려지는 방식을 변경하는 사용자 클릭에 응답할 때
invalidate()메서드를 호출하여 전체 뷰를 무효화하므로onDraw()호출이 강제되어 뷰가 다시 그려집니다.
onDraw() 메서드는 화면이 새로고침될 때마다 호출되며 이는 초당 여러 번일 수 있습니다. 성능상의 이유와 시각적 결함을 방지하기 위해 onDraw()에서 가능한 한 적은 작업을 실행해야 합니다. 특히 할당은 시각적 버벅거림을 유발할 수 있는 가비지 컬렉션으로 이어질 수 있으므로 onDraw()에 할당을 배치하지 마세요.
Canvas 및 Paint 클래스는 다음과 같은 유용한 그리기 단축키를 제공합니다.
drawText()를 사용하여 텍스트를 그립니다.setTypeface()를 호출하여 글꼴을 지정하고setColor()를 호출하여 텍스트 색상을 지정합니다.drawRect(),drawOval(),drawArc()를 사용하여 기본 도형을 그립니다.setStyle()을 호출하여 도형을 채우는지, 윤곽선만 표시하는지 또는 둘 다인지 설정합니다.drawBitmap()을 사용하여 비트맵을 그립니다.
Canvas 및 Paint에 관해서는 이후 Codelab에서 자세히 알아봅니다. Android에서 뷰를 그리는 방법에 관한 자세한 내용은 Android에서 뷰를 그리는 방법을 참고하세요.
이 작업에서는 onSizeChanged() 및 onDraw() 메서드를 사용하여 팬 컨트롤러 맞춤 뷰를 화면에 그립니다(다이얼 자체, 현재 위치 표시기, 표시기 라벨). 또한 다이얼의 표시기 라벨의 현재 X,Y 위치를 계산하는 도우미 메서드 computeXYForSpeed(),를 만듭니다.
1단계: 위치를 계산하고 뷰를 그립니다.
DialView클래스에서 초기화 아래에View클래스의onSizeChanged()메서드를 재정의하여 맞춤 뷰의 다이얼 크기를 계산합니다.kotlin를 가져옵니다.요청이 있는 경우math.min를 가져옵니다.onSizeChanged()메서드는 레이아웃이 확장될 때 처음 그려지는 경우를 비롯하여 뷰의 크기가 변경될 때마다 호출됩니다. 그릴 때마다 다시 계산하는 대신onSizeChanged()를 재정의하여 맞춤 뷰의 위치, 크기, 맞춤 뷰의 크기와 관련된 기타 값을 계산합니다. 이 경우onSizeChanged()를 사용하여 다이얼의 원 요소의 현재 반지름을 계산합니다.
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
radius = (min(width, height) / 2.0 * 0.8).toFloat()
}onSizeChanged()아래에 다음 코드를 추가하여PointF클래스의computeXYForSpeed()확장 함수를 정의합니다. 요청이 있는 경우kotlin.math.cos및kotlin.math.sin를 가져옵니다.PointF클래스의 이 확장 함수는 현재FanSpeed위치와 다이얼의 반지름을 고려하여 텍스트 라벨과 현재 표시기 (0, 1, 2 또는 3)의 화면상 X, Y 좌표를 계산합니다.onDraw().에서 이 값을 사용합니다.
private fun PointF.computeXYForSpeed(pos: FanSpeed, radius: Float) {
// Angles are in radians.
val startAngle = Math.PI * (9 / 8.0)
val angle = startAngle + pos.ordinal * (Math.PI / 4)
x = (radius * cos(angle)).toFloat() + width / 2
y = (radius * sin(angle)).toFloat() + height / 2
}onDraw()메서드를 재정의하여Canvas및Paint클래스로 화면에 뷰를 렌더링합니다. 요청이 있는 경우android.graphics.Canvas를 가져옵니다. 스켈레톤 재정의는 다음과 같습니다.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
}onDraw()내에서 다음 줄을 추가하여 팬 속도가OFF인지 아니면 다른 값인지에 따라 페인트 색상을 회색 (Color.GRAY) 또는 녹색 (Color.GREEN)으로 설정합니다. 요청이 있는 경우android.graphics.Color를 가져옵니다.
// Set dial background color to green if selection not off.
paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREENdrawCircle()메서드를 사용하여 다이얼의 원을 그리는 코드를 추가합니다. 이 메서드는 현재 뷰 너비와 높이를 사용하여 원의 중심, 원의 반지름, 현재 페인트 색상을 찾습니다.width및height속성은View상위 클래스의 멤버이며 뷰의 현재 크기를 나타냅니다.
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)- 다음 코드를 추가하여 팬 속도 표시기 마크에 더 작은 원을
drawCircle()메서드로 그립니다. 이 부분에서는PointF을 사용합니다.computeXYforSpeed()확장 프로그램 메서드를 사용하여 현재 팬 속도를 기반으로 표시기 중심의 X,Y 좌표를 계산합니다.
// Draw the indicator circle.
val markerRadius = radius + RADIUS_OFFSET_INDICATOR
pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
paint.color = Color.BLACK
canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)- 마지막으로 다이얼 주변의 적절한 위치에 팬 속도 라벨 (0, 1, 2, 3)을 그립니다. 이 메서드 부분에서는
PointF.computeXYForSpeed()를 다시 호출하여 각 라벨의 위치를 가져오고 할당을 방지하기 위해 매번pointPosition객체를 재사용합니다.drawText()을 사용하여 라벨을 그립니다.
// Draw the text labels.
val labelRadius = radius + RADIUS_OFFSET_LABEL
for (i in FanSpeed.values()) {
pointPosition.computeXYForSpeed(i, labelRadius)
val label = resources.getString(i.label)
canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
}완성된 onDraw() 메서드는 다음과 같습니다.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Set dial background color to green if selection not off.
paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
// Draw the indicator circle.
val markerRadius = radius + RADIUS_OFFSET_INDICATOR
pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
paint.color = Color.BLACK
canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
// Draw the text labels.
val labelRadius = radius + RADIUS_OFFSET_LABEL
for (i in FanSpeed.values()) {
pointPosition.computeXYForSpeed(i, labelRadius)
val label = resources.getString(i.label)
canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
}
}2단계: 레이아웃에 뷰 추가
앱의 UI에 맞춤 뷰를 추가하려면 활동의 XML 레이아웃에서 요소로 지정합니다. 다른 UI 요소와 마찬가지로 XML 요소 속성으로 모양과 동작을 제어합니다.
activity_main.xml에서dialView의ImageView태그를com.example.android.customfancontroller.DialView로 변경하고android:background속성을 삭제합니다.DialView와 원래ImageView모두View클래스의 표준 속성을 상속하므로 다른 속성을 변경할 필요가 없습니다. 새DialView요소는 다음과 같이 표시됩니다.
<com.example.android.customfancontroller.DialView
android:id="@+id/dialView"
android:layout_width="@dimen/fan_dimen"
android:layout_height="@dimen/fan_dimen"
app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="@dimen/default_margin"
android:layout_marginRight="@dimen/default_margin"
android:layout_marginTop="@dimen/default_margin" />- 앱을 실행합니다. 팬 제어 보기가 활동에 표시됩니다.

마지막 작업은 사용자가 뷰를 탭할 때 맞춤 뷰가 작업을 실행하도록 하는 것입니다. 탭할 때마다 선택 표시기가 다음 위치로 이동해야 합니다(off-1-2-3 및 다시 off). 또한 선택이 1 이상이면 배경을 회색에서 녹색으로 변경하여 팬 전원이 켜져 있음을 나타냅니다.
맞춤 뷰를 클릭할 수 있도록 하려면 다음을 실행해야 합니다.
- 뷰의
isClickable속성을true로 설정합니다. 이렇게 하면 맞춤 뷰가 클릭에 응답할 수 있습니다. - 뷰를 클릭할 때 작업을 실행하도록
View클래스의performClick()를 구현합니다. invalidate()메서드를 호출합니다. 이렇게 하면 Android 시스템에서onDraw()메서드를 호출하여 뷰를 다시 그립니다.
일반적으로 표준 Android 뷰를 사용하면 사용자가 해당 뷰를 클릭할 때 작업을 실행하도록 OnClickListener()를 구현합니다. 맞춤 뷰의 경우 View 클래스의 performClick() 메서드를 대신 구현하고 super를 호출합니다.performClick(). 기본 performClick() 메서드도 onClickListener()를 호출하므로 performClick()에 작업을 추가하고 onClickListener()는 맞춤 뷰를 사용할 수 있는 다른 개발자나 사용자가 추가로 맞춤설정할 수 있도록 남겨둘 수 있습니다.
DialView.kt의FanSpeed열거형 내에서 현재 팬 속도를 목록의 다음 속도로 변경하는 확장 함수next()를 추가합니다 (OFF에서LOW,MEDIUM,HIGH로 변경한 후 다시OFF로 변경). 이제 전체 열거형은 다음과 같습니다.
private enum class FanSpeed(val label: Int) {
OFF(R.string.fan_off),
LOW(R.string.fan_low),
MEDIUM(R.string.fan_medium),
HIGH(R.string.fan_high);
fun next() = when (this) {
OFF -> LOW
LOW -> MEDIUM
MEDIUM -> HIGH
HIGH -> OFF
}
}DialView클래스 내에서onSizeChanged()메서드 바로 앞에init()블록을 추가합니다. 뷰의isClickable속성을 true로 설정하면 뷰가 사용자 입력을 수락할 수 있습니다.
init {
isClickable = true
}init(),아래에서performClick()메서드를 아래 코드로 재정의합니다.
override fun performClick(): Boolean {
if (super.performClick()) return true
fanSpeed = fanSpeed.next()
contentDescription = resources.getString(fanSpeed.label)
invalidate()
return true
}super 호출performClick()가 먼저 발생해야 하며, 이를 통해 접근성 이벤트가 사용 설정되고 onClickListener()가 호출됩니다.
다음 두 줄은 next() 메서드로 팬의 속도를 증가시키고 현재 속도 (off, 1, 2 또는 3)를 나타내는 문자열 리소스로 뷰의 콘텐츠 설명을 설정합니다.
마지막으로 invalidate() 메서드는 전체 뷰를 무효화하여 onDraw() 호출을 강제하여 뷰를 다시 그립니다. 사용자 상호작용을 비롯한 어떤 이유로든 맞춤 뷰가 변경되고 변경사항을 표시해야 하는 경우 invalidate().를 호출합니다.
- 앱을 실행합니다.
DialView요소를 탭하여 표시기를 off에서 1로 이동합니다. 다이얼이 녹색으로 바뀝니다. 탭할 때마다 표시기가 다음 위치로 이동해야 합니다. 표시기가 꺼짐으로 돌아가면 다이얼이 다시 회색으로 바뀝니다.


이 예에서는 맞춤 뷰에서 맞춤 속성을 사용하는 기본 메커니즘을 보여줍니다. 각 팬 다이얼 위치에 다른 색상을 사용하여 DialView 클래스의 맞춤 속성을 정의합니다.
res/values/attrs.xml을 만들고 엽니다.<resources>내부에<declare-styleable>리소스 요소를 추가합니다.<declare-styleable>리소스 요소 내에 속성별로name및format가 있는attr요소를 세 개 추가합니다.format은 유형과 같으며 이 경우color입니다.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="DialView">
<attr name="fanColor1" format="color" />
<attr name="fanColor2" format="color" />
<attr name="fanColor3" format="color" />
</declare-styleable>
</resources>activity_main.xml레이아웃 파일을 엽니다.DialView에서fanColor1,fanColor2,fanColor3의 속성을 추가하고 아래에 표시된 색상으로 값을 설정합니다. 맞춤 속성이android네임스페이스가 아닌schemas.android.com/apk/res/your_app_package_name네임스페이스에 속하므로android:대신app:를 맞춤 속성의 서문 (예:app:fanColor1)으로 사용하세요.
app:fanColor1="#FFEB3B"
app:fanColor2="#CDDC39"
app:fanColor3="#009688"DialView 클래스에서 속성을 사용하려면 속성을 검색해야 합니다. 이러한 값은 생성 시 클래스에 전달되는 AttributeSet에 저장됩니다(있는 경우). init에서 속성을 가져오고 캐싱을 위해 속성 값을 로컬 변수에 할당합니다.
DialView.kt클래스 파일을 엽니다.DialView내에서 속성 값을 캐시하는 변수를 선언합니다.
private var fanSpeedLowColor = 0
private var fanSpeedMediumColor = 0
private var fanSeedMaxColor = 0init블록에서withStyledAttributes확장 함수를 사용하여 다음 코드를 추가합니다. 속성과 뷰를 제공하고 지역 변수를 설정합니다.withStyledAttributes을 가져오면 올바른getColor()함수도 가져옵니다.
context.withStyledAttributes(attrs, R.styleable.DialView) {
fanSpeedLowColor = getColor(R.styleable.DialView_fanColor1, 0)
fanSpeedMediumColor = getColor(R.styleable.DialView_fanColor2, 0)
fanSeedMaxColor = getColor(R.styleable.DialView_fanColor3, 0)
}onDraw()의 지역 변수를 사용하여 현재 팬 속도에 따라 다이얼 색상을 설정합니다. 페인트 색상이 설정된 줄 (paint.color=if(fanSpeed== FanSpeed.OFF) Color.GRAYelseColor.GREEN)을 아래 코드로 바꿉니다.
paint.color = when (fanSpeed) {
FanSpeed.OFF -> Color.GRAY
FanSpeed.LOW -> fanSpeedLowColor
FanSpeed.MEDIUM -> fanSpeedMediumColor
FanSpeed.HIGH -> fanSeedMaxColor
} as Int- 앱을 실행하고 다이얼을 클릭하면 아래와 같이 각 위치의 색상 설정이 다릅니다.
|
|
|
|
맞춤 뷰 속성에 대해 자세히 알아보려면 뷰 클래스 만들기를 참고하세요.
접근성은 장애인을 포함한 모든 사람이 앱을 사용할 수 있도록 하는 디자인, 구현, 테스트 기법의 집합입니다.
Android 기기 사용에 영향을 줄 수 있는 일반적인 장애로는 시각 장애, 저시력, 색맹, 청각 장애, 난청, 거동 장애가 있습니다. 접근성을 염두에 두고 앱을 개발하면 이러한 장애가 있는 사용자뿐만 아니라 다른 모든 사용자의 사용자 환경도 개선됩니다.
Android는 TextView, Button과 같은 표준 UI 뷰에서 기본적으로 여러 접근성 기능을 제공합니다. 하지만 맞춤 뷰를 만들 때는 맞춤 뷰가 화면 콘텐츠의 음성 설명과 같은 접근성 기능을 제공하는 방법을 고려해야 합니다.
이 작업에서는 Android의 스크린 리더인 TalkBack에 대해 알아보고 DialView 맞춤 뷰에 대한 음성 안내와 설명을 포함하도록 앱을 수정합니다.
1단계. TalkBack 살펴보기
TalkBack은 Android의 내장 스크린 리더입니다. TalkBack을 사용 설정하면 Android에서 화면 요소를 소리 내어 설명하므로 사용자는 화면을 보지 않고도 Android 기기와 상호작용할 수 있습니다. 시각장애인의 경우 TalkBack을 통해 앱을 사용할 수 있습니다.
이 작업에서는 TalkBack을 사용 설정하여 스크린 리더가 작동하는 방식과 앱을 탐색하는 방법을 알아봅니다.
- Android 기기 또는 에뮬레이터에서 설정 > 접근성 > TalkBack으로 이동합니다.
- 사용/사용 안함 전환 버튼을 탭하여 TalkBack을 사용 설정합니다.
- 확인을 탭하여 권한을 확인합니다.
- 메시지가 표시되면 기기 비밀번호를 확인합니다. TalkBack을 처음 실행하면 튜토리얼이 시작됩니다. (오래된 기기에서는 튜토리얼을 사용하지 못할 수 있습니다.)
- 눈을 감고 튜토리얼을 탐색하는 것이 도움이 될 수 있습니다. 나중에 다시 튜토리얼을 열려면 설정 > 접근성 > TalkBack > 설정 > TalkBack 튜토리얼 시작하기로 이동하세요.
CustomFanController앱을 컴파일하고 실행하거나 기기의 개요 또는 최근 버튼으로 엽니다. TalkBack을 사용 설정하면 앱 이름과 라벨TextView('팬 제어')의 텍스트가 함께 안내됩니다. 하지만DialView뷰 자체를 탭하면 뷰의 상태 (다이얼의 현재 설정)나 뷰를 탭하여 활성화할 때 발생하는 작업에 관한 정보가 음성으로 제공되지 않습니다.
2단계: 다이얼 라벨의 콘텐츠 설명 추가
콘텐츠 설명은 앱의 뷰의 의미와 목적을 설명합니다. 이러한 라벨이 있어야 Android의 TalkBack 기능과 같은 스크린 리더가 각 요소의 기능을 정확하게 설명해 줄 수 있습니다. ImageView와 같은 정적 뷰의 경우 contentDescription 속성을 사용하여 레이아웃 파일의 뷰에 콘텐츠 설명을 추가할 수 있습니다. 텍스트 뷰 (TextView 및 EditText)는 뷰의 텍스트를 콘텐츠 설명으로 자동 사용합니다.
맞춤 팬 제어 뷰의 경우 뷰를 클릭할 때마다 콘텐츠 설명을 동적으로 업데이트하여 현재 팬 설정을 나타내야 합니다.
DialView클래스 하단에서 인수나 반환 유형이 없는 함수updateContentDescription()를 선언합니다.
fun updateContentDescription() {
}updateContentDescription()내에서 맞춤 뷰의contentDescription속성을 현재 팬 속도 (off, 1, 2, 3)와 연결된 문자열 리소스로 변경합니다. 이러한 라벨은 다이얼이 화면에 그려질 때onDraw()에 사용되는 라벨과 동일합니다.
fun updateContentDescription() {
contentDescription = resources.getString(fanSpeed.label)
}init()블록으로 스크롤하고 블록 끝에updateContentDescription()호출을 추가합니다. 이렇게 하면 뷰가 초기화될 때 콘텐츠 설명이 초기화됩니다.
init {
isClickable = true
// ...
updateContentDescription()
}performClick()메서드에서invalidate()바로 앞에updateContentDescription()호출을 하나 더 추가합니다.
override fun performClick(): Boolean {
if (super.performClick()) return true
fanSpeed = fanSpeed.next()
updateContentDescription()
invalidate()
return true
}- 앱을 컴파일하고 실행한 후 TalkBack이 사용 설정되어 있는지 확인합니다. 탭하여 다이얼 뷰의 설정을 변경하면 이제 TalkBack에서 현재 라벨 (off, 1, 2, 3)과 '더블 탭하여 활성화'라는 문구를 함께 알립니다.
3단계: 클릭 작업에 대한 추가 정보 추가
여기까지만 해도 TalkBack에서 뷰를 사용할 수 있습니다. 하지만 뷰가 활성화될 수 있다는 것 ('더블 탭하여 활성화')뿐만 아니라 뷰가 활성화될 때 어떤 일이 일어나는지 ('더블 탭하여 변경' 또는 '더블 탭하여 재설정') 설명해 주면 도움이 됩니다.
이렇게 하려면 접근성 위임을 통해 접근성 노드 정보 객체에 뷰의 작업 (여기서는 클릭 또는 탭 작업)에 관한 정보를 추가합니다. 접근성 위임자를 사용하면 상속이 아닌 컴포지션을 통해 앱의 접근성 관련 기능을 맞춤설정할 수 있습니다.
이 작업에서는 이전 버전과의 호환성을 위해 Android Jetpack 라이브러리 (androidx.*)의 접근성 클래스를 사용합니다.
DialView.kt의init블록에서 뷰의 접근성 위임자를 새AccessibilityDelegateCompat객체로 설정합니다. 요청이 있는 경우androidx.core.view.ViewCompat및androidx.core.view.AccessibilityDelegateCompat를 가져옵니다. 이 전략을 사용하면 앱에서 하위 호환성을 최대한 확보할 수 있습니다.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
})AccessibilityDelegateCompat객체 내에서AccessibilityNodeInfoCompat객체를 사용하여onInitializeAccessibilityNodeInfo()함수를 재정의하고 super의 메서드를 호출합니다. 메시지가 표시되면androidx.core.view.accessibility.AccessibilityNodeInfoCompat를 가져옵니다.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View,
info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
}
})모든 뷰에는 접근성 노드 트리가 있으며, 이 트리는 뷰의 실제 레이아웃 구성요소에 상응할 수도 있고 상응하지 않을 수도 있습니다. Android의 접근성 서비스는 이러한 노드를 탐색하여 뷰에 관한 정보 (예: 음성으로 읽을 수 있는 콘텐츠 설명 또는 해당 뷰에서 실행할 수 있는 작업)를 알아냅니다. 맞춤 뷰를 만들 때 접근성을 위한 맞춤 정보를 제공하기 위해 노드 정보를 재정의해야 할 수도 있습니다. 이 경우 노드 정보를 재정의하여 뷰의 작업에 맞춤 정보가 있음을 나타냅니다.
onInitializeAccessibilityNodeInfo()내에서 새AccessibilityNodeInfoCompat.AccessibilityActionCompat객체를 만들고customClick변수에 할당합니다. 생성자에AccessibilityNodeInfo.ACTION_CLICK상수와 자리표시자 문자열을 전달합니다. 요청이 있는 경우AccessibilityNodeInfo를 가져옵니다.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View,
info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
AccessibilityNodeInfo.ACTION_CLICK,
"placeholder"
)
}
})AccessibilityActionCompat 클래스는 접근성 목적으로 뷰에서 실행되는 작업을 나타냅니다. 일반적인 작업은 여기에서 사용하는 클릭 또는 탭이지만, 포커스 획득 또는 손실, 클립보드 작업 (잘라내기/복사/붙여넣기), 뷰 내 스크롤과 같은 다른 작업도 포함될 수 있습니다. 이 클래스의 생성자에는 작업 상수 (여기서는 AccessibilityNodeInfo.ACTION_CLICK)와 TalkBack에서 작업이 무엇인지 나타내는 데 사용하는 문자열이 필요합니다.
"placeholder"문자열을context.getString()호출로 바꿔 문자열 리소스를 가져옵니다. 특정 리소스의 현재 팬 속도를 테스트합니다. 현재 속도가FanSpeed.HIGH인 경우 문자열은"Reset"입니다. 팬 속도가 다른 경우 문자열은"Change."입니다. 이러한 문자열 리소스는 나중에 만듭니다.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View,
info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
AccessibilityNodeInfo.ACTION_CLICK,
context.getString(if (fanSpeed != FanSpeed.HIGH) R.string.change else R.string.reset)
)
}
})customClick정의의 닫는 괄호 뒤에addAction()메서드를 사용하여 새 접근성 작업을 노드 정보 객체에 추가합니다.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View,
info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
AccessibilityNodeInfo.ACTION_CLICK,
context.getString(if (fanSpeed != FanSpeed.HIGH)
R.string.change else R.string.reset)
)
info.addAction(customClick)
}
})res/values/strings.xml에서 '변경' 및 '재설정'의 문자열 리소스를 추가합니다.
<string name="change">Change</string>
<string name="reset">Reset</string>- 앱을 컴파일하고 실행한 후 TalkBack이 사용 설정되어 있는지 확인합니다. 이제 '더블 탭하여 활성화'라는 문구가 '더블 탭하여 변경' (팬 속도가 높음 또는 3 미만인 경우) 또는 '더블 탭하여 재설정' (팬 속도가 이미 높음 또는 3인 경우)으로 표시됩니다. '두 번 탭하여...'이라는 메시지는 TalkBack 서비스 자체에서 제공합니다.
완료된 Codelab의 코드를 다운로드합니다.
$ git clone https://github.com/googlecodelabs/android-kotlin-drawing-custom-views
또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.
EditText과 같은View서브클래스의 모양과 동작을 상속하는 맞춤 뷰를 만들려면 해당 서브클래스를 확장하는 새 클래스를 추가하고 서브클래스의 메서드 일부를 재정의하여 조정합니다.- 크기와 모양이 다양한 맞춤 뷰를 만들려면
View를 확장하는 새 클래스를 추가합니다. onDraw()과 같은View메서드를 재정의하여 뷰의 모양과 기본 모양을 정의합니다.invalidate()를 사용하여 뷰를 강제로 그리거나 다시 그립니다.- 성능을 최적화하려면
onDraw()에서 변수를 사용하기 전에 멤버 변수 초기화와 같이 그리기 및 페인팅에 필요한 값을 할당하세요. OnClickListener() 대신 맞춤 뷰에performClick()를 재정의하여 뷰의 대화형 동작을 제공합니다. 이를 통해 맞춤 뷰 클래스를 사용할 수 있는 개발자나 다른 Android 개발자가onClickListener()를 사용하여 추가 동작을 제공할 수 있습니다.- 다른 UI 요소와 마찬가지로 모양을 정의하는 속성을 사용하여 맞춤 뷰를 XML 레이아웃 파일에 추가합니다.
values폴더에attrs.xml파일을 만들어 맞춤 속성을 정의합니다. 그런 다음 XML 레이아웃 파일에서 맞춤 뷰의 맞춤 속성을 사용할 수 있습니다.
Udacity 과정:
Android 개발자 문서:
- 맞춤 뷰 만들기
@JvmOverloads- 맞춤 구성요소
- Android에서 뷰를 그리는 방법
onMeasure()onSizeChanged()onDraw()CanvasPaintdrawText()setTypeface()setColor()drawRect()drawOval()drawArc()drawBitmap()setStyle()invalidate()- View
- 입력 이벤트
- 페인트
- Kotlin 확장 프로그램 라이브러리 android-ktx
withStyledAttributes- Android KTX 문서
- Android KTX 원래 공지사항 블로그
- 맞춤 뷰의 접근성 높이기
AccessibilityDelegateCompatAccessibilityNodeInfoCompatAccessibilityNodeInfoCompat.AccessibilityActionCompat
동영상:
이 섹션에는 강사가 진행하는 과정의 일부로 이 Codelab을 진행하는 학생에게 출제할 수 있는 과제가 나열되어 있습니다. 다음 작업은 강사가 결정합니다.
- 필요한 경우 과제를 할당합니다.
- 과제 제출 방법을 학생에게 알립니다.
- 과제를 채점합니다.
강사는 이러한 추천을 원하는 만큼 사용할 수 있으며 적절하다고 생각되는 다른 과제를 출제해도 됩니다.
이 Codelab을 직접 진행하는 경우 이러한 과제를 자유롭게 사용하여 배운 내용을 테스트해 보세요.
질문 1
맞춤 뷰에 처음 크기가 할당될 때 위치, 크기, 기타 값을 계산하려면 어떤 메서드를 재정의해야 하나요?
▢ onMeasure()
▢ onSizeChanged()
▢ invalidate()
▢ onDraw()
질문 2
속성 값이 변경된 후 뷰를 onDraw()로 다시 그려야 함을 나타내려면 UI 스레드에서 어떤 메서드를 호출해야 하나요?
▢ onMeasure()
▢ onSizeChanged()
▢ invalidate()
▢ getVisibility()
질문 3
맞춤 뷰에 상호작용을 추가하려면 어떤 View 메서드를 재정의해야 하나요?
▢ setOnClickListener()
▢ onSizeChanged()
▢ isClickable()
▢ performClick()
이 과정의 다른 Codelab 링크는 Kotlin 기반 Android 고급 Codelab 방문 페이지를 참고하세요.



