View

맥모닝·2024년 1월 16일
0

Android

목록 보기
2/6

View

드로잉과 이벤트 처리를 담당하는 UI 구성요소의 기본 클래스로, View 클래스를 직접 상속하여 커스텀 뷰를 만들거나, View 클래스의 하위 클래스 중 하나를 상속하여 특정 기능을 하는 위젯(컴포넌트)를 만들 수 있다.

  • 사용자 인터페이스 구성 요소의 기본 구성 요소를 나타낸다.
    • 즉, 사용자 인터페이스를 구성하는 모든 구성 요소는 View의 인스턴스를 의미한다.
  • 화면의 직사각형 영역을 차지하며, 드로잉과 이벤트 처리를 담당한다.
  • 대화형 UI 구성 요소(button, text fields 등)를 만드는 데 사용되는 위젯의 기본 클래스이다.

View가 그려지는 과정

  • 액티비티가 포커스를 받게 되면, Android에게 View 계층의 루트 노드를 제공하여 레이아웃을 그리게 된다.

액티비티가 포커스를 받게되는 시점 : onCreate()

Android에게 View 계층의 루트 노드를 전달하는 방법 : setContentView()

  • 레이아웃은 루트 노드부터 리프 노드까지 트리를 따라 순서대로 그려지게 된다.
  • 부모가 먼저 그려지고 그 다음 자식들 순서대로 그려지는 형태이다. (Top-down 방식)
    • ViewGroup은 자식 뷰들의 draw()를 호출하여 화면에 지정된 형태로 자식 뷰들을 그려줄 것을 요청한다.

루트 노드 : 트리에서 부모가 없는 최상위 노드, 트리의 시작점

리프 노드 : 루트 노드를 제외한 차수가 1인 정점을 뜻한다. 쉽게 말해 자식이 없는 노드. 단말 노드라 부르기도 한다.


✅ ViewGroup(레이아웃)

View 클래스를 상속받아 여러 개의 자식 View나 다른 ViewGroup을 포함하고 배치하는 컨테이너 역할을 하는 클래스로, 자식 View들의 위치와 크기를 결정합니다.

  • ViewGroup의 하위 클래스는 다른 View나 ViewGroup을 보유하고, 해당 레이아웃 속성을 정의하는 보이지 않는 컨테이너인 레이아웃의 기본 클래스이다.

레이아웃을 그리는 과정

  • 레이아웃은 Measure과 Layout 두 단계의 프로세스를 거친다.

1. Measure 단계

  • measure(int, int)에서 구현되며, 뷰 트리의 하향식(Top-down) 순회이다.

measure(int, int) : 뷰가 얼마나 커야 하는지 알아보기 위해 호출되며, 뷰의 실제 측정 작업은 measure 메소드에 의해 호출되는 onMeasure(int, int)에서 수행된다.

하향식 순회 : 각 재귀 호출에서 먼저 노드를 방문하여 일부 값(너비와 높이에 대한 제약 조건 정보)을 생성하고, 함수를 재귀 호출할 때 이 값을 하위 노드에게 전달하는 것을 의미한다.

  • 각 뷰는 재귀 중에 치수 사양을 트리 아래(자식 뷰들에게)로 MeasureSpec과 LayoutParams 클래스를 이용하여 전달한다.

치수 사양 : 뷰의 크기와 위치를 결정하는데 필요한 정보로, 너비와 높이, 그리고 뷰의 위치를 결정하는 데 사용되는 여러 속성들로 구성된다.

  • Ex) 너비 = layout_width, 높이 : layout_height, 그외 : layout_margin, layout_padding 등

LayoutParams 클래스 : 자식 뷰가 너비와 높이 양면에서 얼마나 크게 보이길 원하는지를 부모 뷰에게 설명하는 클래스

  • 정확한 숫자값 : dp, sp 등
  • MATCH_PARENT : 부모 뷰 크기에 꽉 맞출 때
  • WRAP_CONTENT : 자신의 내용물 크기에 맞출 때

MeasureSpec 클래스 : 자식 뷰가 어떤 크기로 그려질 것인지 결정하는 데 사용되는 클래스

  • UNSPECIFIED : 부모 뷰가 자식 뷰에게 어떤 크기 제약도 주지 않는다.
    • 즉, 자식 뷰는 원하는 크기대로 지정할 수 있다.
  • EXACTLY : 부모가 자식 뷰에게 정확한 크기를 결정한다.
    • 즉, 자식 View의 사이즈와 관계없이 주어진 경계 내에서 사이즈가 결정된다.
  • AT_MOST : 자식 뷰는 지정된 크기까지 원하는 만큼 커질 수 있다.
  • onMeasure() 메서드에서 setMeasuredDimension()을 호출하여 측정 결과를 저장한다.
  • setMeasuredDimension() 메서드는 getMeasuredWidth()와 getMeasuredHeight() 메서드를 통해 접근할 수 있는 뷰의 측정된 너비와 높이(뷰가 차지하려고 요청하는 공간의 크기)를 설정한다.
    • 따라서 measure() 메서드가 반환된 후에는 getMeasuredWidth()와 getMeasuredHeight() 메서드를 통해 뷰의 측정된 크기를 가져올 수 있다.

2. Layout 단계

  • 두 번째 단계는 layout(int, int, int, int)에서 발생하며, 역시 하향식이다.
  • 이 단계 동안 각 부모는 Measure 단계에서 계산된 크기를 사용하여 모든 하위 항목의 위치를 지정해야 한다.
    • 즉, Measure 단계에서 모아놓은 크기의 수치값을 기준으로 전체적인 레이아웃을 그리는 과정이다.

생성자(Constructors)

  • View 클래스에는 4가지 유형의 생성자를 사용할 수 있다.
// 1
public View(Context context) { .. }
fun createDynamicView(context: Context) {
    val layout = LinearLayout(context)
    
    val newView = View(context)
    
    newView.setBackgroundColor(Color.RED)
    newView.layoutParams = LinearLayout.LayoutParams(200, 200)
    
    layout.addView(newView)
}
  • 코드에서 View를 동적으로 만들 때 사용할 수 있는 간단한 생성자이다.
  • 매개변수로 필요한 context는 View가 실행될 때 사용되는 context이고, 테마, 리소스 등에 액세스할 수 있다.
// 2
public View(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
}
// xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    
    <View
        android:id="@+id/myView"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:background="#FF0000" />
</LinearLayout>
        
        
// kotlin
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.sample_layout)
  
    val myView = findViewById<View>(R.id.myView)
    ...
}
  • XML에서 View를 inflate 할 때 호출되는 생성자로, XML 파일에서 지정된 속성을 제공해 XML 파일에서 View를 구성할 때 호출된다.
  • 기본 스타일인 0을 사용하기 때문에 context의 테마와 지정된 AttributeSet의 속성 값만 적용된다.
// 3
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
}
// kotlin
class CustomView : View {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, R.style.CustomViewDefaultStyle)
}


// styles.xml
<resources>
    <style name="CustomViewDefaultStyle" parent="android:Widget">
        <item name="android:background">#FF0000</item>
        <item name="android:layout_width">100dp</item>
        <item name="android:layout_height">100dp</item>
    </style>
</resources>

        
// themes.xml
<resources>
    <style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        <item name="customViewStyle">@style/CustomViewDefaultStyle</item>
    </style>
</resources>


// xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    
    <com.example.myapp.CustomView
        android:id="@+id/myCustomView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</LinearLayout>
  • XML에서 inflate를 수행하고, 테마 속성 또는 style 리소스에서 클래스 별 기본 스타일을 적용한다.
  • 하위 클래스가 inflate 될 때 자체적인 기본 스타일을 사용할 수 있도록 한다.
    • Ex) Button 클래스의 생성자는 super class의 생성자를 호출하고, defStyleAttr에 R.attr.buttonStyle을 제공한다. 이를 통해 테마의 Button 스타일은 모든 기본적인 View의 속성들과 Button 클래스의 속성을 수정할 수 있다.
  • defStyleAttr 매개변수는 View의 기본 값을 제공하는 Style 리소스에 대한 참조를 포함하는 현재 테마의 속성이다.
    • 기본 값을 찾지 않으려면 0으로 표시할 수 있다.
// 4
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { .. }
// xml
<resources>
    <style name="CustomViewStyle" parent="android:Widget">
        <item name="android:background">#FF0000</item>
    </style>
</resources>


// kotlin
class CustomView : View {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, R.style.CustomViewStyle)
}
  • XML에서 inflate를 수행하고, 테마 속성 또는 Style 리소스에서 클래스 별 기본 스타일을 적용한다.
  • 서브 클래스들이 inflate 할 때 자체적인 기본 스타일을 사용할 수 있도록 한다. 앞서 살펴본 생성자와 비슷하다.
  • defStyleRes 매개변수는 View의 defaStyleAttr이 0이거나 테마에서 찾을 수 없는 경우에만 기본값을 제공하는 Style 리소스 ID이다.
    • 기본 값을 찾지 않으려면 0으로 표시할 수 있다.

View Lifecycle

View의 생명주기는 아래와 같이 크게 세가지로 구성된다.

  • Attachment / Detachment
  • Traversals
  • State Save / Restore

1. Attachment / Detachment

  • View가 window에 부착되거나 분리되는 단계이다.
  • 이 단계에서 적절한 작업을 수행하기 위해 사용되는 몇가지 콜백 메서드가 있다.

1-1. onAttachedToWindow()

  • View가 window에 연결될 때 호출된다.
  • View가 활성화될 수 있고, draw할 surface(표면)가 있다는 것을 아는 단계이다.
  • 따라서, 리소스 할당을 시작하거나 리스너를 설정할 수 있다.

1-2. onDetachedFromWindow()

  • View가 window에서 분리될 때 호출된다.
  • 이 시점에서는 더 이상 그릴 surface가 없다.
  • 따라서 예약된 작업을 중지하거나 할당된 리소스를 정리해야 한다.
  • ViewGroup에서 remove view를 호출하거나 Activity가 destroy될 때 호출된다.

1-3. onFinishInflate()

  • View의 inflate가 끝날 때 호출된다.
  • 즉, 레이아웃의 경우 모든 Child View가 추가된 후에 호출된다.

2. Traversals

  • View의 계층 구조는 상위 노드인 ViewGroup부터 리프 노드인 하위 View로 분리되는 트리구조와 같다.
    • 그렇기 때문에 이를 traversals(순회) 단계라고 부른다.
  • 각 메서드는 상위 노드부터 시작하여 마지막 리프 노드까지 순회하여 제약 조건을 정의하게 된다.

2-1. onMeasure()

  • View의 크기를 측정하기 위해 호출된다.
  • ViewGroup의 경우 계속해서 하위 View에 대한 측정을 진행하고, 측정 결과가 ViewGroup 자체의 크기를 결정할 것이다.

MeasureSpec

  • 부모에서 자식으로 전달되는 레이아웃 요구사항을 캡슐화한다.
  • 각 MeasureSpec은 너비나 높이에 대한 요구 사항을 나타낸다.
  • MeasureSpec은 크기와 모드로 구성되며, UNSPECIFIED, EXACTLY, AT_MOST 3가지 모드가 있다.

2-2. onLayout()

  • View를 측정하여 화면에 배치한 후 호출되는 메서드이다.

2-3. OnDraw()

  • 크기(size)와 위치(position)는 이전 단계에서 계산되므로, View는 단순히 이를 기반으로 그리게 된다.
  • onDraw(Canvas) 메서드에서 생성된 Canvas 객체에는 GPU로 보낼 OpenGL-ES(그래픽 라이브러리) 명령 목록이 있다.
  • onDraw()는 여러번 호출되기 때문에 이 메서드에서 객체를 만들면 안된다.

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

3-1. onSaveInstanceState()

  • 첫째, 상태를 저장하려면 ID를 제공해야 한다.
    • View 계층에 동일한 ID를 가진 여러 개의 뷰가있는 경우 고유한 ID를 지정하여 모든 상태를 저장할 수 있도록 한다.
  • 둘째, View.BaseSavedState를 확장하여 속성값을 저장하는 클래스가 필요하다.

3-2. onRestoreInstanceState(Parcelable state)

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

기타. View의 속성이 변경되었을 때 작동하는 메소드

1. invalidate()

  • 변경사항을 보여주고자 하는 특정 View에 대해 강제로 다시 draw를 요구하는 메서드이다.
  • View 모양이 변경되면 invalidate()를 호출한다고 이해할 수 있다.

2. requestLayout()

  • View의 속성이 변경되었을 때 View를 다시 측정하기 위해 requestLayout()을 호출한다.
  • 즉, Measure → Layout → Draw로 이어지는 View의 측정 및 레이아웃 단계를 다시 계산해야 한다는 신호를 전달한다.
  • 중요한 점 : View에서 메서드를 호출할 때는 항상 UI Thread에서 호출해야 한다.
    • 다른 Thread에서 작업을 수행중인 상태에서 View를 업데이트하려면 Handler를 사용한다.

참고한 사이트

profile
필요한 내용을 공부하고 저장합니다.

0개의 댓글