페이지

2011년 6월 15일 수요일

CustomView 생성하기

안드로이드에서 기본적으로 지원하지 않는 UI 를 만들때 CustomView 를 사용합니다.
이러한 CustomView 의 기본적인 작성방법을 알아보도록 하겠습니다.

CustomView 는 “android.view.View” 클래스를 상속해서 만들어 집니다.
기본적으로 onDraw() 메소드만 재정의해서 xml 에 view 태그만 추가하면 오류없이 출력되는것을 볼 수 있습니다.

이번 포스트에서는 간단히 클릭하면 반응하는 CustomView 를 만들어 보도록 하겠습니다.
먼저 CustomView 소스를 확인해 보도록 하겠습니다.

- CustomView.java
package net.cranix.android.customviewtest;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;

public class CustomView extends View {
    
    private String text = null;
    private int backgroundColor = Color.RED;

    private String tempText;
    
    
    // 속성이 없는 생성자는 소스상에서 직접 생성할때만 쓰인다. 
    public CustomView(Context context) {
        super(context);
        Log.w(Constants.TAG,"CustomView("+context+")");
    }
    /*
     *  리소스 xml 파일에서 정의하면 이 생성자가 사용된다.
     *  
     *  대부분 this 를 이용해 3번째 생성자로 넘기고 모든 처리를 3번째 생성자에서 한다.
     */
    public CustomView(Context context,AttributeSet attrs) {
        this(context,attrs,0);
        Log.w(Constants.TAG,"CustomView("+context+","+attrs+")");
    }
    
    /*
     * xml 에서 넘어온 속성을 멤버변수로 셋팅하는 역할을 한다.
     */
    public CustomView(Context context,AttributeSet attrs,int defStyle) {
        super(context,attrs,defStyle);
        
        this.text = attrs.getAttributeValue(null,"text");
        
        Log.w(Constants.TAG,"CustomView("+context+","+attrs+","+defStyle+"),text:"+text);
    }
    
    /*
     * xml 로 부터 모든 뷰를 inflate 를 끝내고 실행된다.
     * 
     * 대부분 이 함수에서는 각종 변수 초기화가 이루어 진다.
     * 
     * super 메소드에서는 아무것도 하지않기때문에 쓰지 않는다.
     */
    @Override
    protected void onFinishInflate() {
        setClickable(true);
        Log.w(Constants.TAG,"onFinishInflate()");
    }
    
    /*
     * 넘어오는 파라메터는 부모뷰로부터 결정된 치수제한을 의미한다.
     * 또한 파라메터에는 bit 연산자를 사용해서 모드와 크기를 같이 담고있다.
     * 모드는 MeasureSpec.getMode(spec) 형태로 얻어오며 다음과 같은 3종류가 있다.
     *         MeasureSpec.AT_MOST : wrap_content (뷰 내부의 크기에 따라 크기가 달라짐)
     *         MeasureSpec.EXACTLY : fill_parent, match_parent (외부에서 이미 크기가 지정되었음)
     *      MeasureSpec.UNSPECIFIED : MODE 가 셋팅되지 않은 크기가 넘어올때 (대부분 이 경우는 없다)
     *  
     *   fill_parent, match_parent 를 사용하면 윗단에서 이미 크기가 계산되어 EXACTLY 로 넘어온다.
     *   이러한 크기는 MeasureSpec.getSize(spec) 으로 얻어낼 수 있다.
     *   
     *   이 메소드에서는 setMeasuredDimension(measuredWidth,measuredHeight) 를 호출해 주어야 하는데
     *   super.onMeasure() 에서는 기본으로 이를 기본으로 계산하는 함수를 포함하고 있다.
     *   
     *   만약 xml 에서 크기를 wrap_content 로 설정했다면 이 함수에서 크기를 계산해서 셋팅해 줘야한다.
     *   그렇지 않으면 무조껀 fill_parent 로 나오게 된다.
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        
        // height 진짜 크기 구하기
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = 0;
        switch(heightMode) {
        case MeasureSpec.UNSPECIFIED:    // mode 가 셋팅되지 않은 크기가 넘어올때
            heightSize = heightMeasureSpec;
            break;
        case MeasureSpec.AT_MOST:        // wrap_content (뷰 내부의 크기에 따라 크기가 달라짐)
            heightSize = 20;
            break;
        case MeasureSpec.EXACTLY:        // fill_parent, match_parent (외부에서 이미 크기가 지정되었음)
            heightSize = MeasureSpec.getSize(heightMeasureSpec);
            break;
        }
        
        // width 진짜 크기 구하기
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = 0;
        switch(widthMode) {
        case MeasureSpec.UNSPECIFIED:    // mode 가 셋팅되지 않은 크기가 넘어올때
            widthSize = widthMeasureSpec;
            break;
        case MeasureSpec.AT_MOST:        // wrap_content (뷰 내부의 크기에 따라 크기가 달라짐)
            widthSize = 100;
            break;
        case MeasureSpec.EXACTLY:        // fill_parent, match_parent (외부에서 이미 크기가 지정되었음)
            widthSize = MeasureSpec.getSize(widthMeasureSpec);
            break;
        }

        
        Log.w(Constants.TAG,"onMeasure("+widthMeasureSpec+","+heightMeasureSpec+")");
        
        setMeasuredDimension(widthSize, heightSize);
    }
    
    
    /*
     *  onMeasure() 메소드에서 결정된 width 와 height 을 가지고 어플리케이션 전체 화면에서 현재 뷰가 그려지는 bound 를 돌려준다.
     *  
     *  이 메소드에서는 일반적으로 이 뷰에 딸린 children 들을 위치시키고 크기를 조정하는 작업을 한다.
     *  유의할점은 넘어오는 파라메터가 어플리케이션 전체를 기준으로 위치를 돌려준다.
     *  
     *  super 메소드에서는 아무것도 하지않기때문에 쓰지 않는다.
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right,
            int bottom) {
        Log.w(Constants.TAG,"onLayout("+changed+","+left+","+top+","+right+","+bottom+")");
    }
    
    
    /*
     * 이 뷰의 크기가 변경되었을때 호출된다.
     * 
     * super 메소드에서는 아무것도 하지않기때문에 쓰지 않는다.
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        
        Log.w(Constants.TAG,"onSizeChanged("+w+","+h+","+oldw+","+oldh+")");
    }
    
    
    /*
     * 실제로 화면에 그리는 영역으로 View 를 상속하고 이 메소드만 구현해도 제대로 보여지게 된다.
     * 
     * 그릴 위치는 0,0 으로 시작해서 getMeasuredWidth(), getMeasuredHeight() 까지 그리면 된다.
     * 
     * super 메소드에서는 아무것도 하지않기때문에 쓰지 않는다.
     */
    @Override
    protected void onDraw(Canvas canvas) {
        final Paint p = new Paint();
        p.setColor(backgroundColor);
        canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredHeight(), p);
        if (text != null) {
            p.setColor(Color.BLACK);
            canvas.drawText(text, 10, 15, p); // 왼쪽 아래를 0,0 으로 보고있음
        }
        Log.w(Constants.TAG,"onDraw("+canvas+")");
    }
    
    
    /*
     * 현재 view 가 focus 상태일때 key 를 누르면 이 메소드가 호출됨.
     * 즉 이 메소드를 사용하려면 setFocusable(true) 여야함. 
     * 
     * 그리고 super 메소드에서는 기본적인 키 작업(예를들면 BACK 키 누르면 종료)을 처리하기 때문에 일반적으로 return 시에 호출하는게 좋다.
     * 만약 기본적인 작업을 하지않게 하려면 super 함수를 호출하지 않아도 된다.
     * 
     * 다른 event 메소드들도 유사하게 동작한다.
     */
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        Log.w(Constants.TAG,"onKeyDown("+keyCode+","+event+")");
        return super.onKeyDown(keyCode, event); 
    }
    
    /*
     * 이 view 에 touch 가 일어날때 실행됨.
     * 
     * 기본적으로 touch up 이벤트가 일어날때만 잡아내며 
     * setClickable(true) 로 셋팅하면 up,move,down 모두 잡아냄
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.w(Constants.TAG,"onTouchEvent("+event+")");
        switch(event.getAction()) {
        case MotionEvent.ACTION_UP:
            backgroundColor = Color.RED;
            text = tempText;
            break;
        case MotionEvent.ACTION_DOWN:
            backgroundColor = Color.YELLOW;
            tempText = text;
            text = "Clicked!";
            break;
        case MotionEvent.ACTION_MOVE:
            backgroundColor = Color.BLUE;
            text = "Moved!";
            break;
        }
        invalidate();
        return super.onTouchEvent(event);
    }
    
    
    public String getText() {
        return text;
    }
    public void setText(String text) {
        this.text = text;
    }
}
- 크기 계산하기
여기서 중요한 메소드는 onMeasure() 메소드 입니다.
이 메소드는 뷰의 전체 크기를 정하는 메소드 인데 안드로이드의 크기 정하는 방법에 따라 구현법이 달라져야 합니다.

안드로이드 레이아웃 xml 파일에서 크기를 지정하는 방법은 4가지가 있습니다.
   - fill_parent (상위 View 의 크기에 따름)
   - match_parent (상위 View 의 크기에 따름)
   - fixed (100px 와 같이 픽셀로 박아놨을때)
   - wrap_content (현재 뷰의 내용에 따름)

이렇게 4가지 방법의 특성에 따라서 넘어오는 크기의 종류는 3가지로 구분됩니다.
   - MeasureSpec.EXACTLY : fill_parent, match_parent, fixed 와 같이 상위에서 이미 결정되어버린 크기가 넘어올때 선택됩니다.
   - MeasureSpec.AT_MOST : wrap_content 를 선택했을때 선택됩니다.
   - MeasureSpec.UNSPECIFIED : xml 에 의하지 않고 소스상에서 직접 넣었을 때 나옵니다.

여기서 EXACTLY 과 UNSPECIFIED 는 외부에서 크기가 구해져서 내려오는 것이기 때문에 따로 계산할 것이 없으나 AT_MOST 는 내부적으로 크기계산을 해 주어야 합니다.
위의 소스에서는 간단하게 100,20 으로 박아놨지만 실제로 CustomView 를 구현하게 된다면 뷰의 특성에 따라 구현이 달라져야 할 것입니다.


- xml 에서 파라메터 받아내기
안드로이드 리소스 xml 에서 파라메터를 받아내려면 위 소스의 3번째 생성자에 있는것 처럼 아래와 같은 구문을 써야 합니다.

this.text = attrs.getAttributeValue(null,"text");


- xml 파일 구성하기
이렇게 만든 CustomView 를 xml 파일에서 사용하려면 아래와같은 xml 구성이 필요합니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <view class="net.cranix.android.customviewtest.CustomView"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        text="test"
    /></LinearLayout>

- 실행해 보기
이렇게 구성된 뷰를 Activity 에 넣고 실행해 보면 아래와 같은 화면이 나옵니다.
마우스를 클릭,이동 할때마다 색깔이 변경되는것을 볼 수 있습니다.


image

image

image

댓글 없음:

댓글 쓰기