//
이번에는 마지막에 살펴봤던 ListView를 더 쓸모 있게 만드는 방법을 소개합니다.
내용이 좀 길어져서 2파트로 나누었습니다.
파트 1은 ListView 위젯을 더 복잡하게 꾸미고 성능을 향상 시키는 방법을 소개하고,
파트 2는 사용자와 Interact가 가능한 ListView를 먼저 살펴보고 이를 재활용 하는 방법에 대해 다룹니다.
1. XML을 이용해 꾸미기
스마트폰의 제한적인 스크린 사이즈 때문에 List위젯들(ListView, spinner등)의 중요성은 아주 크다.
list 위젯의 생김새를 결정하는 것은 전적으로 어떤 구성의 Adapter를 해당 list위젯에 연결하느냐에 달려있다.
다음 예는 전 장에서 사용한 썰렁한 text 대신, 아이콘+Text로 이루어진 행(row) xml layout 을 ListView에 적용한 예이다.
소스 펼치기
다음은 실행 화면이다.
2. Java 코드 내부에서 동적으로 꾸미기
위 단락의 예제와 같이 별도의 XML layout파일을 아답터에 적용 하고 List에 아답터를 연결하여 list의 각 row의 생김새를 바꾸는 작업은 어렵지 않다.
하지만, 다음과 같은 경우에는 XML layout 파일 기반의 방법은 사용이 어렵다.
- 모든 행(row)가 일률적인 layout으로 구성되지 않는 List.
- 모든 row가 일률적 layout이라도, layout을 구성하는 content가 제 각각일 때. (eg. 각 row마다 icon이 다를 때 등)
이런 경우 Adapter를 상속하는 커스텀 adapter를 만들고 getView()를 오버라이딩해서 문제를 해결할 수 있다.
소스 펼치기
위 소스에서 나오는 LayoutInflater 클래스는 쉽게 말해서 XML parser이다.
XML 형태로 제공된 layout(\res\layout\row.xml)을 View 객체로 구조화 하고 View 객체가 포함하는 여러 위젯 (여기서는 LinearLayout, ImageView, TextView)을 update 함으로 각 행(row)에 대한 layout을 커스터마이징 한다.
실행 결과는 다음과 같다. 글자수가 4자 이상인 row의 아이콘만 x로 바뀐 것을 볼 수 있다.
3. ListView 성능향상을 위한 여러 방법
위에서와 같이 getView() 를 오버라이딩하여 커스텀 뷰를 얻는 것은 표면적으론 아무 문제가 없지만 ListView의 특징하나를 고려할 때 몇 가지 문제가 있음을 알 수 있다.
문제의 근원은 "ListView는 표현된 row 정보를 cache하지 않는다"인데 이는 다시 말하면 화면에 새로 표현되는 row는 무조건 getView()를 호출한다라는 뜻이다.
(방 금 전에 화면에 그려진 row라고 하더라도 화면을 벋어났다 다시 화면에 그려질 때 getView()가 또 호출된다. 스크롤 바를 이용해 ListView를 위아래로 왔다 갔다 할 때 계속 호출될 getView()를 생각해보면 이해가 쉽다)
이와 특징은 다음과 같은 문제를 일으킨다.
- 반복된 View객체의 생성: getView()가 호출 시 마다 View객체를 새로 생성하고 이를 소멸(garbage collection)해야 함.
- 반복된 layout XML 파일 파싱. (LayoutInflater를 이용)
- 반복된 findViewById()의 호출.
휴 대용 기기 플랫폼에서 전원사용에 대한 중요성은 무척 크며, 안드로이드도 예외는 아니다. 위와 같은 불필요한 operation은 불필요한 전원소모로 이어진다. 또, 불필요한 operation은 사용자 UI의 반응 속도를 저하 할 수도 있다.
결론적으로, 배터리 소모를 최소화 하고, 빠른 UI 반응 속도로 최고의 UX(user experience)를 제공하는 효과 적인 코드 디자인이 필요하다는 소리이다.
ListView를 최적화 시키는 두 가지 방법은 다음과 같다.
[convertView 사용하기]
ListView 의 행(row)가 화면에 그려질 때마다 getView()가 호출된다면, getView()는 LayoutInflater 객체를 생성하고 row의 XML layout을 파싱하고 새로 생성된 View객체에 대입하여 이를 return해야 한다..
이런 반복되는 불필요한 절차를 없애기 위해서는 getView() 메소드의 두 번째 인자, converView를 활용하면 된다.
View getView (int position, View convertView, ViewGroup parent)
두 번째 인자 convertView는 null 또는 바로 전에 생성했던 View 객체이다.
지금 화면에 표현할 ListView의 row의 layout이 바로 전에 표현했던 row와 같은 형태의 layout을 가진다면 view를 새로 생성 할 필요 없이 이를 재활용 할 수 있는 것이다.
접기
ConvertView를 사용한 getView (MyFancyListView03.java)
01 | package com.holim.test; |
03 | import android.app.Activity; |
04 | import android.app.ListActivity; |
05 | import android.content.Context; |
06 | import android.os.Bundle; |
07 | import android.view.LayoutInflater; |
08 | import android.view.View; |
09 | import android.view.ViewGroup; |
10 | import android.widget.ArrayAdapter; |
11 | import android.widget.ImageView; |
12 | import android.widget.ListView; |
13 | import android.widget.TextView; |
15 | public class MyFancyListView03 extends ListActivity { |
18 |
String[] items={ "Android" , "iPhone" , "UI" , "Java" , "SDK" , |
19 |
"Adapter" , "List" , "This" , "is" , "Fun" }; |
21 |
/** Called when the activity is first created. */ |
23 |
public void onCreate(Bundle savedInstanceState) { |
24 |
super .onCreate(savedInstanceState); |
25 |
setContentView(R.layout.main); |
28 |
MyArrayAdapter aa = new MyArrayAdapter( this ); |
33 |
tv = (TextView)findViewById(R.id.selection); |
37 |
public void onListItemClick(ListView l, View v, int position, long id) { |
38 |
tv.setText(items[position]); |
42 |
class MyArrayAdapter extends ArrayAdapter<String> { |
49 |
MyArrayAdapter(Context context) { |
50 |
super (context, R.layout.row, items); |
53 |
this .context = context; |
57 |
public View getView( int position, View convertView, ViewGroup parent){ |
58 |
View row = convertView; |
62 |
LayoutInflater inflater = ((Activity)context).getLayoutInflater(); |
65 |
row = (View)inflater.inflate(R.layout.row, null ); |
69 |
label = (TextView)row.findViewById(R.id.label); |
72 |
label.setText(items[position]); |
76 |
if (items[position].length()> 4 ) { |
79 |
icon = (ImageView)row.findViewById(R.id.icon); |
82 |
icon.setImageResource(R.drawable.delete); |
접기
위 코드에서 보면 convertView가 null이면 새로운 View를 inflation하고 그렇지 않으면 그전에 inflation된 View를 재활용하여 불필요한 View의 생성과 XML 파싱을 방지한다.
하지만 ListView의 각 row가 각기 다른 View layout (어떤 row는 하나의 TextView로 구성되어 있고 어떤 row는 2개의 TextView로 구성)일 때는 위와 같은 방법을 적용하기 힘들 수도 있다.
물론, 조금의 잔머리 (상황에 따라 2개의 TextView로 구성된 row중 한 TextView에 android:visiability="gone"와 같은 속성을 적용해 감춰 버리면 된다.)를 써서 해결할 수도 있지만 문제의 근원이 완전히 해결된 건 아니다.
해결 방법은 차차 알아 보기로 하자.
[Holder Pattern 사용하기]
getView()에서 또 다른 오버해드는 잦은 findViewById()의 호출이다.
LayoutInflater에 의해 inflation 된 row View내부에서 수정할 위젯의 객체를 뽑아내는 역할을 하는 findViewById()는 수정이 필요한 위젯의 숫자가 많을수록 그 오버로드가 커진다.
Holder Pattern을 사용해 inflation 된 root view에서 한번 뽑아낸 위젯의 인스턴스를 보관(hold) 하고 다음 사용시 이를 재활용한다면 오버로드를 크게 줄일 수 있을 것이다.
Holder Pattern을 구현하기 위해서 다음과 같이 ViewWrapper 클래스를 사용한다.
접기
ViewRapper 클래스를 이용한 Holder Pattern구현 (MyFancyListView04.java)
001 | package com.holim.test; |
003 | import android.app.Activity; |
004 | import android.app.ListActivity; |
005 | import android.content.Context; |
006 | import android.os.Bundle; |
007 | import android.view.LayoutInflater; |
008 | import android.view.View; |
009 | import android.view.ViewGroup; |
010 | import android.widget.ArrayAdapter; |
011 | import android.widget.ImageView; |
012 | import android.widget.ListView; |
013 | import android.widget.TextView; |
015 | public class MyFancyListView04 extends ListActivity { |
018 |
String[] items={ "Android" , "iPhone" , "UI" , "Java" , "SDK" , |
019 |
"Adapter" , "List" , "This" , "is" , "Fun" }; |
021 |
/** Called when the activity is first created. */ |
023 |
public void onCreate(Bundle savedInstanceState) { |
024 |
super .onCreate(savedInstanceState); |
025 |
setContentView(R.layout.main); |
028 |
MyArrayAdapter aa = new MyArrayAdapter( this ); |
033 |
tv = (TextView)findViewById(R.id.selection); |
037 |
public void onListItemClick(ListView l, View v, int position, long id) { |
038 |
tv.setText(items[position]); |
042 |
class MyArrayAdapter extends ArrayAdapter<String> { |
044 |
ViewWrapper wrapper = null ; |
047 |
MyArrayAdapter(Context context) { |
048 |
super (context, R.layout.row, items); |
051 |
this .context = context; |
055 |
public View getView( int position, View convertView, ViewGroup parent){ |
056 |
View row = convertView; |
060 |
LayoutInflater inflater = ((Activity)context).getLayoutInflater(); |
063 |
row = (View)inflater.inflate(R.layout.row, null ); |
064 |
wrapper = new ViewWrapper(row); |
068 |
wrapper = (ViewWrapper)row.getTag(); |
072 |
wrapper.getLabel().setText(items[position]); |
077 |
if (items[position].length()> 4 ) { |
080 |
wrapper.getIcon().setImageResource(R.drawable.delete); |
084 |
wrapper.getIcon().setImageResource(R.drawable.ok); |
097 |
ImageView icon = null ; |
098 |
TextView label = null ; |
100 |
ViewWrapper(View base) { |
108 |
label = (TextView)base.findViewById(R.id.label); |
117 |
icon = (ImageView)base.findViewById(R.id.icon); |
접기
위의 소스에서 android.view.View의 메소드인 setTag(), getTag의 활용을 참고하자.
(setTag, getTag 메소드 관련 좋은 포스트가 있어 링크 합니다 - 비즈페이님의 블로그)
wrapper내부의 TextView, ImageView멤버 변수가 null 일 때만 findViewById()를 호출 함으로,
한번 초기화가 이루어 지면 다음부터는 저장된 인스턴스를 사용하게 됨으로 오버헤드를 크게 줄일 수 있다.