Android에서 View는 어떻게 그려질까? - 1
DoDoBest
·2024. 2. 28. 17:43
학습하게 된 이유
TextView에는 marquee라는 속성이 있다. 이것을 이용하면 텍스트가 TextView 영역을 계속 움직이도록 할 수 있다.
그런데 시작과 끝이 완전히 이어지지 않고 약간의 공백이 있는 것을 볼 수 있다. 이 공간을 늘리거나 줄이는 attribute는 TextView가 제공하지 않는다. 해당 공간을 줄일 수 있는 방법을 찾아보다가 아래 답변을 통해 TextView를 상속한 CustomView를 통해 줄일 수 있음을 알게됐다.
https://stackoverflow.com/a/72749204/11722881
이 과정에서 나에게 부족했던 부분은 다음 2가지다.
1. 타인의 코드(여기서는 Google 공식 코드지만)를 분석하고 내가 필요로 하는 부분을 찾을 수 있는 능력
2. CustomView를 구현할 수 있는 능력
1번을 갖추기 위해 sunflower, uamp와 같은 구글 sample repository를 보고 있다.
2번 Custom View는 학습이 필요하다.
View를 상속해서 TextView와 동일한 역할을 하는 Custom View를 만들어보려고 했다. 하지만 관련 자료가 적고, 공식 문서나 주어진 내용들을 아직 나의 수준에서 이해하기란 어려웠다. 또한 16,000줄 가까이 되는 TextView를 View에서부터 구현하는 것은 너무 과도한 목표라는 것을 알게됐다.
그래서 View가 그려지는 과정을 먼저 이해하면 Custom View 학습에 도움이 될 것이라 생각하여 View가 그려지는 과정을 학습하게 됐다. 이후 TextView를 상속하는 Custom View를 만들어 직접 marquee 공백을 없애볼 계획이다.
Android에서 View는 어떻게 그려질까요?
Android framework는 activity가 focus를 받으면 activity에게 자신의 layout을 그릴 것을 요청한다. Android Framework가 layout 그리는 것을 직접 처리하지만, Activity UI의 시작점이 되는 activity의 root node가 필요하기 때문에 요청하는 것이다. 따라서 Activity는 반드시 layout 계층 구조의 root node를 제공해야 하며, onCreate 함수에서 setContentView 함수를 호출하는 것이 이에 해당한다.
View를 그리는 순서는 아래와 같다.
1. layout의 root node를 그린다.
2. layout tree를 measure한다.
3. layout tree를 그린다.
root node부터 시작하여 leaf node 까지 순차적으로 탐색하며, 새롭게 그려져야 하는 UI 영역(the invalid region)과 겹치는View를 그린다. ViewGroup은 draw 함수를 이용해서 child view가 그려지도록 요청하는 책임이 있고, 각 View는 자신의 UI를 그리는 데 책임(responsible)이 있다.
tree는 pre-order 순서로 그려지기 때문에, 부모 노드가 먼저 그려지고, child node를 그린 후, sibling node를 그린다.
Android framework는 valid region에 없는 View를 그리지 않는다. invalidate() 함수를 호출해서 View가 그려지도록 강제할 수 있다.
The framework는 두 가지 과정을 거쳐서 layout을 그리는 데, measure pass와 layout pass가 있다. measure pass는 measure 함수(measure 함수가 onMeasure 함수를 호출한다)에서 수행되며, View tree에서 top-down 순으로 순회하며 수행된다. 각 View는 자신의 width와 height(dimension specification)을 측정하고, child View에게 측정한 width와 height을 전달한다. dimension specification은 dp, px와 같은 정확한 값일 수도 있고, match_parent, wrap_content 일 수도 있다. measure pass가 끝나면 각 View는 자신의 크기를 가지게 된다. 이제 layout pass를 수행한다. layout pass는 layout 함수에서 수행되며, parent View가 child View의 위치를 지정한다. 이때 보통은 measure pass 과정에서 측정한 크기 값을 활용한다.
이제 각 pass에 대해 자세히 알아보자
measure pass
시작하기 앞서 measure 함수와 onMeasure 함수를 알아보자. measure 함수는 view의 크기를 측정하기 위해 호출된다.
widthMeasureSpec, heightMeasureSpec은 Parent View 내에서 child View가 최대한 사용해도 된다고 권고되는 크기이다. 권고라고 적은 이유는 child View가 이 값보다 크게 설정해도 RunTime Exception이 발생하는 것은 아니나, 일부 View가 짤려보이는 것과 같이 의도와 다르게 보일 수 있기 때문이다.
27121 줄을 보면 onMeasure 함수를 호출하는 것을 볼 수 있다.
이제 onMeasure 함수를 살펴보자. \View의 크기를 계산하고, 계산한 값을 setMeasuredDimension 함수를 통해 설정하라고 되어 있다.
또한, getSuggestedMinimumHeight() and getSuggestedMinimumWidth() 함수를 통해 얻을 수 있는 최솟값보다 크게 설정하라고 되어 있다. 코드에서는 suggestedMinimumWidth, suggestedMinimumHeight로 이 값에 접근할 수 있다.
setMeasuredDimension 함수를 보자. 먼저, View가 ViewGroup이고 LayoutMode가 Optical bound 인지 확인한다. Optical bound의 정의는 아래와 같다.
Optical bounds describe where a widget appears to be. They sit inside the clip bounds which need to cover a larger area to allow other effects, such as shadows and glows, to be drawn.
이후 측정한 View의 길이를 내부 변수에 저장하는 것을 볼 수 있다.
Optical Bound는 무엇일까? 아래 그림에서 빨간 선으로 되어 있는 영역이 Optical Bound이고, 외곽에 있는 파란선이 clip bound이다. 분홍색으로 칠해진 영역은 마진 영역이다.
이것은 개발자 모드에서 레이아웃 표시 기능을 키면 볼 수 있다. 에뮬레이터에서는 Show layout bounds를 키면 된다.
왼쪽은 clip bound(LAYOUT_MODE_CLIP_BOUNDS)가 적용된 ViewGroup을 보여준다. optical bound와 clip bound 영역이 일치한다.
오른쪽은 optical bound(LAYOUT_MODE_OPTICAL_BOUNDS)가 적용된 ViewGroup을 보여준다. optical bound는 View가 차지하는 영역와 완전히 일치하도록 변경되었다.
xml에서 layoutMode를 이용해 이 값을 설정할 수 있으며, 기본 값은 clip mode이다.
이것은 나인 패치(NinePatch)와 관련된 값이다. 2시간 동안 찾아봤으나 이와 관련된 자세한 설명은 찾지 못했다.
그래서 직접 만든 코드와 실행 결과만 남긴다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutMode="opticalBounds"
tools:context=".MainActivity">
<TextView
android:id="@+id/firstText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="@drawable/insert_drawable"
android:text="안녕하세요 안녕하세요 하세요하세요 안녕하세요"
android:textSize="30sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/secondText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="@drawable/insert_drawable"
android:text="안녕하세요 안녕하세요 하세요하세요 안녕하세요"
android:textSize="30sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/firstText" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="@drawable/insert_drawable"
android:text="안녕하세요 안녕하세요 하세요하세요 안녕하세요"
android:textSize="30sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/secondText" />
</androidx.constraintlayout.widget.ConstraintLayout>
nine patch drawable은 아래 파일을 이용했다.
https://github.com/JakeSteam/9patch/blob/master/app/src/main/res/drawable/background_image.9.png
남은 내용은 다음 글에서 이어서 작성하겠다.
참고자료
https://developer.android.com/about/versions/android-4.3.html#OpticalBounds
https://blog.naver.com/purplestudiogames/220605836258
https://academy.realm.io/posts/360-andev-2017-andrea-falcone-android-developer-options-deep-dive/
https://developer.android.com/guide/topics/ui/how-android-draws
https://medium.com/flobiz-blog/create-resizable-bitmaps-9-patch-files-48c774db4526
https://unyongkim.tistory.com/11
https://blog.jakelee.co.uk/how-to-use-9-patch-images-for-resizable-backgrounds-in-android/
'학습' 카테고리의 다른 글
for .. in 은 무엇일까 (0) | 2024.03.08 |
---|---|
Android에서 ConstraintLayout은 왜 사용하는 걸까 (0) | 2024.03.03 |
RecyclerView 공백 ViewHolder 문제 (0) | 2024.02.14 |
Android에서 Retrofit은 왜 사용하는 걸까 (0) | 2024.02.08 |
Android popUpTo가 동작하지 않는 경우 (0) | 2023.11.13 |