[ 앱 개발자 도전기 : 안드로이드 ] 안드로이드 : View의 생명주기.

0

App_Dev : Android.

목록 보기
8/11
post-thumbnail

[ 앱 개발자 도전기 : 안드로이드 ] 안드로이드 : View의 생명주기.

∇ 안드로이드 : View의 생명주기.

목  차

1. View란?
2. View의 생명주기.
3. 순회
4. 상태의 저장과 복구

Ⅰ. View란??

"안드로이드 앱"에서 'View'는 UI를 구성하는 가장 기본적인 단위 클래스입니다.

"View"는 화면 상에서 사각형 영역을 가지며 {그리기 및 이벤트 처리} 등을 처리할 수 있습니다.

또한, 'View'는 {버튼이나 텍스트 필드}와 같은 유저-인터렉션 요소들의 집합인 "Widget"의 가장 기초 클래스입니다.

"View"의 서브 클래스인 "ViewGroup"은 레이아웃들의 기초 클래스이며,
"View"들(또는 다른 ViewGroup)을 담는 보이지 않는 컨테이너 역할을 합니다.

'안드로이드'의 "Activity"나 "Fragment"에 생명주기가 존재하는 것처럼
'View"에도 역시나 생명주기가 존재합니다.

Ⅱ. View의 생명주기

모든 액티비티는 생명주기를 가지는데, View도 생명주기를 가지고 있습니다.

화면에 렌더링 된 View는 아래 그림과 같은 생명주기 메서드를 거쳐서 화면에 그려집니다.

위 생명주기의 각 단계들은 모두 중요한 역할을 수행합니다.

1. 생성자 ( Constructors )

'커스텀 뷰(Custom Context)를 만들 때 생성자를 어떤 걸 써야하는지 난감할 때가 많습니다.

'View'의 생성자가 여러 종류가 있는데다가 각기 어떤 차이점이 있는지 명확히 모르고 쓰기 때문입니다.

확실하게 정의를 해봅시다.

1-1. View(Context Context)

"View"를 동적으로 만들 때 사용하는 간단한 생성자입니다.
여기서 매개변수 'context'는 'view'가 실행될 때 현재 테마, 리소스 등을 구성하는데 사용됩니다.

1-2. View(Context Context, @Nullable AttributeSet attrs)

'XML에서 'View'를 전개(Inflate)할 때 호출되는 생성자'로,
XML 파일에서 지정된 속성을 제공하여 XML 파일에서 View를 구성 할 때 호출됩니다.

이 생성자는 기본 스타일을 적용하므로 'Context'의 테마 및 지정된 'AttributeSet'의 속성 값만 적용됩니다.

1-3. View(Context Context, @Nullable AttributeSet attrs, int defStyleAttr)

XML을 통해 전개(Inflate)를 하고 '테마 속성에서 클래스별 기본 스타일을 적용'합니다.

이 생성자는 서브 클래스가 전개(Inflate)할 때 자체 기본 스타일을 사용할 수 있도록 합니다.

ex)
'Button클래스의 생성자는 수퍼 클래스 생성자를 호출하고,
'defStlyeAttr' 에 R.attr.buttonStyle 을 제공합니다.

이를 통해 테마의 버튼 스타일은 모든 기본 View 속성(특히 배경!) 과 Button 클래스의 속성을 수정할 수 있습니다.

1-4. View(Context Context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)

XML을 전개하고 '테마 속성 또는 Style 리소스에서 클래스 별 기본 스타일을 적용' 합니다.

이 생성자는 서브 클래스가 전개(Inflate)할 때 자체 기본 스타일을 사용가능하도록 합니다.

매개변수 'defStyleRes'는 'View'의 'defStyleAttr'가 0이거나 테마에서 찾을 수 없는 경우에만 기본값을 제공하는 Style 리소스 ID입니다.

기본값을 찾지 않으려면 0으로 지정합니다.

2. Attachment/ Detachmente

:: "VIew"가 Window에서 연결되거나 분리 될 때의 단계입니다.

이 단계에는 적절한 작업을 수행하기 위해서, 콜백을 받는 몇가지 방법이 있습니다.

onAttachedToWindow()

'View'가 Window에 연결되는 순간에 호출됩니다.
'View'가 화면에 표시되고, 실제로 그리기 작업이 가능한 상태가 됩니다.
따라서 리소스 할당을 시작하거나 리스너를 설정 가능합니다.

override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    // 리소스 초기화
    initializeResources()
    // 이벤트 리스너 설정
    setupEventListeners()
    // 애니메이션 시작
    startAnimations()
}

OnDetachedFromWindow()

'View'가 Window에서 분리되는 순간에 호출됩니다.

이 시점에서 더 이상 드로잉을 할 표면이 존재하지 않습니다.

예약 된 자원을 정리하거나, 모든 종류의 작업을 중지해야 하는 곳입니다.

이 메소드는 ViewGroup에서 View가 제거되거나, 액티비티가 종료(Destroyed) 될 때 호출됩니다.

override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    // 리소스 해제
    releaseResources()
    // 이벤트 리스너 제거
    removeEventListeners()
    // 실행 중인 작업 중단
    cancelOngoingTasks()
    // 애니메이션 정지
    stopAnimations()
}

OnFinishInflate()

XML 레이아웃이 메모리에 완전히 로드된 후 호출됩니다.
모든 하위 View들이 생성되고 추가된 이후에 실행됩니다.

레이아웃의 경우 모든 Child View가 추가 된 후에 호출됩니다.

override fun onFinishInflate() {
    super.onFinishInflate()
    // View 참조 초기화
    initializeViewReferences()
    // 초기 상태 설정
    setupInitialState()
}

순서

  1. XML 레이아웃 인플레이션
  2. onFinishInflate()호출
  3. onAttachedToWindow()호출
  4. View 사용
  5. onDetachedFromWindow() 호출.

Ⅲ. 순회(Traversals)

안드로이드에서 'View의 "순회"는 '부모 노드(ViewGroup)에서 분기 한 리프 노드(Child Views)의 "트리 구조"를 순회하는 것을 말합니다.

이때는, '전위순회 방식'으로 순회합니다.

◎ 트리 구조.

  • ViewGroup(부모)에서 Child Views(자식)으로 이어지는 계층적 구조.
  • 부모 노드에서 시작하여서 모든 자식 노드를 방문하는 순회 방식
  • 각 순회 단계에서 레이아웃 제약조건과 그리기 작업을 수행합니다.

◎ 순회 특징.

◆순서
1. 부모(ViewGroup)에서 시작.
2. 깊이 우선 탐색(DFS)의 방식으로 진행
3. 모든 자식 노드 방문
4. 각 노드에서 필요한 작업 수행.

◆제약 조건 처리

  • 부모가 자식의 크기와 위치에 제약을 설정
  • 자식은 부모가 제공한 제약 내에서 자신의 크기 결정
  • 최종적으로 전체 View 계층의 레이아웃이 결정.

◆주의사항
1. 불필요한 순회 최소화
2. 복잡한 중첩 구조 피하기!
3. 효율적인 레잉아웃 구성 고려
4. 성능 최적화를 위한 캐싱 활용.

1. Measure Pass

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    // 각 자식 View의 크기 측정
    measureChildren(widthMeasureSpec, heightMeasureSpec)
}

◎ onMeasure()
:: onMeasure() 메소드는 'View'의 크기를 확인하기 위해서 호출됩니다.
'ViewGroup'의 경우 계속해서 각 Child View에 대한 측정을 하고, 그에 대한 결과로 자신의 사이즈를 결정합니다.

onMeasure(int widthMeasureSpec, int heightMeasureSpec)
// @param widthMeasureSpec 부모 View에 의해 적용된 수평 공간 요구사항
// @param heightMeasureSpec 부모 View에 의해 적용된 수직 공간 요구사항

'onMeasure()'는 값을 반환하지 않고, setMeasureDimension()을 호출하여 너비와 높이를 명시적으로 설정.

◎ MeasureSpec
'MeasureSpec'은 부모에서 자식으로 전달되는 레이아웃 요구 사항을 캡슐화합니다.

각 MeasureSpec은 너비 또는 높이에 대한 요구사항을 나타냅니다.

MeasureSpec은 크기와 모드로 구성되며, 총 3가지 모드가 있습니다.

ⓐ MeasureSpec.EXACTLY
: 부모가 자식의 정확한 크기를 결정합니다.
자식의 사이즈와 관계없이 주어진 경계 내에서 사이즈가 결정됩니다.

ⓑ MeasureSpec.AT_MOST
: 자식은 지정된 크기까지 원하는 만큼 커질 수 있습니다.

ⓒ MeasureSpec.UNSPECIFIED
: 부모가 자식에 제한을 두지 않기 때문에, 자식은 원하는 크기가 될 수 있습니다.

2. Layout Pass

override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
    super.onLayout(changed, left, top, right, bottom)
    // 각 자식 View의 위치 결정
    layoutChildren()
}

.
◎ onLayout()
'View'의 위치를 측정하여, 화면 상에 배치 한 후에 호출됩니다.

3. Draw Pass

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    // 각 자식 View를 순서대로 그리기
    drawChildren(canvas)
}

◎ onDraw()
크기와 위치등의 값은 이전 단계에서 계산되므로 View는 그것들을 기준으로 그려집니다.

'onDraw(Canvas canvas)' 메서드에서 생성된 캔버스 객체에는 GPU로 보낼 OpenGL-Es 명령목록(displayList)'이 있습니다.

onDraw()는 여러번 호출되므로 이곳에서 객체를 만들면 안됩니다.

특정 'View'의 속성이 변경되었을 때 실행되는 두 가지 메서드가 있습니다.

▼invalidate()
'invalidate()'는 변경 사항을 보여주고자 하는 특정 'View'에 대해 강제로 다시 그리기를 요구하는 메서드입니다.
View모양이 변경되면 invalidate()를 호출해야합니다.
.
.

▼ requestlayout()
어떤 시점에서 view의 경계가 변경되었다면 view를 다시 측정하기 위해서
'requestLayout()'을 호출하여 Measure 및 Layout 단계를 다시 거치게 됩니다.

view에서 메소드를 호출 할 때는 항상 UI 스레드내에서 수행해야 합니다.

다른 스레드에서 작업하고 있고, 해당 스레드에서 view 상태를 업데이트 하려고 하는 경우 핸들러 를 사용해줘야 합니다.

class CustomViewGroup : ViewGroup {
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // 1. 자식 View들의 크기 측정
        measureChildren(widthMeasureSpec, heightMeasureSpec)
        
        // 2. 자식 View들의 크기를 기반으로 ViewGroup의 크기 결정
        var totalWidth = 0
        var totalHeight = 0
        
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            totalWidth = max(totalWidth, child.measuredWidth)
            totalHeight += child.measuredHeight
        }
        
        // 3. 최종 크기 설정
        setMeasuredDimension(totalWidth, totalHeight)
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        var currentTop = 0
        
        // 각 자식 View의 위치 지정
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            child.layout(
                0,                          // left
                currentTop,                 // top
                child.measuredWidth,        // right
                currentTop + child.measuredHeight  // bottom
            )
            currentTop += child.measuredHeight
        }
    }
}

Ⅳ. 상태의 저장과 복구(State Save / Restore)

◎ onSqveInstanceState()

먼저 상태를 저장하려면 ID를 제공해야합니다.

View계층에 동일한 ID를 가진 여러 개의 뷰가 있는 경우에 고유한 ID를 지정하여 모든 상태를 저장할 수 있도록 해야 합니다.

둘재, View.BaseSavedState를 확장하여서 속성값을 지정하는 클래스가 필요합니다.

◎ onRestoreInstanceState(Parcelable state)

onRestoreInstanceState메서드를 재정의하고 Parcelable에서 데이터를 읽은 다음 Parcelable에서 사용 가능한 데이터를 기반으로 로직을 작성합니다.

@Override
public Parcelable onSaveInstanceState() {
    Bundle bundle = new Bundle();
    // The vars you want to save - in this instance a string and a boolean
    String someString = "something";
    boolean someBoolean = true;
    State state = new State(super.onSaveInstanceState(), someString, someBoolean);
    bundle.putParcelable(State.STATE, state);
    return bundle;
}

@Override
public void onRestoreInstanceState(Parcelable state) {
    if (state instanceof Bundle) {
        Bundle bundle = (Bundle) state;
        State customViewState = (State) bundle.getParcelable(State.STATE);
        // The vars you saved - do whatever you want with them
        String someString = customViewState.getText();
        boolean someBoolean = customViewState.isSomethingShowing());
        super.onRestoreInstanceState(customViewState.getSuperState());
        return;
    }
    // Stops a bug with the wrong state being passed to the super
    super.onRestoreInstanceState(BaseSavedState.EMPTY_STATE); 
}

protected static class State extends BaseSavedState {
    protected static final String STATE = "YourCustomView.STATE";

    private final String someText;
    private final boolean somethingShowing;

    public State(Parcelable superState, String someText, boolean somethingShowing) {
        super(superState);
        this.someText = someText;
        this.somethingShowing = somethingShowing;
    }

    public String getText(){
        return this.someText;
    }

    public boolean isSomethingShowing(){
        return this.somethingShowing;
    }
}

0개의 댓글

관련 채용 정보