맞춤 뷰 만들기

이 Codelab은 Kotlin 기반 Android 고급 교육 과정의 일부입니다. Codelab을 순서대로 진행하는 경우 학습 효과를 극대화할 수 있지만 순서를 바꿔 진행해도 괜찮습니다. 모든 교육 과정 Codelab은 Kotlin 기반 고급 Android Codelab 방문 페이지에 나열되어 있습니다.

소개

Android는 Button, TextView, EditText, ImageView, CheckBox, RadioButton 등 다양한 View 하위 클래스를 제공합니다. 이러한 서브클래스를 사용하여 사용자 상호작용을 지원하고 앱에 정보를 표시하는 UI를 구성할 수 있습니다. View 서브클래스가 요구사항을 충족하지 않는 경우 맞춤 뷰라고 하는 View 서브클래스를 만들 수 있습니다.

맞춤 뷰를 만들려면 기존 View 서브클래스 (예: Button 또는 EditText)를 확장하거나 View의 자체 서브클래스를 만들면 됩니다. View를 직접 확장하면 ViewonDraw() 메서드를 재정의하여 모든 크기와 모양의 상호작용 UI 요소를 만들어 그릴 수 있습니다.

맞춤 뷰를 만든 후 TextView 또는 Button를 추가하는 것과 같은 방식으로 활동 레이아웃에 추가할 수 있습니다.

이 과정에서는 View를 확장하여 처음부터 맞춤 뷰를 만드는 방법을 보여줍니다.

기본 요건

  • 활동이 있는 앱을 만들고 Android 스튜디오를 사용하여 실행하는 방법

학습할 내용

  • View을 확장하여 맞춤 뷰를 만드는 방법
  • 원형 모양의 맞춤 뷰를 그리는 방법
  • 리스너를 사용하여 맞춤 뷰와의 사용자 상호작용을 처리하는 방법
  • 레이아웃에서 맞춤 뷰를 사용하는 방법

실습할 내용

  • View를 확장하여 맞춤 뷰를 만듭니다.
  • 그리기 및 페인팅 값으로 맞춤 뷰를 초기화합니다.
  • onDraw()를 재정의하여 뷰를 그립니다.
  • 리스너를 사용하여 맞춤 뷰의 동작을 제공합니다.
  • 맞춤 뷰를 레이아웃에 추가합니다.

CustomFanController 앱은 View 클래스를 확장하여 맞춤 뷰 하위 클래스를 만드는 방법을 보여줍니다. 새 서브클래스의 이름은 DialView입니다.

앱은 오프 (0), 로우 (1), 미디엄 (2), 하이 (3) 설정이 있는 실제 팬 컨트롤과 유사한 원형 UI 요소를 표시합니다. 사용자가 뷰를 탭하면 선택 표시기가 다음 위치(0-1-2-3)로 이동한 후 다시 0으로 돌아갑니다. 또한 선택이 1 이상이면 뷰의 원형 부분의 배경색이 회색에서 녹색으로 변경됩니다 (팬 전원이 켜져 있음을 나타냄).

뷰는 앱 UI의 기본 구성요소입니다. View 클래스는 일반적인 Android 앱의 사용자 인터페이스의 많은 요구사항을 충족하는 UI 위젯이라고 하는 많은 서브클래스를 제공합니다.

ButtonTextView과 같은 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 자리표시자가 있는 앱 만들기

  1. Empty Activity 템플릿을 사용하여 제목이 CustomFanController인 Kotlin 앱을 만듭니다. 패키지 이름이 com.example.android.customfancontroller인지 확인합니다.
  2. 텍스트 탭에서 activity_main.xml을 열어 XML 코드를 수정합니다.
  3. 기존 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"/>
  1. 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"/>
  1. 두 UI 요소에서 문자열 및 크기 리소스를 추출합니다.
  2. 디자인 탭을 클릭합니다. 레이아웃은 다음과 같이 표시됩니다.

2단계. 맞춤 뷰 클래스 만들기

  1. DialView라는 새 Kotlin 클래스를 만듭니다.
  2. 클래스 정의를 수정하여 View를 확장합니다. 메시지가 표시되면 android.view.View를 가져옵니다.
  3. View를 클릭한 다음 빨간색 전구를 클릭합니다. '@JvmOverloads'를 사용하여 Android 뷰 생성자 추가를 선택합니다. Android 스튜디오에서 View 클래스의 생성자를 추가합니다. @JvmOverloads 주석은 Kotlin 컴파일러에 기본 매개변수 값을 대체하는 이 함수의 오버로드를 생성하도록 지시합니다.
class DialView @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
  1. DialView 클래스 정의 위, 가져오기 바로 아래에 최상위 enum을 추가하여 사용 가능한 팬 속도를 나타냅니다. 값이 실제 문자열이 아닌 문자열 리소스이므로 이 enumInt 유형입니다. 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);
}
  1. enum 아래에 다음 상수를 추가합니다. 다이얼 표시기와 라벨을 그리는 과정에서 이를 사용합니다.
private const val RADIUS_OFFSET_LABEL = 30      
private const val RADIUS_OFFSET_INDICATOR = -35
  1. 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 포인트입니다.

이러한 값은 뷰가 실제로 그려질 때가 아니라 여기에서 생성되고 초기화되어 실제 그리기 단계가 최대한 빠르게 실행되도록 합니다.

  1. DialView 클래스 정의 내부에서 몇 가지 기본 스타일을 사용하여 Paint 객체를 초기화합니다. 요청이 있는 경우 android.graphics.Paintandroid.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)
}
  1. 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()에 할당을 배치하지 마세요.

CanvasPaint 클래스는 다음과 같은 유용한 그리기 단축키를 제공합니다.

  • drawText()를 사용하여 텍스트를 그립니다. setTypeface()를 호출하여 글꼴을 지정하고 setColor()를 호출하여 텍스트 색상을 지정합니다.
  • drawRect(), drawOval(), drawArc()를 사용하여 기본 도형을 그립니다. setStyle()을 호출하여 도형을 채우는지, 윤곽선만 표시하는지 또는 둘 다인지 설정합니다.
  • drawBitmap()을 사용하여 비트맵을 그립니다.

CanvasPaint에 관해서는 이후 Codelab에서 자세히 알아봅니다. Android에서 뷰를 그리는 방법에 관한 자세한 내용은 Android에서 뷰를 그리는 방법을 참고하세요.

이 작업에서는 onSizeChanged()onDraw() 메서드를 사용하여 팬 컨트롤러 맞춤 뷰를 화면에 그립니다(다이얼 자체, 현재 위치 표시기, 표시기 라벨). 또한 다이얼의 표시기 라벨의 현재 X,Y 위치를 계산하는 도우미 메서드 computeXYForSpeed(),를 만듭니다.

1단계: 위치를 계산하고 뷰를 그립니다.

  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()
}
  1. onSizeChanged() 아래에 다음 코드를 추가하여 PointF 클래스의 computeXYForSpeed() 확장 함수를 정의합니다. 요청이 있는 경우 kotlin.math.coskotlin.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
}
  1. onDraw() 메서드를 재정의하여 CanvasPaint 클래스로 화면에 뷰를 렌더링합니다. 요청이 있는 경우 android.graphics.Canvas를 가져옵니다. 스켈레톤 재정의는 다음과 같습니다.
override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   
}
  1. 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
  1. drawCircle() 메서드를 사용하여 다이얼의 원을 그리는 코드를 추가합니다. 이 메서드는 현재 뷰 너비와 높이를 사용하여 원의 중심, 원의 반지름, 현재 페인트 색상을 찾습니다. widthheight 속성은 View 상위 클래스의 멤버이며 뷰의 현재 크기를 나타냅니다.
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
  1. 다음 코드를 추가하여 팬 속도 표시기 마크에 더 작은 원을 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)
  1. 마지막으로 다이얼 주변의 적절한 위치에 팬 속도 라벨 (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 요소 속성으로 모양과 동작을 제어합니다.

  1. activity_main.xml에서 dialViewImageView 태그를 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. 앱을 실행합니다. 팬 제어 보기가 활동에 표시됩니다.

마지막 작업은 사용자가 뷰를 탭할 때 맞춤 뷰가 작업을 실행하도록 하는 것입니다. 탭할 때마다 선택 표시기가 다음 위치로 이동해야 합니다(off-1-2-3 및 다시 off). 또한 선택이 1 이상이면 배경을 회색에서 녹색으로 변경하여 팬 전원이 켜져 있음을 나타냅니다.

맞춤 뷰를 클릭할 수 있도록 하려면 다음을 실행해야 합니다.

  • 뷰의 isClickable 속성을 true로 설정합니다. 이렇게 하면 맞춤 뷰가 클릭에 응답할 수 있습니다.
  • 뷰를 클릭할 때 작업을 실행하도록 View 클래스의 performClick()를 구현합니다.
  • invalidate() 메서드를 호출합니다. 이렇게 하면 Android 시스템에서 onDraw() 메서드를 호출하여 뷰를 다시 그립니다.

일반적으로 표준 Android 뷰를 사용하면 사용자가 해당 뷰를 클릭할 때 작업을 실행하도록 OnClickListener()를 구현합니다. 맞춤 뷰의 경우 View 클래스의 performClick() 메서드를 대신 구현하고 super를 호출합니다.performClick(). 기본 performClick() 메서드도 onClickListener()를 호출하므로 performClick()에 작업을 추가하고 onClickListener()는 맞춤 뷰를 사용할 수 있는 다른 개발자나 사용자가 추가로 맞춤설정할 수 있도록 남겨둘 수 있습니다.

  1. DialView.ktFanSpeed 열거형 내에서 현재 팬 속도를 목록의 다음 속도로 변경하는 확장 함수 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
   }
}
  1. DialView 클래스 내에서 onSizeChanged() 메서드 바로 앞에 init() 블록을 추가합니다. 뷰의 isClickable 속성을 true로 설정하면 뷰가 사용자 입력을 수락할 수 있습니다.
init {
   isClickable = true
}
  1. 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().를 호출합니다.

  1. 앱을 실행합니다. DialView 요소를 탭하여 표시기를 off에서 1로 이동합니다. 다이얼이 녹색으로 바뀝니다. 탭할 때마다 표시기가 다음 위치로 이동해야 합니다. 표시기가 꺼짐으로 돌아가면 다이얼이 다시 회색으로 바뀝니다.

이 예에서는 맞춤 뷰에서 맞춤 속성을 사용하는 기본 메커니즘을 보여줍니다. 각 팬 다이얼 위치에 다른 색상을 사용하여 DialView 클래스의 맞춤 속성을 정의합니다.

  1. res/values/attrs.xml을 만들고 엽니다.
  2. <resources> 내부에 <declare-styleable> 리소스 요소를 추가합니다.
  3. <declare-styleable> 리소스 요소 내에 속성별로 nameformat가 있는 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>
  1. activity_main.xml 레이아웃 파일을 엽니다.
  2. 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에서 속성을 가져오고 캐싱을 위해 속성 값을 로컬 변수에 할당합니다.

  1. DialView.kt 클래스 파일을 엽니다.
  2. DialView 내에서 속성 값을 캐시하는 변수를 선언합니다.
private var fanSpeedLowColor = 0
private var fanSpeedMediumColor = 0
private var fanSeedMaxColor = 0
  1. 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)
}
  1. 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
  1. 앱을 실행하고 다이얼을 클릭하면 아래와 같이 각 위치의 색상 설정이 다릅니다.

맞춤 뷰 속성에 대해 자세히 알아보려면 뷰 클래스 만들기를 참고하세요.

접근성은 장애인을 포함한 모든 사람이 앱을 사용할 수 있도록 하는 디자인, 구현, 테스트 기법의 집합입니다.

Android 기기 사용에 영향을 줄 수 있는 일반적인 장애로는 시각 장애, 저시력, 색맹, 청각 장애, 난청, 거동 장애가 있습니다. 접근성을 염두에 두고 앱을 개발하면 이러한 장애가 있는 사용자뿐만 아니라 다른 모든 사용자의 사용자 환경도 개선됩니다.

Android는 TextView, Button과 같은 표준 UI 뷰에서 기본적으로 여러 접근성 기능을 제공합니다. 하지만 맞춤 뷰를 만들 때는 맞춤 뷰가 화면 콘텐츠의 음성 설명과 같은 접근성 기능을 제공하는 방법을 고려해야 합니다.

이 작업에서는 Android의 스크린 리더인 TalkBack에 대해 알아보고 DialView 맞춤 뷰에 대한 음성 안내와 설명을 포함하도록 앱을 수정합니다.

1단계. TalkBack 살펴보기

TalkBack은 Android의 내장 스크린 리더입니다. TalkBack을 사용 설정하면 Android에서 화면 요소를 소리 내어 설명하므로 사용자는 화면을 보지 않고도 Android 기기와 상호작용할 수 있습니다. 시각장애인의 경우 TalkBack을 통해 앱을 사용할 수 있습니다.

이 작업에서는 TalkBack을 사용 설정하여 스크린 리더가 작동하는 방식과 앱을 탐색하는 방법을 알아봅니다.

  1. Android 기기 또는 에뮬레이터에서 설정 > 접근성 > TalkBack으로 이동합니다.
  2. 사용/사용 안함 전환 버튼을 탭하여 TalkBack을 사용 설정합니다.
  3. 확인을 탭하여 권한을 확인합니다.
  4. 메시지가 표시되면 기기 비밀번호를 확인합니다. TalkBack을 처음 실행하면 튜토리얼이 시작됩니다. (오래된 기기에서는 튜토리얼을 사용하지 못할 수 있습니다.)
  5. 눈을 감고 튜토리얼을 탐색하는 것이 도움이 될 수 있습니다. 나중에 다시 튜토리얼을 열려면 설정 > 접근성 > TalkBack > 설정 > TalkBack 튜토리얼 시작하기로 이동하세요.
  6. CustomFanController 앱을 컴파일하고 실행하거나 기기의 개요 또는 최근 버튼으로 엽니다. TalkBack을 사용 설정하면 앱 이름과 라벨 TextView ('팬 제어')의 텍스트가 함께 안내됩니다. 하지만 DialView 뷰 자체를 탭하면 뷰의 상태 (다이얼의 현재 설정)나 뷰를 탭하여 활성화할 때 발생하는 작업에 관한 정보가 음성으로 제공되지 않습니다.

2단계: 다이얼 라벨의 콘텐츠 설명 추가

콘텐츠 설명은 앱의 뷰의 의미와 목적을 설명합니다. 이러한 라벨이 있어야 Android의 TalkBack 기능과 같은 스크린 리더가 각 요소의 기능을 정확하게 설명해 줄 수 있습니다. ImageView와 같은 정적 뷰의 경우 contentDescription 속성을 사용하여 레이아웃 파일의 뷰에 콘텐츠 설명을 추가할 수 있습니다. 텍스트 뷰 (TextViewEditText)는 뷰의 텍스트를 콘텐츠 설명으로 자동 사용합니다.

맞춤 팬 제어 뷰의 경우 뷰를 클릭할 때마다 콘텐츠 설명을 동적으로 업데이트하여 현재 팬 설정을 나타내야 합니다.

  1. DialView 클래스 하단에서 인수나 반환 유형이 없는 함수 updateContentDescription()를 선언합니다.
fun updateContentDescription() {
}
  1. updateContentDescription() 내에서 맞춤 뷰의 contentDescription 속성을 현재 팬 속도 (off, 1, 2, 3)와 연결된 문자열 리소스로 변경합니다. 이러한 라벨은 다이얼이 화면에 그려질 때 onDraw()에 사용되는 라벨과 동일합니다.
fun updateContentDescription() {
   contentDescription = resources.getString(fanSpeed.label)
}
  1. init() 블록으로 스크롤하고 블록 끝에 updateContentDescription() 호출을 추가합니다. 이렇게 하면 뷰가 초기화될 때 콘텐츠 설명이 초기화됩니다.
init {
   isClickable = true
   // ...

   updateContentDescription()
}
  1. performClick() 메서드에서 invalidate() 바로 앞에 updateContentDescription() 호출을 하나 더 추가합니다.
override fun performClick(): Boolean {
   if (super.performClick()) return true
   fanSpeed = fanSpeed.next()
   updateContentDescription()
   invalidate()
   return true
}
  1. 앱을 컴파일하고 실행한 후 TalkBack이 사용 설정되어 있는지 확인합니다. 탭하여 다이얼 뷰의 설정을 변경하면 이제 TalkBack에서 현재 라벨 (off, 1, 2, 3)과 '더블 탭하여 활성화'라는 문구를 함께 알립니다.

3단계: 클릭 작업에 대한 추가 정보 추가

여기까지만 해도 TalkBack에서 뷰를 사용할 수 있습니다. 하지만 뷰가 활성화될 수 있다는 것 ('더블 탭하여 활성화')뿐만 아니라 뷰가 활성화될 때 어떤 일이 일어나는지 ('더블 탭하여 변경' 또는 '더블 탭하여 재설정') 설명해 주면 도움이 됩니다.

이렇게 하려면 접근성 위임을 통해 접근성 노드 정보 객체에 뷰의 작업 (여기서는 클릭 또는 탭 작업)에 관한 정보를 추가합니다. 접근성 위임자를 사용하면 상속이 아닌 컴포지션을 통해 앱의 접근성 관련 기능을 맞춤설정할 수 있습니다.

이 작업에서는 이전 버전과의 호환성을 위해 Android Jetpack 라이브러리 (androidx.*)의 접근성 클래스를 사용합니다.

  1. DialView.ktinit 블록에서 뷰의 접근성 위임자를 새 AccessibilityDelegateCompat 객체로 설정합니다. 요청이 있는 경우 androidx.core.view.ViewCompatandroidx.core.view.AccessibilityDelegateCompat를 가져옵니다. 이 전략을 사용하면 앱에서 하위 호환성을 최대한 확보할 수 있습니다.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   
})
  1. 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의 접근성 서비스는 이러한 노드를 탐색하여 뷰에 관한 정보 (예: 음성으로 읽을 수 있는 콘텐츠 설명 또는 해당 뷰에서 실행할 수 있는 작업)를 알아냅니다. 맞춤 뷰를 만들 때 접근성을 위한 맞춤 정보를 제공하기 위해 노드 정보를 재정의해야 할 수도 있습니다. 이 경우 노드 정보를 재정의하여 뷰의 작업에 맞춤 정보가 있음을 나타냅니다.

  1. 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에서 작업이 무엇인지 나타내는 데 사용하는 문자열이 필요합니다.

  1. "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)
      )
   }  
})
  1. 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)
   }
})
  1. res/values/strings.xml에서 '변경' 및 '재설정'의 문자열 리소스를 추가합니다.
<string name="change">Change</string>
<string name="reset">Reset</string>
  1. 앱을 컴파일하고 실행한 후 TalkBack이 사용 설정되어 있는지 확인합니다. 이제 '더블 탭하여 활성화'라는 문구가 '더블 탭하여 변경' (팬 속도가 높음 또는 3 미만인 경우) 또는 '더블 탭하여 재설정' (팬 속도가 이미 높음 또는 3인 경우)으로 표시됩니다. '두 번 탭하여...'이라는 메시지는 TalkBack 서비스 자체에서 제공합니다.

완료된 Codelab의 코드를 다운로드합니다.

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-custom-views


또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.

ZIP 파일 다운로드

  • EditText과 같은 View 서브클래스의 모양과 동작을 상속하는 맞춤 뷰를 만들려면 해당 서브클래스를 확장하는 새 클래스를 추가하고 서브클래스의 메서드 일부를 재정의하여 조정합니다.
  • 크기와 모양이 다양한 맞춤 뷰를 만들려면 View를 확장하는 새 클래스를 추가합니다.
  • onDraw()과 같은 View 메서드를 재정의하여 뷰의 모양과 기본 모양을 정의합니다.
  • invalidate()를 사용하여 뷰를 강제로 그리거나 다시 그립니다.
  • 성능을 최적화하려면 onDraw()에서 변수를 사용하기 전에 멤버 변수 초기화와 같이 그리기 및 페인팅에 필요한 값을 할당하세요.
  • OnClickListener() 대신 맞춤 뷰에 performClick()를 재정의하여 뷰의 대화형 동작을 제공합니다. 이를 통해 맞춤 뷰 클래스를 사용할 수 있는 개발자나 다른 Android 개발자가 onClickListener()를 사용하여 추가 동작을 제공할 수 있습니다.
  • 다른 UI 요소와 마찬가지로 모양을 정의하는 속성을 사용하여 맞춤 뷰를 XML 레이아웃 파일에 추가합니다.
  • values 폴더에 attrs.xml 파일을 만들어 맞춤 속성을 정의합니다. 그런 다음 XML 레이아웃 파일에서 맞춤 뷰의 맞춤 속성을 사용할 수 있습니다.

Udacity 과정:

Android 개발자 문서:

동영상:

이 섹션에는 강사가 진행하는 과정의 일부로 이 Codelab을 진행하는 학생에게 출제할 수 있는 과제가 나열되어 있습니다. 다음 작업은 강사가 결정합니다.

  • 필요한 경우 과제를 할당합니다.
  • 과제 제출 방법을 학생에게 알립니다.
  • 과제를 채점합니다.

강사는 이러한 추천을 원하는 만큼 사용할 수 있으며 적절하다고 생각되는 다른 과제를 출제해도 됩니다.

이 Codelab을 직접 진행하는 경우 이러한 과제를 자유롭게 사용하여 배운 내용을 테스트해 보세요.

질문 1

맞춤 뷰에 처음 크기가 할당될 때 위치, 크기, 기타 값을 계산하려면 어떤 메서드를 재정의해야 하나요?

onMeasure()

onSizeChanged()

invalidate()

onDraw()

질문 2

속성 값이 변경된 후 뷰를 onDraw()로 다시 그려야 함을 나타내려면 UI 스레드에서 어떤 메서드를 호출해야 하나요?

▢ onMeasure()

▢ onSizeChanged()

▢ invalidate()

▢ getVisibility()

질문 3

맞춤 뷰에 상호작용을 추가하려면 어떤 View 메서드를 재정의해야 하나요?

▢ setOnClickListener()

▢ onSizeChanged()

▢ isClickable()

▢ performClick()

이 과정의 다른 Codelab 링크는 Kotlin 기반 Android 고급 Codelab 방문 페이지를 참고하세요.