Android 개발 시 새로운 디바이스와 새로운 OS는 개발자에게 있어서 다양한 시도를 도와주는 부분이기도 한다.
다양한 자원들의 상한선/하한선 등 사용 가능한 경계선의 범위가 점점 넓어지고 있습니다. 그러기에 Android 앱 개발시에 자원을 남용하기 쉽다.
본 글에서는 레이아웃 깊이를 1단계 줄이는 방법 중 하나를 소개한다.
서론
하나의 화면을 구성하는 방법은 작성자에 따라서 다양한 방법으로 결과를 만든다. 그중 레이아웃 작업시에는 XML을 사용해서 구성하는 경우가 대부분이다.
Android Studio에 기본적으로 적용된 Lint는 아래와 같다.
- 단일 XML에서 View Element의 갯수가 80개 이상인 경우
- 단일 XML에서 레이아웃 깊이가 10-Depth 이상인 경우
1번 경우인 단일 XML에 View Element가 80개를 정의한 경우는 간혹(?) 존재한다. 나의 경험에서는 단일 XML에서 레이아웃의 깊이는 10-Depth까지 사용하는 경우는 없는 것 같다. 개인적인 경험으로는 5~6 Depth를 넘어가면 Frame 부족으로 흔한 버벅대는 현상을 경험한다.
만약 버벅대는? 현상을 경험한다면 Android 개발에서는 아래의 케이스를 의심할 수 있다.
- 메인 스레드에서 무거운(?) 작업
- 레이아웃 깊이가 깊음
- GPU Overdraw가 많음
레이아웃 깊이를 해결하는 방법은 다양하게 있다.
- 레이아웃 구조 수정
- ConstraintLayout으로 깊이 수정
- CustomView 수정
- 디자인 수정
이 글에서는 CustomView의 레이아웃 깊이를 줄이는 방법을 살펴볼 예정이다.
매우 간단한 CustomView 작업
이번에 테스트할 내용은 아래와 같은 View이다.
LinearLayout만 사용한다면 몇 개의 깊이가 될 것이라는 걸 쉽게 떠올릴 거다.
<LinearLayout>
<View />
<LinearLayout>
<TextView />
<TextView />
</LinearLayout>
</LinearLayout>
위 예로는 2-level의 깊이로 구현되었습니다.
만약 ConstraintLayout이라면 아래와 같이 구현될 것이다.
<androidx.constraintlayout.widget.ConstraintLayout>
<View />
<TextView />
<TextView />
</androidx.constraintlayout.widget.ConstraintLayout>
일반적인 방법
흔하게 사용하는 방법은 아래와 같다. 아래의 레이아웃을 보더라도 기존에 작성하던 레이아웃 방식과 다르지 않다.
<androidx.constraintlayout.widget.ConstraintLayout ...
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp">
<View
android:id="@+id/ivIcon"
...
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvTitle"
...
app:layout_constraintStart_toEndOf="@id/ivIcon"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvSubtitle"
...
app:layout_constraintStart_toStartOf="@id/tvTitle"
app:layout_constraintTop_toBottomOf="@id/tvTitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
정의한 Layout을 사용하는 것 또한 매우 간단하다. 이 예제에서는 NormalSampleView 이름으로 정의했다.
class NormalSampleView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
init {
inflate(context, R.layout.widget_normal_sample, this)
}
}
결과
이렇게 사용하는 경우 위의 레이아웃 결과를 얻을 수 있다.
NormalSampleView
⎣ ConstraintLayout
⎣ Child Views
NormalSampleView 또한 ConstraintLayout으로 작성되었으므로, 실제로 2중 ConstraintLayout으로 NormalSampleView가 구성되어 있다. 또한, Layout Inspector의 Rotate View를 통해서도 해당 현상을 더 명확하게 확인할 수 있다.
일반적인 CustomView 동작의 원리
// LayoutInflater.java
public abstract class LayoutInflater {
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
...
try {
...
if (TAG_MERGE.equals(name)) {
...
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
...
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
...
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
...
}
} catch (XmlPullParserException e) {
...
} catch (Exception e) {
...
} finally {
...
}
return result;
}
}
}
inflate(context, R.layout.widget_normal_sample, this)를 호출한 함수는 최종적으로 LayoutInflater#inflate(XmlPullParser, ViewGroup, boolean) 함수를 호출한다.
LayoutInflater#inflate(XmlPullParser, ViewGroup, boolean) 함수로 전달되는 parameter의 실제 내용은 아래와 같다.
- XmlPullParser : XmlBlock$Parser
- ViewGroup : NormalSampleView
- boolean attachToRoot : true
inflate 내부에서 merge tag를 사용하지 않는 경우 아래와 같은 처리를 한다.
- 임시 View 생성 : final View temp = createViewFromTag(root, name, inflaterContext, attrs)
- 자식 뷰들을 생성 : rInflateChildren(parser, temp, attrs, true);
- root view에 임시 View를 추가 : root.addView(temp, params);
이 경우에는 LayoutInflater#inflate 내부의 과정에서 include와 유사한 형태로 적용된다. 그렇기에 레이아웃 깊이가 1-depth 많다.
이 1-depth는 merge tag를 사용해서 줄일 수 있다. 이어서 확인해보겠다.
Merge 방식
기존 CustomView에서 inflate할 XML의 Root는 ConstraintLayout였다. merge 방식에서는 merge tag로 root를 선언한다. 해당 화면이 호출하는 곳이 ConstraintLayout이라는 것을 Preview에 알리기 위해서 parentTag를 사용한다.
<merge ...
tools:padding="10dp"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<View
android:id="@+id/ivIcon"
...
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvTitle"
...
app:layout_constraintStart_toEndOf="@id/ivIcon"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvSubtitle"
...
app:layout_constraintStart_toStartOf="@id/tvTitle"
app:layout_constraintTop_toBottomOf="@id/tvTitle" />
</merge>
merge tag를 사용한 Layout을 사용한 예제를 MergeSampleView에서 사용한다.
class MergeSampleView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
init {
inflate(context, R.layout.widget_merge_sample, this)
setPadding(25)
}
}
기존 방식에서는 Root Layout에 padding을 정의할 수 있었다. 그러나 merge된 레이아웃에는 padding을 처리를 레이아웃에서 불가능하므로 코드로 대응한다.
결과
MergeSampleView는 기존 방법과는 다르게 1-depth가 적은 결과를 얻었다. Layout Inspector의 Rotate View를 통해서도 깊이를 확인할 수 있다.
merge의 CustomView 동작의 원리
merge tag 또한 기존 방식과 유사하게 LayoutInflater#inflate 호출한다.
public abstract class LayoutInflater {
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
...
try {
...
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
...
}
} catch (XmlPullParserException e) {
...
} catch (Exception e) {
...
} finally {
...
}
return result;
}
}
}
inflate(context, R.layout.widget_merge_sample, this)를 호출한 함수는 최종적으로 LayoutInflater#inflate(XmlPullParser, ViewGroup, boolean) 함수를 호출한다.
merge가 아닌 형태와 다른 부분은 임시 root를 만들지 않고, root에 직접 child view를 추가한다는 점이다.
또한, merge tag를 사용한 경우 root가 null이거나 root에 attach 되지 않은 경우라면 InflateException 에러를 발생한다.
Summary
지금까지 CustomView에서 1-depth 레이아웃을 깎는 방법을 간단하게 살펴봤다.
이 방식을 사용하더라도 큰 폭으로 레이아웃 퍼포먼스가 개선되지는 않습니다. 작업량에 비해서 얻는 효과는 적은 편입니다.
또한 merge 레이아웃을 호출한 ViewGroup의 Layout 속성을 초기화 처리하기 위해서는 코드로 직접 정의해야 하는 불편함을 감수해야 하는 부분은 아쉬운 포인트이다.
'Android > 커스텀 뷰' 카테고리의 다른 글
디자인 시스템의 텍스트 크기 문제 해결 경험 (0) | 2024.03.23 |
---|---|
CustomBottomSheet (0) | 2023.10.14 |
하이라이트 튜토리얼 커스텀뷰를 만들어보자 (0) | 2023.05.20 |
둥근 모서리를 가진 그라데이션 테두리 사각형 만들기 (0) | 2023.03.29 |
커스텀 뷰 만들어보자 (0) | 2022.12.30 |