Android에서 View는 어떻게 그려질까? - 1

DoDoBest

·

2024. 2. 28. 17:43

 학습하게 된 이유

TextView에는 marquee라는 속성이 있다. 이것을 이용하면 텍스트가 TextView 영역을 계속 움직이도록 할 수 있다.

 

 

그런데 시작과 끝이 완전히 이어지지 않고 약간의 공백이 있는 것을 볼 수 있다. 이 공간을 늘리거나 줄이는 attribute는 TextView가 제공하지 않는다. 해당 공간을 줄일 수 있는 방법을 찾아보다가 아래 답변을 통해 TextView를 상속한 CustomView를 통해 줄일 수 있음을 알게됐다.

 

https://stackoverflow.com/a/72749204/11722881

 

Android: How to remove end blank space from Textview with marquee

I have created TextView with marquee, but I want to remove the blank space between end and start showing the text again. Here is the screen https://i.stack.imgur.com/dfdT8.png Here is my layou...

stackoverflow.com

이 과정에서 나에게 부족했던 부분은 다음 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

 

Android 4.3 API  |  Android 개발자  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Android 4.3 API 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. API 수준: 18 Android 4.3 (JELLY_BEAN_MR2)은 사용자

developer.android.com

https://blog.naver.com/purplestudiogames/220605836258

 

안드로이드: 나인 패치 Nine Patch

1. 나인 패치가 뭔가요? 나인 패치는 안드로이드, iOS에서 이미지 리소스의 크기를 줄이기 위해서 꼭 필...

blog.naver.com

https://developer.android.com/about/versions/jelly-bean#:~:text=the%20view%20hierarchy.-,Optical%20bounds%20layout%20mode,-A%20new%20layout

 

Jelly Bean  |  Android 개발자  |  Android Developers

더욱 향상된 Jelly Bean 버전인 Android 4.3에 오신 것을 환영합니다. Android 4.3에는 사용자와 개발자를 위한 성능 최적화 및 뛰어난 새로운 기능이 포함되어 있습니다. 이 문서에서는 개발자를 위한 새

developer.android.com

https://academy.realm.io/posts/360-andev-2017-andrea-falcone-android-developer-options-deep-dive/

 

Android Developer Options Deep Dive

If you enjoy talks from 360 AnDev, please support the conference via Patreon! Testing an app on your perfect Pixel device under perfect network conditions works great, but you’re getting reports of strange behavior from the wild and weird crashes that yo

academy.realm.io

https://developer.android.com/guide/topics/ui/how-android-draws

 

Android가 뷰를 그리는 방법  |  Views  |  Android Developers

활동이 포커스를 받으면 레이아웃을 그리라는 요청을 받습니다. Android 프레임워크에서 그리기 절차를 처리하지만 활동에서 레이아웃 계층 구조의 루트 노드를 제공해야 합니다. 그리기는 레이

developer.android.com

https://medium.com/flobiz-blog/create-resizable-bitmaps-9-patch-files-48c774db4526

 

Create resizable bitmaps (9-Patch files)

What is 9-patch image

medium.com

https://unyongkim.tistory.com/11

 

[Android] 나인패치(9-Patch) 이미지 만드는 방법(안드로이드 스튜디오 또는 나인패치 생성 웹사이트

안녕하세요~ 우뇽킴입니다. 개발하면서, 모바일 앱 내에 이미지를 적용할 일들이 있습니다. 그런데, 모바일 기기 환경 상 기기의 종류도 여러가지이다보니 해상도도 기기마다 다를 수 밖에 없습

unyongkim.tistory.com

https://blog.jakelee.co.uk/how-to-use-9-patch-images-for-resizable-backgrounds-in-android/

 

How to use 9-patch images for resizable backgrounds in Android

In Android, almost all views can have a background colour or image set. Whilst a colour can be any size / shape, as can a vector drawable, a bitmap drawable cannot. For example, trying to make a 100px wide & 100px tall image 500px wide and 50px tall would

blog.jakelee.co.uk