이 Codelab은 Kotlin 기반 Android 고급 교육 과정의 일부입니다. Codelab을 순서대로 진행하는 경우 학습 효과를 극대화할 수 있지만 순서를 바꿔 진행해도 괜찮습니다. 모든 과정 Codelab은 Kotlin Codelab의 고급 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
라고 합니다.
앱이 물리적 팬 제어와 유사한 원형 UI 요소를 표시하고 꺼짐 (0), 낮음 (1), 중간 (2), 높음 (3) 설정을 표시합니다. 사용자가 뷰를 탭하면 선택 표시기가 다음 위치(0-1-2-3)로 이동하고 다시 0으로 돌아갑니다. 또한 선택사항이 1 이상인 경우 보기의 원형 부분의 배경 색상이 회색에서 녹색으로 변경됩니다 (팬 전원이 켜져 있음을 표시).
뷰는 앱 UI의 기본 구성요소입니다. View
클래스는 Android 위젯이라고 하는 여러 서브클래스를 제공하며 이 서브클래스는 일반적인 Android 앱의 사용자 인터페이스 요구사항을 대부분 다룹니다.
Button
및 TextView
와 같은 UI 빌딩 블록은 View
클래스를 확장하는 서브클래스입니다. 시간을 절약하고 개발 작업을 절약하기 위해 이러한 View
서브클래스 중 하나를 확장할 수 있습니다. 맞춤 뷰는 상위 요소의 스타일 및 동작을 상속합니다. 변경하려는 모양이나 동작의 동작 또는 측면을 재정의할 수 있습니다. 예를 들어 EditText
를 확장하여 맞춤 뷰를 만들면 뷰는 EditText
뷰처럼 작동하지만 예를 들어 텍스트 입력 필드에서 텍스트를 지우는 X 버튼도 표시하도록 맞춤설정할 수 있습니다.
View
와 같은 View
서브클래스를 확장하여 맞춤 뷰를 얻을 수 있습니다. 원하는 클래스와 가장 가까운 하나를 선택하세요. 그런 다음 하나 이상의 레이아웃에서 다른 View
서브클래스와 마찬가지로 맞춤 뷰를 속성이 있는 XML 요소로 사용할 수 있습니다.
자체 맞춤 뷰를 처음부터 만들려면 View
클래스 자체를 확장합니다. 코드는 View
메서드를 재정의하여 뷰의 모양과 기능을 정의합니다. 자체 맞춤 뷰를 만드는 핵심은 모든 크기와 도형의 전체 UI 요소를 화면에 그리도록 해야 한다는 것입니다. Button
와 같은 기존 뷰의 서브클래스를 만들면 이 클래스에서 자동으로 그리기를 처리합니다. (이 Codelab의 뒷부분에서 그림에 관해 자세히 알아봅니다.)
맞춤 뷰를 만드는 방법은 다음과 같습니다.
View
를 확장하거나Button
또는EditText
와 같은View
서브클래스를 확장하는 맞춤 뷰 클래스를 만듭니다.- 기존
View
서브클래스를 확장하는 경우 변경하려는 디자인의 동작이나 측면만 재정의합니다. View
클래스를 확장하는 경우 새 클래스에서onDraw()
및onMeasure()
와 같은View
메서드를 재정의하여 맞춤 뷰의 모양을 그리고 모양을 제어합니다.- 사용자 상호작용에 응답하기 위한 코드를 추가하고, 필요한 경우 맞춤 뷰를 다시 그립니다.
- 활동의 XML 레이아웃에서 맞춤 뷰 클래스를 UI 위젯으로 사용합니다. 뷰의 맞춤 속성을 정의하여 다양한 레이아웃의 뷰를 맞춤설정할 수도 있습니다.
이 작업에서는 다음 작업을 수행합니다.
ImageView
를 맞춤 뷰의 임시 자리표시자로 사용하여 앱을 만듭니다.View
를 확장하여 맞춤 뷰를 만듭니다.- 그리기 및 색칠 값으로 맞춤 뷰를 초기화합니다.
1단계: ImageView 자리표시자로 앱 만들기
- 제목이 Empty Activity 템플릿을 사용하여 제목이
CustomFanController
인 Kotlin 앱을 만듭니다. 패키지 이름이com.example.android.customfancontroller
인지 확인합니다. - Text 탭에서
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 요소에서 문자열 및 측정기준 리소스를 추출합니다.
- Design 탭을 클릭합니다. 레이아웃은 다음과 같습니다.
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 = -35
DialView
클래스 내에서 맞춤 뷰를 그리는 데 필요한 여러 변수를 정의합니다. 요청이 있는 경우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()
메서드를 사용하여 팬 컨트롤러 맞춤 뷰를 화면에(다이얼 자체, 현재 위치 표시기, 표시기 라벨) 그립니다. 또한 도우미 메서드인 computeXYForSpeed(),
을 만들어 다이얼에서 표시기 라벨의 현재 X,Y 위치를 계산합니다.
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.GREEN
- 이 코드를 추가하여
drawCircle()
메서드로 다이얼에 원을 그리세요. 이 메서드는 현재 뷰 너비와 높이를 사용하여 원의 중심, 원의 반경 및 현재 페인트 색상을 찾습니다.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" />
- 앱을 실행합니다. 활동에 팬 제어 보기가 표시됩니다.
마지막 작업은 맞춤 뷰를 사용 설정하여 사용자가 뷰를 탭할 때 특정 작업을 실행하는 것입니다. 한 번 누르면 선택 표시기가 다음 위치(꺼짐-1-2-3)로 돌아갔다가 다시 꺼집니다. 또한 선택사항이 1 이상이면 배경이 회색에서 초록색으로 바뀌어 팬 전원이 켜져 있음을 나타냅니다.
맞춤 뷰를 클릭 가능하게 하려면 다음과 같이 하세요.
- 뷰의
isClickable
속성을true
로 설정합니다. 이렇게 하면 맞춤 보기에서 클릭에 응답할 수 있습니다. View
클래스performClick
()
를 구현하여 뷰를 클릭했을 때 작업을 실행합니다.invalidate()
메서드를 호출합니다. 그러면 Android 시스템에서onDraw()
메서드를 호출하여 뷰를 다시 그립니다.
일반적으로 표준 Android 뷰에서는 OnClickListener()
를 구현하여 사용자가 해당 뷰를 클릭할 때 작업을 실행합니다. 맞춤 뷰의 경우, View
클래스의 performClick
()
메서드를 대신 구현하고 super
를 호출합니다.performClick().
기본 performClick()
메서드도 onClickListener()
를 호출하므로 작업을 performClick()
에 추가하고 개발자 또는 맞춤 뷰를 사용할 수 있는 다른 개발자가 onClickListener()
를 추가로 맞춤설정할 수 있습니다.
DialView.kt
의FanSpeed
열거 내에서 현재 팬 속도를 목록의 다음 속도 (OFF
에서LOW
,MEDIUM
,HIGH
로 변경 후 다시OFF
)로 변경하는 확장 함수next()
를 추가합니다. 이제 전체 열거가 다음과 같습니다.
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()
메서드로 늘리고 뷰의 콘텐츠 설명을 현재 속도 (끔, 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:fanColor1
과 같이 맞춤 속성의 맨 앞에app:
를 사용합니다.
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 = 0
init
블록에서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.
GRAY
else
Color.
GREEN
로 바꿉니다.
paint.color = when (fanSpeed) {
FanSpeed.OFF -> Color.GRAY
FanSpeed.LOW -> fanSpeedLowColor
FanSpeed.MEDIUM -> fanSpeedMediumColor
FanSpeed.HIGH -> fanSeedMaxColor
} as Int
- 앱을 실행하고 다이얼을 클릭하면 아래와 같이 위치마다 색상 설정이 달라집니다.
맞춤 뷰 속성에 관한 자세한 내용은 뷰 클래스 만들기를 참고하세요.
접근성은 장애인을 포함하여 모든 사람이 앱을 사용할 수 있도록 하는 디자인, 구현 및 테스트 기법의 모음입니다.
사람에게 영향을 미칠 수 있는 일반적인 장애로는 시각 장애, 저시력, 색맹, 청각 장애, 청각 장애, 거동 장애가 있습니다. 접근성을 염두에 두고 앱을 개발할 때는 장애가 있는 사용자뿐만 아니라 다른 모든 사용자를 위해 사용자 환경을 개선해야 합니다.
Android는 표준 UI 뷰에서 TextView
및 Button
와 같은 여러 접근성 기능을 기본적으로 제공합니다. 하지만 맞춤 뷰를 만들 때는 맞춤 콘텐츠가 화면의 콘텐츠에 대한 음성 설명과 같은 접근성 기능을 어떻게 제공하는지 고려해야 합니다.
이 작업에서는 음성 안내 지원, Android의 스크린 리더에 관해 알아보고 DialView
맞춤 뷰의 음성 안내 지원 힌트와 설명을 포함하도록 앱을 수정합니다.
1단계. 음성 안내 지원 탐색
음성 안내 지원은 Android의 내장 스크린 리더입니다. 음성 안내 지원을 사용 설정하면 Android에서 화면 요소를 소리 내어 설명하므로 사용자는 화면을 보지 않고도 Android 기기와 상호작용할 수 있습니다. 시각장애인의 경우 음성 안내 지원을 통해 앱을 사용할 수 있습니다.
이 작업에서는 스크린 리더의 작동 방식과 앱을 탐색하는 방법을 이해할 수 있도록 음성 안내 지원을 사용 설정합니다.
- Android 기기 또는 에뮬레이터에서 Settings > Accessibility > 음성 안내 지원으로 이동합니다.
- 사용/사용 중지 전환 버튼을 탭하여 음성 안내 지원을 사용 설정합니다.
- 확인을 탭하여 권한을 확인합니다.
- 메시지가 표시되면 기기 비밀번호를 확인합니다. 음성 안내 지원을 처음 실행한다면 가이드가 실행됩니다. (이전 기기에서는 가이드를 사용하지 못할 수도 있습니다.)
- 눈을 감고 튜토리얼을 탐색하는 것이 도움이 될 수 있습니다. 나중에 튜토리얼을 다시 열려면 설정 및 접근성, 음성 안내 지원 및 음성 안내 지원, 음성 안내 지원 설정 및 음성 안내 지원 가이드 시작하기로 이동하세요.
CustomFanController
앱을 컴파일하고 실행하거나 기기의 Overview 또는 Recents 버튼으로 엽니다. 음성 안내 지원을 사용 설정하면 앱 이름 및TextView
라벨("팬 제어")의 텍스트가 표시됩니다. 하지만DialView
뷰 자체를 탭하면 뷰 상태 (다이얼의 현재 설정)나 뷰를 탭하여 활성화할 때 발생하는 작업에 관한 정보가 음성으로 안내되지 않습니다.
2단계: 다이얼 라벨의 콘텐츠 설명 추가
콘텐츠 설명은 앱 뷰의 의미와 목적을 설명합니다. Android의 음성 안내 지원 기능과 같은 스크린 리더를 통해 각 요소의 기능을 정확하게 설명할 수 있습니다. ImageView
과 같은 정적 뷰의 경우 contentDescription
속성을 사용하여 레이아웃 파일의 뷰에 콘텐츠 설명을 추가할 수 있습니다. 텍스트 뷰(TextView
및 EditText
)는 뷰의 텍스트를 자동으로 콘텐츠 설명으로 사용합니다.
맞춤 팬 컨트롤 보기의 경우, 보기를 클릭할 때마다 콘텐츠 설명을 동적으로 업데이트하여 현재 팬 설정을 표시해야 합니다.
DialView
클래스 하단에서 인수나 반환 유형이 없는 함수updateContentDescription()
를 선언합니다.
fun updateContentDescription() {
}
updateContentDescription()
내부에서 맞춤 뷰의contentDescription
속성을 현재 팬 속도 (사용 중지, 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
}
- 앱을 컴파일하고 실행한 후 음성 안내 지원이 사용 설정되어 있는지 확인합니다. 탭하여 다이얼 보기의 설정을 변경하면 이제 음성 안내 지원에서 현재 라벨 (끄기, 1, 2, 3)은 물론 '두 번 탭하여 활성화'라고 알려주는 것을 볼 수 있습니다.
3단계: 클릭 액션에 대한 정보 추가하기
여기서 멈추고 음성 안내 지원에서 뷰를 사용할 수 있습니다. 하지만 이렇게 하면 뷰가 활성화될 수 있다고("두 번 탭하여 활성화'할 수 있음)할 수 있을 뿐 아니라 뷰가 활성화되면 어떤 상태가 될지 설명("두 번 탭하여 재설정)(2쿼터)하면 도움이 될 수 있습니다.
이를 위해 접근성 위임을 통해 보기의 작업 (여기서는 클릭 또는 탭 작업)에 대한 정보를 접근성 노드 정보 객체에 추가합니다. 접근성 위임을 사용하면 상속이 아닌 컴포지션을 통해 앱의 접근성 관련 기능을 맞춤설정할 수 있습니다.
이 작업에서는 Android Jetpack 라이브러리(androidx.*
)의 접근성 클래스를 사용하여 이전 버전과의 호환성을 보장합니다.
DialView.kt
의init
블록에서 뷰의 접근성 위임을 새AccessibilityDelegateCompat
객체로 설정합니다. 요청이 있는 경우androidx.core.view.ViewCompat
및androidx.core.view.AccessibilityDelegateCompat
를 가져옵니다. 이 전략을 사용하면 앱의 이전 버전과의 호환성이 가장 높아집니다.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
})
AccessibilityDelegateCompat
객체 내에서onInitializeAccessibilityNodeInfo()
함수를AccessibilityNodeInfoCompat
객체로 재정의하고 슈퍼&메서드를 호출합니다. 메시지가 표시되면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
)와 음성 안내 지원에서 작업이 무엇인지 나타내는 문자열이 필요합니다.
- 문자열 리소스를 검색하려면
"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
에서 "Change" 및 "Reset" 문자열 문자열을 추가합니다.
<string name="change">Change</string>
<string name="reset">Reset</string>
- 앱을 컴파일하고 실행한 후 음성 안내 지원이 사용 설정되어 있는지 확인합니다. 이제 "두 번 탭하여 활성화' 문구는 "두 번 탭하여 변경" (팬 속도가 높음 또는 3 미만인 경우) 또는 "두 번 탭하여 재설정(2013년) 프롬프트 '두 번 탭하기...'는 음성 안내 지원 서비스 자체에서 제공됩니다.
완성된 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()
Canvas
Paint
drawText()
setTypeface()
setColor()
drawRect()
drawOval()
drawArc()
drawBitmap()
setStyle()
invalidate()
- 보기
- 입력 이벤트
- 페인트
- Kotlin 확장 프로그램 라이브러리 android-ktx
withStyledAttributes
- Android KTX 문서
- Android KTX 원본 공지사항 블로그
- 맞춤 뷰의 접근성 높이기
AccessibilityDelegateCompat
AccessibilityNodeInfoCompat
AccessibilityNodeInfoCompat.AccessibilityActionCompat
동영상:
이 섹션에는 강사가 진행하는 과정의 일부로 이 Codelab을 통해 작업하는 학생들의 숙제 과제가 나와 있습니다. 강사는 다음을 처리합니다.
- 필요한 경우 과제를 할당합니다.
- 학생에게 과제 과제를 제출하는 방법을 알려주세요.
- 과제 과제를 채점합니다.
강사는 이러한 추천을 원하는 만큼 사용할 수 있으며 다른 적절한 숙제를 할당해도 좋습니다.
이 Codelab을 직접 학습하고 있다면 언제든지 숙제를 통해 지식을 확인해 보세요.
질문 1
맞춤 뷰에 크기가 먼저 할당될 때 위치, 측정기준, 기타 값을 계산하려면 어떤 방법을 재정의해야 하나요?
▢ onMeasure()
▢ onSizeChanged()
▢ invalidate()
▢ onDraw()
질문 2
속성 값을 변경한 후 onDraw()
를 사용하여 뷰를 다시 그리려고 함을 나타내려면 UI 스레드에서 어떤 메서드를 호출해야 하나요?
▢ onMeasure()
▢ onSizeChanged()
▢ invalidate()
▢ getVisibility()
질문 3
맞춤 뷰에 상호작용을 추가하려면 어떤 View
메서드를 재정의해야 하나요?
▢ setOnClickListener()
▢ onSizeChanged()
▢ isClickable()
▢ performClick()
이 과정의 다른 Codelab에 관한 링크는 Kotlin Codelab의 고급 Android 방문 페이지를 참고하세요.