출처 : http://underclub.tistory.com/333

안드로이드의 커스텀 컴포넌트에 관한 글입니다.



커스텀 확장 뷰 생성하기

안드로이드의 기초 레이아웃인 뷰와 뷰그룹에서는 컴포넌트화 된 모델을 제공해 줍니다. UI 를 디자인하는데 필요한 미리 정의된 뷰와 뷰 그룹의 하위 클래스 ( 위젯과 레이아웃 ) 를 가지고 있습니다.

이런것들 중에서 새로운 것을 만들기 위해 자신만의 뷰 클래스를 만들수도 있고, 기존의 위젯-레이아웃을 조금만 수정하고 싶다면 상속받은 뒤 메소드를 오버라이드 하는 방식을 사용합니다.

순서
1. 사용할 클래스에서 기존의 뷰 또는 뷰 하위클래스를 상속 받습니다.

2. 슈퍼클래스의 메소드를 오버라이드 합니다. ( 오버라이드할 슈퍼클래스의 메소드들은 on....() 으로 시작됩니다. )

3. 새로운 클래스를 기존의 뷰클래스를 대신하여 사용합니다.
[ 상속받은 클래스는 액티비티 내부에서 이너클래스(Inner Class) 로 정의할 수 있는데요, 이것은 그 액티비티 내부에서만 쓰일 수 있으므로, 어플리케이션 내에서 더 넓게 사용할 수 있는 public 뷰를 만드는것이 더 유리합니다 ]


완전한 커스텀 컴포넌트

완전한 커스텀 컴포넌트란, 만들고자 하는 어떠한 디자인으로도 컴포넌트를 만들 수 있다는 것을 의미합니다. 예를들어, " A " 라는 디자인을 하고싶은데, 이것이 내장된 컴포넌트를 어떻게 조합하더라도 만들 수 없는 경우가 있을 수도 있습니다.

즉, 원하는 방식으로 보여지고 동작하는 컴포넌트를 만들 수 있다는 얘기인데, 구글에서는 상상력과 스크린의 크기, 사용가능한 프로세스 파워만이 제약조건이 될 수 있다고 말하고 있네요 ㅎㅎ 재미있군요 ㅎ

커스텀 컴포넌트의 생성하는 순서
1. 뷰 ( View ) 클래스를 상속합니다.

2. XML 내에 속성과 파라미터를 가지는 생성자를 만들수 있고, 그것을 사용할 수 있습니다.

3. 이벤트 리스너, 속성, 접근자 및 수정자 등을 추가하여 동작을 결정합니다.

4. onMeasure() 메소드와 onDraw() 메소드를 오버라이드하여 구현을 해야합니다. 이때 둘다 디폴트 동작구조는 onDraw() 는 아무런 일도 하지않고, onMeasure() 는 100X100 의 크기로 설정되기 때문에 알맞게 수정해야 합니다.

5. 추가적으로 필요한 메소드들을 추가합니다.


onDraw() 와 onMeasure() 구현하기

onDraw() 는 캔버스(Canvas) 를 파라미터로 전달하며, 그 캔버스위에 원하는 것. 즉, 2D 그래픽이나 기타 다른것들을 구현합니다
이것은 3D 그래픽에는 적용되지 않는데, 3D 그래픽을 쓰려면 서피스뷰(SurfaceView) 를 상속해야 하며, 별도의 스레드에서 그리기를 해야되기 때문입니다.



onMeasure() 는 정확한 값을 가지고 오버라이드 해야되는데요, 부모(parent) 의 요구조건과 측정된 너비와 높이를 가지고 setMeasuredDimension() 메소드를 호출해야만 합니다. 만약 이 메소드를 호출하지 않으면 측정 시점에서 예외가 발생합니다.





onMeasure() 메소드 구현 순서
1. onMeasure() 메소드에 너비와 높이를 파라미터로 전달합니다. ( widthMeasureSpec, heightMeasureSpec 은 정수형 값 입니다. )

2. onMeasure() 메소드에서 요구하는 너비와 높이를 계산합니다. 즉, 요구하는 값이 부모의 값보다 크다면 부모는 잘라내기, 스크롤, 예외발생, 재시도( onMeasure() 다시호출) 등을 선택할 것입니다.

3. 계산되어진 너비와 높이를 가지고 setMeasuredDimension(int width, int height) 를 호출합니다. 위에서 언급했지만 호출하지 않으면 예외가 발생합니다.


▶ 그 밖의 뷰에서 프레임워크가 호출하는 다른 메소드들..
⊙ 생성자 : 소스코드에서 뷰가 생성될 때 호출되는 형태와, 레이아웃 파일에서 뷰가 전개(inflate) 될 때 호출되는 형태 두가지가 있습니다. 두번째 형태는 레이아웃에 정의된 속성을 분석한 후 적용합니다.

onFinishInflate()뷰와 그 하위 자식들이 XML 에서 전개 된 후 호출됩니다.

onMeasure(int, int) : 뷰와 그 자식들에 대한 크기를 결정하기 위해 호출됩니다.

onLayout(boolean, int, int, int, int) : 뷰가 그 자식들에게 크기와 위치를 할당할 때 호출됩니다.

onSizeChanged(int, int, int, int) : 뷰의 크기가 바뀔 때 호출됩니다.

onDraw(Canvas) : 뷰가 컨텐츠를 그릴 때 호출되지요

onKeyDown(int, KeyEvent) : 새로운 키 이벤트 발생시 호출됩니다.

onKeyUp(int, KeyEvent) : 키 업 이벤트 발생시에 호출됩니다.

onTrackballEvent(MotionEvent) : 트랙볼 모션 이벤트 발생시에 호출됩니다.

onTouchEvnet(MotionEvent) : 터치스크린의 모션 이벤트 발생시에 호출됩니다.

onFocusChanged(boolean, int, Rect) : 뷰가 포커스를 가지거나 잃을 때 호출됩니다.

onWindowFocusChanged(boolean) : 뷰를 포함한 윈도우가 포커스를 가지거나 잃을 때 호출됩니다.

onAttachedToWindow() : 뷰가 윈도우에 포함될 때 호출됩니다.

onDetachedFromWindow() : 뷰가 윈도우에서 분리될 때 호출됩니다.

onWindowVisibillityChanged(int) : 뷰를 포함한 윈도우가 보여지는 상태가 변할 때 호출됩니다.


커스텀 뷰 예제

커스텀 뷰 예제는 안드로이드에서 제공하는 샘플들 중에서 API Demos 의 CustomView 예제가 있는데, 이 예제의 컴스텀 뷰는 LabelView 클래스에서 정의되어 있습니다.



LabelView 샘플은 커스텀 컴포넌트를 이해하기에 아주 적합합니다.
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.android.apis.view;

// Need the following import to get access to the app resources, since this
// class is in a sub-package.
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

import com.example.android.apis.R;


/**
* Example of how to write a custom subclass of View. LabelView
* is used to draw simple text views. Note that it does not handle
* styled text or right-to-left writing systems.
*
*/
public class LabelView extends View {
    private Paint mTextPaint;
    private String mText;
    private int mAscent;
    
    /**
* Constructor. This version is only needed if you will be instantiating
* the object manually (not from a layout XML file).
* @param context
*/
    public LabelView(Context context) {
        super(context);
        initLabelView();
    }

    /**
* Construct object, initializing with any attributes we understand from a
* layout file. These attributes are defined in
* SDK/assets/res/any/classes.xml.
*
* @see android.view.View#View(android.content.Context, android.util.AttributeSet)
*/
    public LabelView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initLabelView();

        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.LabelView);

        CharSequence s = a.getString(R.styleable.LabelView_text);
        if (s != null) {
            setText(s.toString());
        }

        // Retrieve the color(s) to be used for this view and apply them.
        // Note, if you only care about supporting a single color, that you
        // can instead call a.getColor() and pass that to setTextColor().
        setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF000000));

        int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0);
        if (textSize > 0) {
            setTextSize(textSize);
        }

        a.recycle();
    }

    private final void initLabelView() {
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setTextSize(16);
        mTextPaint.setColor(0xFF000000);
        setPadding(3, 3, 3, 3);
    }

    /**
* Sets the text to display in this label
* @param text The text to display. This will be drawn as one line.
*/
    public void setText(String text) {
        mText = text;
        requestLayout();
        invalidate();
    }

    /**
* Sets the text size for this label
* @param size Font size
*/
    public void setTextSize(int size) {
        mTextPaint.setTextSize(size);
        requestLayout();
        invalidate();
    }

    /**
* Sets the text color for this label.
* @param color ARGB value for the text
*/
    public void setTextColor(int color) {
        mTextPaint.setColor(color);
        invalidate();
    }

    /**
* @see android.view.View#measure(int, int)
*/
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec),
                measureHeight(heightMeasureSpec));
    }

    /**
* Determines the width of this view
* @param measureSpec A measureSpec packed into an int
* @return The width of the view, honoring constraints from measureSpec
*/
    private int measureWidth(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text
            result = (int) mTextPaint.measureText(mText) + getPaddingLeft()
                    + getPaddingRight();
            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by measureSpec
                result = Math.min(result, specSize);
            }
        }

        return result;
    }

    /**
* Determines the height of this view
* @param measureSpec A measureSpec packed into an int
* @return The height of the view, honoring constraints from measureSpec
*/
    private int measureHeight(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        mAscent = (int) mTextPaint.ascent();
        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text (beware: ascent is a negative number)
            result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop()
                    + getPaddingBottom();
            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by measureSpec
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    /**
* Render the text
*
* @see android.view.View#onDraw(android.graphics.Canvas)
*/
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent, mTextPaint);
    }
}


뜯어보자면...

1. 완전한 커스텀 컴포넌트를 위해 뷰 클래스를 상속 받았습니다.

2. 생성자는 inflation 파라미터를 가지고 있습니다 ( XML 에서 정의됨 )
또한 LabelView 를 위해 정의된 속성들을 가지고 있지요.

3. 라벨에 표시할 public 메소드를 사용하고 있습니다
( setText(), setTextColor() 등 )

4. 그리기를 수행할 때 필요한 크기를 설정하기 위하여 onMeasure() 메소드가 있습니다.
이 예제의 실제 작업처리는 private measureWidth() 메소드에서 수행하도록 되어 있네요

5. 캔버스에 라벨을 그리기 위해 onDraw() 메소드를 오버라이드 했습니다. 추가로 custom_view_1.xml 에 보면 android: 네임스페이스 와 app: 네임스페이스가 있는데요, app: 네임스페이스는 LabelView 가 작업하는 커스텀 파라미터이며, R 클래스의 styleable 내부클래스에서 정의됩니다.


합성 ( Compound ) 컴포넌트

완전한 커스텀 컴포넌트를 만들기 보다는, 기존의 컨트롤들을 조합하여 구성하는 컴포넌트를 만들려면 합성 컴포넌트를 사용하면 됩니다.

이것은 하나로 취급될 수 있는 아이템들을 그룹안에서 합쳐놓는 방식이며, 안드로이드에는 이런것을 위해 제공되는 두 개의 뷰가 존재합니다.
바로 SpinnerAutoCompleteTextView 가 그것이지요.

합성 컴포넌트 만들기
1. 일반적인 시작점은 레이아웃이 됩니다. 따라서 하나의 레이아웃을 확장하는 클래스를 만들고, 컴포넌트가 구조화 될 수 있도록 다른 레이아웃을 포함합니다. 컴포넌트에 포함된 레이아웃은 XML 에서 선언하거나 소스코드에서 추가할 수 있습니다.

2. 새로운 클래스의 생성자에서 슈퍼클래스의 생성자를 먼저 호출해 줍니다. 그 다음에 새로운 컴포넌트 안에서 다른 뷰를 구성합니다. 이것 또한 XML 로 선언할 수 있으며, 속성과 파라미터를 만들어 사용할 수도 있습니다.

3. 알맞은 이벤트 리스너를 구현해 줍니다.

4. 레이아웃을 상속하는 경우에는 onDraw() 나 onMeasure() 메소드를 오버라이드 하지 않아도 됩니다. 왜냐하면 레이아웃은 디폴트 동작 구조가 있기 때문입니다. 만약 필요하다면 오버라이드 해도 무방합니다.

5. onKeyDown() 메소드 같은 다른 on....() 메소드를 오버라이드 할 수 있습니다.


▶ 커스텀 컴포넌트를 레이아웃으로 사용하면서의 장점은 무엇일까요?
1. XML 로 레이아웃을 만들 수 있고, 소스코드에서 동적으로 만들어서 추가할 수도 있습니다.

2. onDraw() 와 onMeasure() 메소드를 오버라이드 하지 않아도 됩니다.

3. 복잡한 뷰를 생성할 수도 있고, 그것들이 하나의 컴포넌트처럼 재사용 할 수도 있습니다.

합성 컴포넌트 예제

합성 컴포넌트도 마찬가지로 안드로이드 샘플의 API Demos 의 Views/Lists 안에 있는 Example4 와 Example6 클래스가 해당되는 예제입니다.

이것들은 SpeechView 기능을 합니다. SpeechView 란 말 그대로 연설문 보여주기 같은 기능입니다.

자바 클래스는 List4.java 와 List6.java 클래스이고, 이 클래스는 합성 컴포넌트 생성을 위해 LinearLayout 을 상속했네요~







커스텀 컴포넌트에 관한 글이었습니다.
꽤 긴글이 되어 버렸네요...;;

다음 포스팅엔 커스텀 컴포넌트에 이어지는 글인 이미 존재하는 뷰를 수정해서 커스텀 뷰를 만드는 것을 살펴보죠

+ Recent posts