본문 바로가기
Android/안드로이드 기본지식

[Android] Custom View 만들기

by 붕어사랑 티스토리 2022. 4. 13.
반응형

https://developer.android.com/training/custom-views/create-view

 

뷰 클래스 만들기  |  Android 개발자  |  Android Developers

잘 설계된 맞춤 뷰는 잘 설계된 다른 클래스와 매우 유사합니다. 즉, 사용하기 쉬운 인터페이스로 일련의 특정 기능을 캡슐화하고 CPU 및 메모리를 효율적으로 사용하는 등의 역할을 합니다. 그

developer.android.com

 

0. 개요

먼저 안드로이드의 기본개념을 학습하자. 안드로이드의 UI는 기본적으로 두개의 구성요소로 구성되어 있다

 

View 그리고 ViewGroup

 

이 둘의 상속받는 subclass들을 각각 Widget 그리고 Layout 이라고 한다

 

 

 

View -> 위젯

ViewGroup -> 레이아웃

 

이라고 생각하면 편하다

 

 

위젯의 예시는 Button, TextView, EditText, ListView, CheckBox, RadioButton, Gallery, Spinner 등이 있다

레이아웃의 예시는 LinearLayout, FrameLayout, RelativeLayout 이 있다

 

1. Custom View란?

안드로이드는 여러가지 widget들을 제공한다. 예를들어 Button, TextView, EditText, ListView, CheckBox 등

이렇게 기본적으로 제공되는 위젯들로 대부분 원하는 기능들을 구현 가능하다

허나 안드로이드에서 제공하지 않는 기능을 구현하고 싶다면? 이때 사용하는 것이 Custom View이다

 

 

2. Custom View의 기본적인 구현 flow

큰그림을 보는 관점에서 Custom View를 만드는 방법은 다음과 같은 형태로 진행된다

  1. View 클래스를 확장하는 클래스를 생성한다
  2. View 클래스으 몇가지 메소드를 오버라이딩 한다. 오버라이딩 될 메소드는 보통 'on' 이라는 이름으로 시작한다
    onDraw(), onMeasure(), onKeyDown() 등등
  3. 이러한 방법으로 뼈대가 되는 클래스를 만든후 이를 또 확장하면서 여러가지 CustomView를 만든다.

 

 

 

3. onDraw()와 onMeasure()

커스텀뷰를 만들 때 오버라이딩 될 View 클래스의 함수중 제일 중요한 두 함수는 onDraw() onMeasure()이다

 

 

onDraw(Canvas canvas)

요약하면 뷰를 그려주는 함수

onDraw() 메소드는 Canvas 라는것을 제공한다. 이 Canvas로 원하는 2D graphic을 구현 할 수 있다

 

 

(만약 2D가 아닌 3D 그래픽을 구현하고 싶다면 View가 아닌 SurfaceView를 상속받아야 한다. 그리고 2D와 3D는 다른 스레드에서 그려진다. 자세한 사항은 GLSurfaceViewActivity 샘플을 참고하면 된다)

 

 

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

 

요약하면 뷰의 크기를 결정해주는 함수. 디폴트값으로 100x100 사이즈를 제공함

 

뷰의 크기를 계산하고 width와 height가 결정이 되면 이 함수 안에 setMeasuredDimension(int width, int height)를 반드시 호출해 주어야 한다. 이함수를 call 하는데 실패하면 exception이 난다.

 

 

 

 

 

 

4. 다른 standart method

 

Methods Description
Constructors View의 생성자에는 두가지가 있다. 하나는 코드상에서 정의하는 View의 생성자이고, 다른하나는 XML을 파싱하여 뷰를 생성하는 생성자이다
onFinishInflate() XML으로 파싱되어 뷰와 그 children들이 inflated가 완려되면 호출된다
onMeasure(int, int) 뷰와 뷰의 자식들의 사이즈를 결정한다
onLayout(boolean, int, int, int, int) 뷰의 자식들의 사이즈와 포지션을 결정할 때 호출된다
onSizeChanged(int, int, int, int) 뷰의 사이즈가 바뀔대 호출된다
onDraw(Canvas) 뷰를 그릴 때 호출된다
onKeyDown(int, KeyEvent) key down일 때 호출된다
onKeyUp(int, KeyEvent) key up일 때 호출된다
onTrackballEvent(MotionEvent) 트랙볼모션 이벤트 때 호출된다
onTouchEvent(MotionEvent) 터치 이벤트 때 호출된다
onFocusChanged(boolean, int, Rect) 포커스를 얻을 때, 잃을 때 둘다 호출된다
onWindowFocusChanged(boolean) 뷰를 가진 윈도우가 포커스를 얻을 때, 잃을 때 호출된다
onAttachedToWindow() 뷰가 윈도우에 attach 될 때 호출된다
onDetachedFromWindow() 뷰가 윈도우에 detach 될 때 호출된다
onWindowVisibilityChanged(int) window의 visible이 바뀔 때 호출된다

 

 

 

 

5. Compound Controls, 복합 컨트롤

 

만약 여러분이 커스텀뷰를 만들고 싶은데, 앞서 설명한 방법처럼 뷰를 확장해서 재정의 하는 방식이 아닌, 기존에 이미 있는 뷰들을 그룹핑 하여서 조합하여 만들고 싶다면? Compound Control 을 사용하면 된다.

 

가령 예를들면 Combo Box는 하나의 EditText와 하나의 Button의 조합, 그리고 PopupList의 조합이라고 생각 할 수 있다.

 

 

 

Compound Control 생성 방법

  1. Compound Control은 기본적으로 레이아웃 개념으로 시작한다. 그러므로 Layout을 상속받는 클래스를 만들면 된다
    콤보박스로 예를 들면, LinearLayout에 horizontal orientation으로 생각 할 수 있다
    레이아웃안에 또다른 레이아웃을 nested 할 수 있음을 기억하자
    액티비티 생성방법 처럼 XML 베이스로 정의하여 만들 수 있고 코드상으도 만들 수 있다.
  2. 생성자 안에 superclass가 필요한 파라미터들을 받아와 superclass 생성자에 넘겨준다. 이후 레이아웃이 필요한 다른 View들을 생성자 안에서 생성해준다. 앞서 콤보박으로 예를 들면, 여기서 EidtText와 PopupList를 생성해준다.
    이러한 Layout에 components를 생성할 때 XML을 이용할 수 도 있다.
  3. contained view들의 리스너들을 정의해준다. 예를들어 콤보박스의 리스트아이템을 하나 클릭하면 콤보박스의 텍스트가 바뀌도록 EditText를 수정한다.
  4. 레이아웃을 확장할 때 onDraw()와 onMeasure()는 재정해줄 필요가 없다. 굳이 재정의 하지 않아도 원하는 방식으로 돌아가기 때문. 그래도 재정의하길 원한다면 재정의 해 주자

 

Compound Control은 다음과 같은 이점을 가진다

 

  • 커스텀뷰와 다르게 XML을 이용할 수 있다. Layout을 XML로 정의 할 수 있고, View들을 XML로 정의하여 Layout에 nested 할 수 있다
  • onDraw()와 onMeasure()의 재정의가 딱히 필요하지 않다
  • 빠르게 만들 수 있고, 마치 하나의 component처럼 재활용 할 수 있다

 

 

 

6. 기존의 뷰 수정

앞서 View를 확장하여 커스텀뷰를 만드는 법을 배웠습니다.

헌데 만약 내가 원하는 기능의 뷰가 이미 구현이 되어있다면? 그 뷰를 확장하여 더 빠른 개발을 할 수 있습니다.

 

가령 NotePad 앱을 만들다고 칩시다. 줄이 있는 텍스트를 작성하는 위젯을 만들어야 하는데 이미 비슷한 기능을 가진 LinedEditText라는 위젯이 있습니다.

 

그럼 View를 상속하여 처음부터 구현하는것이 아닌 기존에 이미 구현된 뷰를 확장하여 더 빠르게 개발 할 수 있습니다.

 

 

 

 

 

 

 

 

7. 본격적으로 커스텀 뷰 만들기, 서브클래스

 

앞서 배운것처럼 View를 확장합니다. 또는 이미 원하는기능이 구현된 위젯이 있다면 그것을 확장합니다.(ex: Button)

 

안드로이드 스튜디오와 View가 서로 상호작용 할 수 있으려면 최소 Context 및 AttributeSet 객체를 매개변수로 취급하는 생성자를 제공해야 합니다.

 

이 생성자를 이용하면 Layout Editor에 뷰의 인스턴스를 만들고 고정할 수 있습니다

 

Kotlin

class PieChart(context: Context, attrs: AttributeSet) : View(context, attrs)

Java

class PieChart extends View {
    public PieChart(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

 

 

여기서 개념 몇개 집고 넘어가죠

 

 

Context : 공식문서에 따르면 컨텐스트란 어플리케이션의 전반적인 환경과 소통하는 인터페이스 입니다.

다시말하면 뷰와 어플리케이션과의 소통창구라고 할 수 있겠죠?

 

AtrributeSet : XML로 뷰를 만들때, XML에 정의된 태그 데이터들이 AttributeSet이라는 형태로 데이터가 전달됨

 

 

 

 

 

 

8. 본격적으로 커스텀 뷰 만들기, Custom Attributes 정의, 맞춤속성 정의

자 이제 클래스를 만들었으니 xml에서 뷰를 선언하는 작업을 해야겠죠?

 

 

뷰를 XML로 작성할 때

xmlns:android="http://schemas.android.com/apk/res/android"

라는 문구 많이 보셨을 겁니다.

 

여기서 xmlns는 xml 네임스페이스입니다. 네임스페이스가 android인 안드로이드 패키지를 가져오겠다는 의미입니다.

그리고 저희가 android:layout_width 등등 이렇게 사용하지요

 

이번주제에서는 위와 같이 XML을 통하여 저희만의 View 패키지를 만들어보도록 하겠습니다.

 

 

커스텀뷰는 다음 네가지를 만족시켜야 합니다

 

  • Cusstom attributes의 구조를 <declare-styleable> 엘리먼트에서 정의합니다
  • xml로 ui를 작성할 때 attribute의 value값을 정의합니다
  • 런타임에서 xml에서 정의한 attribute value값을 받아옵니다
  • 받아온 attribute value값을 앞서 정의한 클래스에 적용합니다

 

 

<declare-styleable> 는 주로 res/values/attrs.xml에 정의합니다. 아래는 그 예제입니다

<resources>
   <declare-styleable name="PieChart">
       <attr name="showText" format="boolean" />
       <attr name="labelPosition" format="enum">
           <enum name="left" value="0"/>
           <enum name="right" value="1"/>
       </attr>
   </declare-styleable>
</resources>

 

위 예제에서는 이름이 PieChart인 뷰에 showText와 labelPosition 이라는 속성을 정의하였습니다.

여기서 styleable name인 PieChart는 반드시 클래스 뷰의 이름과 일치할 필요는 없으나 똑같이 맞춰주는게 기본입니다.

 

 

 

 

(아래 빨간글은 공식문서에 나온 내용이지만 옛날 내용입니다. 읽어도 되고, 그냥 건너가시는것도 추천드립니다.)

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

자 xml로 View의 구조를 만들었으니 실제로 이를이용하여 XML에서 UI를 만들어봅시다!

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:custom="http://schemas.android.com/apk/res/com.example.customviews">
 <com.example.customviews.charting.PieChart
     custom:showText="true"
     custom:labelPosition="left" />
</LinearLayout>

 

여기서 주목할것은 아래 내용입니다

xmlns:custom="http://schemas.android.com/apk/res/com.example.customviews"

xmlns는 앞서 설명한것처럼 네임스페이스입니다. 네임스페이스 custom을 만들고

그 패키지의 주소는 http://schemas.android.com/apk/res/[your package name] 로 하겠다는 의미입니다

여기서 your package name에 앞서 정의한 클래스뷰의 패키지 주소를 적어주면 되겠습니다

 

그리고 아래와 같이 사용합니다

 <com.example.customviews.charting.PieChart
     custom:showText="true"
     custom:labelPosition="left" />

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

 

 

 

 

 

라고 적힌게 공식문서 내용입니다.... 하지만 위 내용을 실제로 적용해보면 다음과 같은 에러가 뜹니다

 

custom attributes의 스키마 주소는 항상 아래처럼 되어야 한다는 의미이지요

http://schemas.android.com/apk/res-auto

 

 

고로 위 내용을 아래처럼 바꿔주어야 합니다

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:custom="http://schemas.android.com/apk/res-auto">
     <com.example.customviews.charting.PieChart
         custom:showText="true"
         custom:labelPosition="left" />
    </LinearLayout>

 

res-auto 가 저런 기능을 하는거였군요!

 

 

 

자 여기까지 읽으시면 많이 어지러우실 겁니다.

 

 

내용 정리해 보겠습니다.

 

 

1. View를 확장하는 클래스를 만든다!

2. res/values/attrs.xml에 CustomAttributes를 만든다!

 

 

위 두가지 일을 하면 다음과 같은 작업을 할 수 있는 것 입니다.

 

     <com.example.customviews.charting.PieChart
         custom:showText="true"
         custom:labelPosition="left" />

 

색으로 보면 이해가 되지요?

 

 

 

 

 

 

9. 맞춤속성을 클래스에 적용

자 앞서 설명한것 처럼 xml로 ui를 정의하면 tag값에 적힌 데이터들은 AttributeSet으로 전달된다 하였습니다.

이제 클래스 뷰 에서 Custom Attributes 값을 읽어 옵시다.

 

데이터를 AttributeSet에서 직접 읽어 올 수 있지만 다음과 같은 단점이 존재합니다

 

  • 속성값에 대한 리소스 참조가 결정되지 않음
  • 스타일이 적용되지 않음

흠 위 두개의 말이 뭔말일까요... 사실 잘 모르겠습니다... 검색해도 안나오네요. 다들 무슨말인지 이해 못했나 봅니다.

유추해보건데 xml로 생성된 ui의 attribute값의 주소를 얻어 오지 못하고, attribute의 형태가 어떤지 파악 못한다는 얘기인것 같습니다.

 

 

아무튼! 공식문서에서는 맞춤속성 값을 읽어오기위해 obtainStyledAttributes() 메소드를 이용하라고 권장합니다!

이 메소드는 리턴값으로 TypedArray를 넘겨주며 값이 dereferenced(값을 읽어왔다)가 되어있고, styled 되어있다고 합니다.

 

public class PieChart extends View {
    private boolean mShowText;
    private int textPos;
    public PieChart(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.getTheme().obtainStyledAttributes(
                attrs,
                R.styleable.PieChart,
                0, 0);

        try {
            mShowText = a.getBoolean(R.styleable.PieChart_showText, false);
            textPos = a.getInteger(R.styleable.PieChart_labelPosition, 0);
        } finally {
            a.recycle();
        }
    }
}

 

그리고 TypedArray를 다 사용하였디면 항상 recycle()함수를 호출하라고 공식문서에 명시되어 있습니다!

 

 

 

 

 

 

10. 커스텀뷰의 프로퍼티 노출

앞선 내용에서 우리는 View를 정의할 때 XML에서 Attribute라는것을 통하여 UI의 값들을 전달하였습니다.

허나 이 Attribute라는 것은 View가 생성될 때 딱 한번만 사용됩니다.

 

객체화된 View의 프로퍼티들의 값을 핸들링 하고 싶으면 어떻게 할 까요? getter, setter가 필요할 것입니다.

public boolean isShowText() {
   return mShowText;
}

public void setShowText(boolean showText) {
   mShowText = showText;
   invalidate();
   requestLayout();
}

 

앗 근데 setter에 이상한 함수 두개가 보이네요! 이게 뭘까요?

 

 

결론부터 말씀드리면, setter를 통해 mShowText에 값을 바꾸어도 화면에 보이는 UI는 바로 바뀌지 않습니다.

UI를 바뀌게 하려면, UI에 객체의 프로퍼티값이 바뀌었다고 알려야 하기 때문이지요!

 

이를 가능하게 하는 것이 바로 invalidate()함수와 requestLayout()함수 입니다.

 

 

invalidate는 무효화하다 라는 뜻입니다. 이미 그려져 있는 View를 무효화해서 시스템에게 이 View가 다시 redrawn 되어야 함을 알리고, requestLayout() 함수를 통해 UI를 다시 그려달라 요청하는 것 입니다!

이를 까먹어서 버그가 발생하면 디버깅 고생 많이 하겠죠?

 

이에 공식문서에서는 UI에 영향을 줄 수 있는 프로퍼티는 항상 값을 노출하도록(게터세터를 정의하도록) 하는것을 권장하고 있습니다.

 

 

 

 

지금까지 커스텀뷰의 기본적인 개념에 대해서 훑어보았습니다. 다음에는 Canvas 를 사용하여 커스텀 UI를 그려보겠습니다

반응형

댓글