커스텀 뷰 만들기

커스텀뷰는 왜 필요할까?

안드로이드 프레임워크에서 기본 제공되는 위젯들로는 Button, TextView, EditText, ListView, CheckBox, RadioButton, Spinner 등이 있고 레이아웃으로는 LinearLayout, FrameLayout, RelativeLayout 등이 있습니다.

하지만 실제로 앱을 만들다 보면 기획자, 디자이너, 사용자(클라이언트)의 요구사항에 맞는 기능을 위의 열거한 뷰들로 만들기 어렵거나 불가능한 경우가 부지기수입니다. 

예를들면, 원형 썸네일을 표현하기 위한 뷰를 만들기 위해서 ImageView를 상속할 수있고, 텍스트의 일부 내용만 보여주고 원할 때 펼쳐 모든 내용을 보여줄 수 있는 뷰를 만들기 위해서는 TextView를 상속할 수도 있습니다.

이럴때는 요구사항에 맞는 View를 직접 만드는 방법밖에 없습니다.

커스텀뷰 만드는 기본적인 원리

  1. 기존에 존재하는 View 클래스를 상속합니다.
  2. onDraw(), onMeasure(), onKeyDown()과 같이 시작하는 키워드가 ‘on’인 수퍼 클래스 메서드를 오버라이드 합니다.
  3. 새로만든 커스텀 뷰를 사용합니다. 기존에 사용하던 방식과 같이 xml레이아웃 등에 사용합니다

onDraw()와  onMeasure()

onDraw() 에서는 개발자가 원하는대로 구현할 수 있는 Canvas를 제공합니다. onDraw()를 오버라이드 하고,  Canvas를 이용하여 그리고 싶은 내용을 화면에 그리면 됩니다.

Note: 3D 그래픽에는 적용되지 않으며 View대신 SurfaceView를 상속하여 별도의 쓰레드에서 그려야합니다. OpenGL ES관련 포스팅을 참고해주세요

onMeasure()는 조금 더 복잡한데요 , 뷰와 뷰에 포함된 컨텐츠를 측정하여서 측정된 width와 height를 결정합니다. onMeasure()는 measure(int,int)에 의해 호출이 됩니다. measure 메소드에서는 뷰의 사이즈를 측정하고 실제 측정된 사이즈가 수행되는 곳은 onMeasure()입니다. 

onMeasure()를 오버라이드 하는 경우 setMeasuredDimension(int, int)를 호출해서 측정된 사이즈를 저장할 수 있도록 해야 합니다. super.onMeasure()를 호출 하는게 방법이 될수 있습니다.(onMeasure내에서 이미 한번 호출 하고 있기 때문에..)

프레임워크가 호출하는 다른 View 메소드

항목 메소드 설명
생성 생성자 뷰가 런타임에 코드로 부터 생성되는 경우와, xml 파일을 파싱하는 경우 두가지가 있습니다.
onFinishInflate() 뷰, 또는 뷰그룹의 경우 자식뷰들이 인플레이션이 다 끝나면 호출 됩니다.
레이아웃 onMeasure(int, int) 뷰, 자식뷰의 사이즈가 결정될때 호출됨
onLayout(boolean, int,int, int, int) 뷰, 자식뷰의 위치가 결정될때 호출됨
onSizeChanged(int, int,int, int) 뷰의 사이즈가 변경되었을때 호출됨
그리기 onDraw(Canvas) 뷰가 가지고 있는 콘텐츠를 그릴때 호출됨
이벤트 처리 onKeyDown(int, KeyEvent) 키 이벤트가 눌렸을 때 호출됨
onKeyUp(int, KeyEvent) 키가 떨어졌을 때 호출됨
onTrackballEvent(MotionEvent) 트랙볼 이벤트 발생시 호출됨
onTouchEvent(MotionEvent) 스크린에 터치 이벤트 발생시 호출됨
포커스 onFocusChanged(boolean,int, Rect) 포커스를 잃을 때 호출됨 
onWindowFocusChanged(boolean) 윈도우가 포함하고 있는 뷰가 포커스를 얻거나 잃을때 호출됨
Attaching onAttachedToWindow() 윈도우에 뷰가 붙을때 호출됨
onDetachedFromWindow() 윈도우로부터 뷰가 떨어질 때 호출 됨
onWindowVisibilityChanged(int) 윈도우가 가지고 있는 뷰의 가시성이 변경 될 때 호출 됨

이미 존재하는 View를 커스텀 해보자(LinedEditText;밑줄있는 EditText만들기)

public class LinedEditText extends EditText {
        private Rect mRect;
        private Paint mPaint;
        // 이 생성자는 LayoutInflter에서 사용됩니다.
        public LinedEditText(Context context, AttributeSet attrs) {
            super(context, attrs);
            // Rect와 Paint객체 생성
            // Paint객체에 스타일과 색상 입힘
            mRect = new Rect();
            mPaint = new Paint();
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setColor(0x800000FF);
        }

        @Override
        protected void onDraw(Canvas canvas) {
            // View안의 텍스트의 라인수가 몇개인지 가져옵니다.
            int count = getLineCount();
            Rect r = mRect;
            Paint paint = mPaint;
            /*
             * EditText의 모든 라인에 밑줄을 그립니다
             */
            for (int i = 0; i < count; i++) {
                // 현재 텍스트 라인의 베이스라인 좌표를 가져옵니다.
                int baseline = getLineBounds(i, r);
                /*
                 * Paint객체를 이용하여 배경에 밑줄을 그립니다
                 */
                canvas.drawLine(r.left, baseline + 1, r.right, baseline + 1, paint);
            }
            // 수퍼 메소드를 호출하는것으로 마무리 짓습니다.
            super.onDraw(canvas);
        }
    }

커스텀 뷰 클래스 만들기

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

안드로이드 스튜디오에서 레이아웃 편집기(layout editor)와 상호작용 할 수 있게 하려면 최소한 Context와 AttributeSet 객체를 매개변수로 취하는 생성자를 만들어야합니다. 이 생성자를 사용하면 레이아웃 편집기에서 뷰의 인스턴스를 만들고 편집 할 수 있습니다.

커스텀 속성 선언(Define Custom Attributes)

xml 요소를 통해 View를 제어하기 위해 res/values/attrs.xml 파일에 <declare-styleable> 요소를 추가합니다.

<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>

커스텀 속성인 showText 및 labelPosition을 선언한 모습입니다.

사용자 정의 속성을 정의한 후에는 빌트인 속성처럼 레이아웃 XML 파일에서 사용할 수 있습니다. 유일한 차이점은 사용자 지정 특성이 다른 네임스페이스에 속한다는것입니다.  http://schemas.android.com/apk/res/android 네임 스페이스에 속하는 대신 http://schemas.android.com/apk/res/ [패키지 이름]에 속합니다. 예를 들면 아래와 같습니다.

<?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>

긴 네임 스페이스 URI를 반복하지 않아도되도록 위의 샘플에서는 xmlns 지시문을 사용합니다. 이 지시문은 별칭 custom을 http://schemas.android.com/apk/res/com.example.customviews 네임 스페이스에 할당합니다. custom 대신 원하는 별칭을 선택할 수 있습니다.

커스텀뷰 클래스가 내부 클래스인 경우 뷰의 외부 클래스 이름으로 추가로 정규화해야합니다. 더욱이. 예를 들어 PieChart 클래스에는 PieView라는 내부 클래스가 있습니다. 이 클래스의 사용자 정의 속성을 사용하려면 com.example.customviews.charting.PieChart $ PieView 태그를 사용합니다.

커스텀 속성을 적용해보자

뷰가 XML 레이아웃으로부터 작성되었을 때, XML 태그의 모든 속성은 리소스 번들로부터 읽어져 뷰의 생성자에 AttributeSet로서 건네받습니다. AttributeSet에서 직접 값을 읽을 수도 있지만 이렇게하면 몇 가지 단점이 있습니다.

  • 속성값의 리소스참조가 안됩니다.
  • Style이 적용되지 않습니다.

대신에 AttributeSet을 obtainStyledAttributes()에게 넘깁니다. 이 메소드는 TypedArray를 돌려줍니다.

안드로이드 리소스 컴파일러는 obtainStyledAttributes()를 더 쉽게 호출 할 수 있도록 많은 작업을 수행합니다. res 디렉토리의 각 <declare-styleable> 리소스에 대해 생성 된 R.java는 속성 ID 배열과 배열의 각 속성에 대한 인덱스를 정의하는 상수 세트를 모두 정의합니다. 미리 정의 된 상수를 사용하여 TypedArray에서 특성을 읽습니다. PieChart 클래스가 그 속성을 읽는 방법은 다음과 같습니다.

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);
       mTextPos = a.getInteger(R.styleable.PieChart_labelPosition, 0);
   } finally {
       a.recycle();
   }
}

주의:TypedArray 객체는 공유 리소스이기 때문에 반드시 사용후에 recycle() 해야합니다.

속성 및 이벤트 추가하기

속성은 뷰의 동작 및 모양을 제어하는 강력한 방법이지만 View가 초기화 될때만 읽을 수 있습니다. 동적으로 이를 제어하려면 각 사용자 정의 속성에 대한 getter와 setter를 쌍으로 제공해야합니다. 다음 스니펫은 PieChart가 showText라는 속성을 노출하는 방법을 보여줍니다.

public boolean isShowText() {
   return mShowText;
}

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

setShowText는 글로벌변수 mShowText에 값을 대입한 뒤  invalidate () 및 requestLayout ()을 호출합니다. 이러한 호출은 View가 안정적으로 작동하도록하는 데 중요합니다. 모양을 변경할 수있는 속성을 변경 한 후에 뷰를 무효화해야 시스템에서 다시 그려야 함을 알 수 있습니다. 마찬가지로 View의 크기 나 모양에 영향을 줄 수있는 속성이 변경된 경우 새 레이아웃을 요청해야합니다. 이러한 메소드 호출을 잊어 버리면 찾기 힘든 버그가 발생할 수 있습니다.

카테고리: 미분류

1개의 댓글

성빈 · 2023년 6월 27일 6:03 오후

정리 감사합니다!

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.