<com.android.launcher.Workspace
android:id="@+id/workspace"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
launcher:defaultScreen="1">
<include android:id="@+id/cell1" layout="@layout/workspace_screen" />
<include android:id="@+id/cell2" layout="@layout/workspace_screen" />
<include android:id="@+id/cell3" layout="@layout/workspace_screen" />
</com.android.launcher.Workspace>
<include android:layout_width="fill_parent" layout="@layout/image_holder" />
<include android:layout_width="256dip" layout="@layout/image_holder" />
Layout Tricks: Creating Efficient Layouts
안드로이드 UI 툴킷은 몇 가지 Layout 매니저(LinearLayout / RelativeLayout 등의 ViewGroup)를 제공한다. Layout 매니저들은 쉽게 사용할 수 있으며, 많은 경우 기본적인 기능만을 이용해서 원하는 사용자 인터페이스를 구현할 수 있다.
하지만 불행하게도, 기본적인 구성요소만을 사용하는 것이 사용자 인터페이스를 작성하는데 가장 효율적인 방법은 아니다. 예를 들어, 개발자가 LinearLayout 을 남용해서 사용할 경우, 전체 UI를 구성하는 View의 계층구조가 복잡해지고, 사용되는 View 의 숫자가 크게 증가될 수 있다. 화면을 구성하는데 사용된 모든 View 와 Layout 매니저들을 처리하는데는 비용이 든다. 특히 LinearLayout 의 weight 파라매터를 사용하는 경우, Layout 화면을 구성하기 위해, 두 번의 Measure Pass 를 거쳐야 하기 때문에 더욱 많은 비용이 소모된다.
여기 매우 단순하고 일반적인 예가 하나 있다. 아래의 그림과 같이 하나의 리스트 아이템을 표현하기 위한 Layout 을 생각해 보자. 왼쪽에는 아이콘 이미지가 있고, 상단에는 타이틀 텍스트가 그 아래는 부가적인 설명이 포함된 Layout 이다.
HierarchyViewer 로 캡처한 Layout 와이어 프레임을 살펴보면, 하나의 ImageView 와 두 개의 TextView 가 어떻게 구성되어 있는지 보다 명확하게 확인 할 수 있다.
LinearLayout 을 이용하 이러한 화면을 구성하는 것은 어렵지 않다. 전체 Layout 은 Horizontal LinearLayout (orientation 속성값이 Horizontal ) 이며, 하나의 ImageView 와 Vertical LinearLayout 을 포함하고 있다. 또한 Vertical LinearLayout 은 두 개의 TextView 를 포함한다. 소스 코드는 다음과 같다.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:padding="6dip">
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_marginRight="6dip"
android:src="@drawable/icon" />
<LinearLayout
android:orientation="vertical"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="fill_parent">
<TextView
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="My Application" />
<TextView
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:singleLine="true"
android:ellipsize="marquee"
android:text="Simple application" />
</LinearLayout>
</LinearLayout>
이 Layout 은 정상적으로 작동하지만, 낭비 요소를 담고 있다. 개발자는 하나의 RelativeLayout 이용해서 동일한 형태의 UI를 구성할 수도 있다. 그럼으로 화면을 구성하는데 사용되는 View 의 수를 하나 줄일 수 있고, 추가 적으로 View 의 계층 구조도 좀 더 단순화 될 수 있다. 또한 RelativeLayout 을 이용해서 구현하는 것은 어렵지도 않다.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:padding="6dip">
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_alignParentTop="true"
android:layout_alignParentBottom="true"
android:layout_marginRight="6dip"
android:src="@drawable/icon" />
<TextView
android:id="@+id/secondLine"
android:layout_width="fill_parent"
android:layout_height="26dip"
android:layout_toRightOf="@id/icon"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:singleLine="true"
android:ellipsize="marquee"
android:text="Simple application" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/icon"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_above="@id/secondLine"
android:layout_alignWithParentIfMissing="true"
android:gravity="center_vertical"
android:text="My Application" />
</RelativeLayout>
새로운 구현 방식은 이전에 LinearLayout을 이용한 구현한 것과 거의 동일하게 작동하지만, 한가지 주의해야할 점이 있다. 위의 Layout 중에, 만일 특정 리스트 아이템에 대해서, 부가적인 설명을 나타내는 TextView 가 사용될 필요가 없는 경우 이전 LinearLayout 을 이용해서 구성한 경우, 해당 TextView 의 Visibility 속성을 Gone 으로 설정하면 손쉽게 부가 설명 항목을 제외 시킬 수 있다. 하지만 그러한 방식은, RelativeLayout 을 사용하는 경우 정상적으로 작동하지 않을 수도 있다.
RelativeLayout 에서 View 들은 특정 부모 View (RelativeLayout 자체나 아니면 해당 Layout이 참조하고 있는 View) 를 기반으로 정렬되어 진다. 예를 들어, 위 예제의 경우, 부가적인 설명을 표시하는 TextView 는 RelativeLayout 의 바닦면에 정렬되며, 타이틀을 표시하는 TextView 는 그 View 위에 위치하도록 선언되어 있다. 이 때, 부가적인 설명을 표시하는 TextView 가 Gone 이 될 경우, RelativeLayout 은 타이틀을 표시하는 View 를 어디에 위치시켜야 할지 알 수 없게 된다. 이러한 문제를 해결하기 위해, 개발자는 layout_alignWithParentIfMissing 파라매터를 사용할 수 있다.
이 Boolean 인자는 만일 특정한 View 가 기준으로 삼아야되는 부모 View 가 없을 경우, 해당 View 를 포함하고 있는 RelativeLayout 자체를 기준으로 삼으라고 알려준다. 예제의 경우, aliginWithParentIfMissing 를 설정함으로서, 만일 타이틀을 표시하는 TextView 가 기준으로 삼고 있는 TextView 가 없을 경우 (Gone 에 의해...), 해당 View 대신 RelativeLayout 을 기준으로 삼게 되고, 그 결과 TextView 는 아래와 같이 RelativeLayout 의 바닦면 바로 위에 위치하게 된다.
이로서 RelativeLayout 을 이용해 구성한 두 번째 UI도 완벽하게 작동한다. 무엇보다도, 계층구조는 더 단순해 졌고, LinearLayout 의 weight 파라메터를 사용하지 않았기 때문에 더 효율적이다. 두 가지 구현 방식의 차이점은 HierarchyViewer 를 이용해 살펴 보면 확실하게 들어난다.
다시한번 말하지만, 이러한 차이는 ListView 를 구성하는 개발 아이템 아이템 마다, 동일한 형식의 Layout 을 사용하게 되면 훨씬 더 중요해 질 수 있다. 개발자들이 이번 글에서 예로든 간단한 예제를 통해, 여러가지 레이아웃에 대해 잘 아는 것이, UI 를 최적화하는데 최선의 길임을 알 수 있기를 희망한다.
Layout Tricks: Using ViewStubs
안드로이드에서 Activity 간에 UI 구성 요소를 공유하고 재사용하는 것은 매우 쉽다. <include/> 태그에게 감사하자. 때로는 복잡한 UI를 만들어내는 것이 너무 쉬운 나머지, 매우 드물게 사용되는 View 들도 전부 포함하는, 굉장히 많은 View 로 구성된 UI를 만들어 낼 수도 있다. 이런 경우를 대비해, 매우 고맙게도, 안드로이드에서는 ViewStub 위젯을 제공해 준다. ViewStub 을 사용하면, 개발자는 <include/> 태그가 제공하는 이점을 취하면서도, 잘 사용되지 않는 View 는 생성하지 않고 Layout 을 구성할 수 있다.
ViewStub 은 매우 가벼운 더미 View 이다. 이 View 는 어떤 영역을 차지하지도 않으며, 무엇을 그리지도 않고, UI 를 구성하는 Layout 에 참여하지도 않는다. 즉, ViewStub 을 생성하고, View 계층구조내에서 유지하는데 매우 적은 비용만이 필요하다. ViewStub 은 Lazy Include 하다고 표현할 수 있는데, ViewStub 에 의해 참조되고 있는 View 는 오직 개발자가 명시적으로 지시하는 경우에 한해서, 생성(inflate)되고 View 계층 구조에 추가되기 때문에 그렇다. (즉, Layout Inflate 시점에 바로 생성되지 않고 그 이 후에 생성된다...)
다음 그림은 Shelves 어플리케이션의 스크린 샷이다. 이 Activity 는 책 선 반위에 놓인, 사용자들이 열람가능한 책 목록을 보여준다.
동일한 Activtriy 가 사용자가 새로운 책을 추가할 때도 사용된다. 그런데, 책을 추가하는 작업이 이루어지는 동안에, Shelves 는 추가적인 사용자 인터페이스를 보여준다. 아래의 스크린샷에서 볼 수 있듯이, 책을 추가하는 동안 화면 하단에, 프로그래스바와 취소 버튼이 나타난다.
책을 추가하는 작업은, 적어도 책 목록을 살펴보는 작업에 비해 일반적인 작업이 아니기 때문에, 해당 정보를 표시하는 패널은 ViewStub 을 이용하여 구현되어 있다.
사용자가 책을 추가하는 작업을 시작하는 시점에 ViewStub 이 실재로 형상화 되며, 해당 ViewStub 이 참조하고 있는 Layout 파일이 ViewStub 을 대체하게된다.
ViewStub 을 사용하기 위해서, 개발자는 ViewStub 을 실재로 형상화 하기 위해 필요한 android:id 속성 값과 해당 ViewStub 이 어떤 Layout 파일에 정의된 View 와 교체될지를 나타내는 android:layout 속성 값을 지정해 주면된다. 추가로, ViewStub 에서는 또 한 가지 속성값이 사용된다. android:inflatedId 는 참조되는 Layout 파일의 루트 View 의 android:id 속성 값을 Override 할 수 있게 해 준다. 또한, ViewStub 에 정의한 Layout 파래매터들은, ViewStub 이 참조한 Layout 의 루트 View 에 적용된다. 실재 사용예는 아래와 같다.
<ViewStub
android:id="@+id/stub_import"
android:inflatedId="@+id/panel_import"
android:layout="@layout/progress_overlay"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />
개발자가 ViewStub 을 형상화 하고자 할 때면, 단순히 해당 ViewStub 의 inflate()메서드를 호출 하거나, Visibility 속성을 VISIBLE 이나 INVISIBILE 로 변경하면 된다. 단, inflate() 메서드를 를 호출할 경우, 형상화된 Layout 파일의 루트 View 를 반환값으로 전달 받을 수 있는 장점이 있다.
((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
// or
View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();
ViewStub 이 형상화된 이 후에는, View 계층 구조에서 완전히 제거된다는 것을 기억하는 것은 매우 중요하다. 즉, ViewStub 에 대한 참조를 Class 멤버 변수로 두는 식으로 오래 동안 유지할 필요가 없다.
ViewStub 은 쉬운 프로그래밍과 효율적인 프로그래밍 사이에서 환상적인 조화를 이루고 있다. 어플리케이션 실행 중에, 특정한 View 를 코드상에서 생성한 후, View 계층 구조에 추가하도록 구현하기 보다 ViewStub 을 사용하는 것이 훨씬 쉽고 효율적일 수 있다. 현재 ViewStub 이 갖고 있는, 딱 한가지 단점은 <merge/> 태그를 지원하지 않는다는 것이다.
Layout Tricks: Merging Layouts
한 번 작성한 Layout 코드를 공유하고 재사용하기 위해 <include/> 태그를 사용하는 방법에 대하여 이야기 했었다. 이번 글에서는 <include/> 태그를 사용할 때 생기는 문제점을 보완해 줄 수 있는 <merge/> 태그에 관해 이야기해 본다.
안드로이드에서 UI Layout 을 구성 할 때, View 계층 구조의 단계를 줄여 최적화 하기 위해 <merge /> 태그가 만들어졌다. 예제를 통해 살펴보면, 이 태그를 사용하는 목적에 대해 쉽게 이해할 수 있다. 다음의 예는, 어떤 이미지를 표시하고, 그 이미지 위해 해당 이미지의 제목을 표시해 주는 XML Layout 이다. 구조는 매우 단순하다. 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>
이 Layout 은 원하는 대로 잘 작동하며, 특별히 잘못된 점은 찾을 수 없다.
하지만 만일 이 레이아웃을 HierarchyViewer 를 통해 살펴보면 흥미로운 점이 들어난다. 개발자가 View 계층 구조를 잘 살펴보면, 우리가 XML 파일에 정의한 FrameLayout 이 딱 하나의 FrameLayout 만을 자식 View 로 가지고 있는 것을 확인 할 수 있다. (파란색으로 강조되어 있다.)
이 때 FrameLayout 은 fill_parent 속성 값을 사용하고 있기 때문에, 그 부모와 동일한 영역을 차지한다. 또한, 특별한 배경을 지정하지도 않았으며, 추가적인 Padding 속성이나 Gravity 속성을 지정하지 않았기 때문에, 사실 화면을 구성 하는데 역할을 수행하지 않는다. 즉, 이 경우 추가적으로 사용된 FrameLayout 은 어떠한 이유도 없이 그저 UI 를 보다 복잡하게 만들 뿐이다. 하지만 우리가 어떻게 이 FrameLayout 을 제거할 수 있을까? 어찌되었든, Layout 을 지정하는 XML 다큐먼트는 Root 태그를 가져야 하면, XML 상에 정의된 태그 는 실제View 로 구현된다.
바로 이 경우에 <merge/> 태그가 쓸모있다. LayoutInflater 가 View 를 형상화 하는 과정 중에, <merge/> 태그를 만나게 되면, 해당 <merge/> 태그는 건너 뛰고, 그 자식 View 들을 <merge/> 태그의 부모 View 에 추가한다. 설명이 조금 헷갈릴 수도 있겠다. 이해를 돕기 위해, 이전 예제에서 사용된 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>
이 새로운 버전의 Layout 에서 TextView 와 ImageView 는 최상위 FrameLayout 에 바로 추가된다. 그 결과, 화면상으로 동일하게 보이지만, 실재 View 계층구조는 좀 더 단순해 진다.
명백하게도 위의 예제에서 <merge /> 태그를 사용 할 수 있는 것은, Activity 에 사용된 Content View 가 FrameLayout 이기 때문이다. (FrameLayout 이 두 번 반복되기 때문에 하나를 줄일 수 있음) 만일 FrameLayout 대신 LinearLayout 이 Root 태그로 사용되었다면, <merge/> 태그를 사용할 수 없다.
하지만 <merge /> 태그는 다른 경우에도 유용하게 사용될 수 있다. 예를 들어, <merge /> 태그는 <include /> 를 통해 View 를 추가하고자 할 때, 완벽하게 작동한다. 또한, XML 상에서 몇개의 View 를 조합하여, 커스텀한 View 를 구성하고자 하는 경우에도 <merge /> 태그는 유용하게 사용된다. Button 내부의 내용을 원하는 대로 수정할 수 있는, 두 개의 Button 을 보여주는 OKCancelBar 라는 CustomView 를 만들 때, <merge /> 를 어떻게 사용할 수 있는지 한 번 살펴보자. (원한 다면, 이 예제의 완벽한 소스를 다운로드 받을 수도 있다.) 아래의 예제는, 어떤 이미지 위에 우리가 새롭게 정의한 CustomView 를 표시한다.
<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>
새로운 Layout 은 다음과 같은 결과를 만들어 낸다.
새로운 CustomView 인 OKCancelBar 의 소스 코드는 매우 단순하다. 왜냐하면 두 개의 버튼을 생성하기 위한 코드는 외부 XML 파일에 별도로 지정되어 있기 때문이다. 다음 코드에서 확인 할 수 있듯이, R.layout.okcancelbar 에 지정된 Layout 이 LayoutInflate 를 통해 형상화 된 후, OKCancelBar 의 자식 View로 추가된다.
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 Layout에 정의되어 있다. 추가적인 Layout 없이, 부모 View 인 OKCancelBar 에 두 개의 버튼을 직접 추가하기 위해서 <merge /> 태그를 사용했다. 또한, 개별 버튼은 유지하고 관리하기 쉽게 하기 위해, 외부 XML 파일에 별도로 구현된 후, <include/> 태그를 이용해 두 번 포함되었고, 단순히 id 값만을 Override 하였다.
<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 계층 구조를 갖으면서도, 유연하고 유지보수 하기 쉬운 Custom View 를 작성 하였다.
살펴본 봐와 같이, <merge/> 태그는 코드를 작성할 때, 굉장히 유용하며 깜짝 놀랄만한 일들을 해 준다. 하지만, 몇 가지 한계점도 갖고 있다.
- <merge/> 는 Root 태그로만 사용될 수 있다.
- <merge/> 로 시작되는 Layout 을 형상화 할 때, 개발자는 반드시, ViewGroup 을 지정해 주어야 하고, attachToRoot 값을 true 로 설정 해 주어야 한다. (보다 자세한 설명은 inflate() 메서드에 관한 다음 내용을 살펴 보라.)