본문 바로가기

안드로이드

레이아웃 기법 - 레이아웃 합치기

item13 에서 레이아웃코드를 재사용하고 공유하기 위해 XML 레이아웃에서 <include />태그를 어떡해 사용하지는 보여주었다. 본 글은 <merge />태그를 설명하고 어떡해 <include />태그를 보강하는지에 대해 설명한다.

<merge />태그는 view트리의 레벨수를 줄여 안드로이드 레이아웃을 최적화하기 위한 목적으로 작성되었다.  예제를 보면 태그가 이 문제를 어떡해 해결하는지 이해하기 쉬울 것이다. 아래의 XML레이아웃은 이미지와 그 위에 이미지의 제목을 보여주는 레이아웃을 선언한다. 이 구조는 상당히 단순하다. FrameLayout이 ImageView의 위에 TextView를 올리기 위해 사용된다.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">

<ImageView
android:layout_width="fill_parent"
android:layout_height="fill_parent"

android:scaleType="center"
android:src="@drawable/golden_gate" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dip"
android:layout_gravity="center_horizontal|bottom"

android:padding="12dip"

android:background="#AA000000"
android:textColor="#ffffffff"

android:text="Golden Gate" />

</FrameLayout>

이 레이아웃은 멋지게 렌더링되고 아무 문제도 없는 것 처럼 보인다. 

A FrameLayout is used to overlay a title on top of an image

HierarchyViewer 의 결과를 조사해보면 좀더 흥미로운 것들을 볼수 있다. 결과 트리를 자세히 보면 XML파일에 정의된 FrameLayout은 또 다른 FrameLayout의 유일한 child임을 알게 될 것이다. 

A layout with only one child of same dimensions can be removed

FrameLayout은 fill_parent제약의 사용이나 gravity, padding과 같은 배경정의를 하지 않으므로서 그 부모와 같은 차원을 갖기 때문에 전적으로 불필요하다. 합당한 이유 없이 더 복잡한 UI를 만들었을 뿐이다. 하지만 어떡해 FrameLayout을 제거할 것인가? 결국, XML문서는 루트 태그를 필요로 하고 XML레이아웃의 태그는 항상 View 인스턴스를 표현한다.

여기에 <merge /> 태그의 출연이 쓸모가 있다. LayoutInflater가 이 태그를 만날때, 이 태그를 무시하고 <merge /> 부모에 <merge /> child를 추가한다. 혼돈 스러운가? 이전 XML 레이아웃의 FrameLayout을 <merge />로 대체하여 제작성해보자.

<merge xmlns:android="http://schemas.android.com/apk/res/android">

<ImageView
android:layout_width="fill_parent"
android:layout_height="fill_parent"

android:scaleType="center"
android:src="@drawable/golden_gate" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dip"
android:layout_gravity="center_horizontal|bottom"

android:padding="12dip"

android:background="#AA000000"
android:textColor="#ffffffff"

android:text="Golden Gate" />

</merge>

이 새로운 버전은 TextView와  ImageView모두 top-level FrameLayout에 추가될 것이다. 그 결과 view 의 시각적인 효과는 같지만 view 계층 구조는 더 단순해진다.

Optimized view hierarchy using the merge tag

activity를 구성하는 view의 부모는 항상 FrameLayout이기 때문에 이 경우에 <merge />의 사용은 명확하게 동작한다. 레이아웃이 인스턴스를 위해 root 태그로 LiearLayout을 사용한다면 이 기법을 적용하지 못할 것이다. 그럼에도 불구하고 <merge />는 다른 상황에서 유용하게 사용될수 있다. 예를 들어, <include />태그와 결합할때 완벽하게 동작할 것이다. 커스텀 view를 구성할때도 <merge />를 사용할수 있다. 예제의 소스코드를 다운로드 받아 OKCancelBar라는 새로운 view를 생성하기 위해 이 태그가 어떡해 사용되는지를 보자. 이미지 위에 이 커스텀 view를 디스플레이하기 위해 사용된 XML은 다음과 같다.

<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:okCancelBar="http://schemas.android.com/apk/res/com.example.android.merge">

<ImageView
android:layout_width="fill_parent"
android:layout_height="fill_parent"

android:scaleType="center"
android:src="@drawable/golden_gate" />

<com.example.android.merge.OkCancelBar
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"

android:paddingTop="8dip"
android:gravity="center_horizontal"

android:background="#AA000000"

okCancelBar:okLabel="Save"
okCancelBar:cancelLabel="Don't save" />

</merge>

새로운 레이아웃은 디바이스에서 다음과 같은 결과를 보여줄 것이다. 

Creating a custom view with the merge tag

OkCancelBar의 소스코드는 외부의 XML파일에 두개의 버튼이 정의되어 있고 LayoutInflate사용해 로딩하므로 매우 단순하다. 아래의 코드와 같이 XML레이아웃 R.layout.okcancelbar는 OkCancelBar와 함께 부모로 inflate 된다.

public class OkCancelBar extends LinearLayout {
public OkCancelBar(Context context, AttributeSet attrs) {
super(context, attrs);
setOrientation(HORIZONTAL);
setGravity(Gravity.CENTER);
setWeightSum(1.0f);

LayoutInflater.from(context).inflate(R.layout.okcancelbar, this, true);

TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.OkCancelBar, 0, 0);

String text = array.getString(R.styleable.OkCancelBar_okLabel);
if (text == null) text = "Ok";
((Button) findViewById(R.id.okcancelbar_ok)).setText(text);

text = array.getString(R.styleable.OkCancelBar_cancelLabel);
if (text == null) text = "Cancel";
((Button) findViewById(R.id.okcancelbar_cancel)).setText(text);

array.recycle();
}
}

다음의 XML레이아웃에 정의된 두개의 버튼이 정의되어 있다. 보는 바와 같이 OkCancelBar에 직접 두개의 버튼을 추가하기 위해 <merge />태그를 사용한다. 각 버튼은 관리하기 쉽도록 같은 외부 XML파일로부터 로딩된다. 각각의 id를 제외하고 단순화하여 보면 다음과 같다.

<merge xmlns:android="http://schemas.android.com/apk/res/android">
<include
layout="@layout/okcancelbar_button"
android:id="@+id/okcancelbar_ok" />

<include
layout="@layout/okcancelbar_button"
android:id="@+id/okcancelbar_cancel" />
</merge>

효율적인 view계층 구조를 생성하는 유연하고 관리하기 쉬운 커스텀 뷰 작성하였다.

<merge />태그는 매우 유용하며 코드에서 기적같은 일을 수행할 것이다. 하지만 아래와 같은 한계를 갖는다.

  • XML 레이아웃의 루트태그로만 사용가능하다.
  • <merge />로 시작하는 레이아웃을 inflat 시키때 부모 ViewGroup을 반드시 명시해야 하고 attachToRoot를 true로 설정해야 한다. (inflate(int, android.view.ViewGroup, boolean) 메서드 문서 참고)