간단하게 ArrayList, ArrayAdapter와 ListView를 같이 사용하는 방법을 다루었던 첫번째 포스팅의 코드를 수정하여 진행합니다
2018. 7. 5 - 최초 작성
ArrayList에 저장되어 있는 데이터를 ListView에서 요구하는 View(TextView, ImageVIew 등)으로 변환하여 보여질 수 있도록 하는 것이 ArrayAdapter입니다.
안드로이드 디바이스마다 화면크기가 다르기 때문에 하나의 ListView에서 보여질 수 있는 데이터 개수도 달라집니다.
ArrayAdapter는 ArrayList에 있는 몇번째 데이터들을 화면에 보여주게 할 지 결정합니다.
그리고 ListView의 레이아웃에서 요구하는 화면처럼 각 줄이 보이도록 데이터를 view 로 변환합니다.
view는 하나 또는 다수의 문자열이 될 수도 있고 텍스트와 이미지의 조합일 수도 있습니다.
LIstView에 ArrayAdapter가 연결되면 우선 화면에 보이는 갯수 만큼의 데이터가 보여지도록 ArrayAdapter가 뷰(VIew)를 생성합니다.
ListView에서 하나의 데이터를 보여주는 하나의 줄(row)가 하나의 뷰입니다.
스크롤되어 새로 보이게 되는 데이터를 위한 뷰를 새로 생성하고 안보이는 데이터를 위한 뷰(row view)는 나중에 사용되기 위해서 메모리에 유지됩니다.
1. ListView에서 스크롤이 발생하면 새로운 데이터를 보여주기 위해 필요한 뷰(view)를 달라고 ArrayAdapter에게 요청합니다. 이때 ArrayAdapter의 getView 메소드가 호출됩니다.
2. ArrayAdapter가 새로운 뷰를 리턴해주면 ListView는 해당 뷰를 화면에 보여주고 이제 화면에 보이지 않게 된 뷰는 메모리에 유지합니다. 필요시에는 바로 다른 뷰를 보여주기 위해 재사용됩니다.
만약 아이템 갯수가 많아진다면 이런 메커니즘은 메모리 관련 문제가 있습니다.
무한히 뷰들을 메모리에 저장할 수 없습니다.
이 문제를 해결하기 위해선 뷰를 재사용해야 합니다.
1. 아무리 많은 데이터가 있다하더라고 화면에 보여지는 것은 일부 입니다.
2. ListView가 처음 화면에 보여질때에는 모든 줄을 위한 뷰가 새로 생성되어야 합니다.
이때 getView 메소드의 convertView는 NULL 입니다.
3. 이후 ListView가 스크롤되면 보이지 않던 데이터를 화면에 보여줘야 합니다. 동시에 이젠 보이지 않게된 데이터가 발생합니다.
보이지 않게 되는 데이터를 위한 뷰를 새로 보여지게 되는 데이터의 뷰로 재사용합니다. 즉 뷰에 보여지는 데이터만 바꿉니다.
Voca.java
1. 리스트 뷰에 보여줄 데이터를 저장할 때 사용될 클래스를 생성합니다.
Custom Adapter에서 ListView의 한 줄(row)에 보여질 단위 데이터(=ArrayList의 하나의 아이템)로 사용됩니다.
public class Voca { private String english_name; private String korean_name; } |
2. Vocal 클래스는 ArrayList와 Custom Adapter에서 공통적으로 사용됩니다.
프로젝트 창에서 app > java 하위에 있는 패키지 이름을 선택한 채 마우스 우클릭합니다.
이때 보이는 메뉴에서 New > Java Class를 선택합니다.
3. Name에 저장할 데이터를 위한 클래스 이름(포스팅에선 Voca)을 적어줍니다.
4. 데이터 저장 및 불러올 때 사용할 Voca 클래스가 생성되었습니다.
5. 클래스에 저장할 데이터 타입별로 변수들을 선언합니다.
영어 단어와 대응하는 한국어 단어를 저장할거라서 2개의 String이 필요합니다.
package com.tistory.webnautes.usearrayadapterwithlistview;
public class Voca { private String english_name; private String korean_name; } |
6. 직접 변수에 접근해도 무방하지만 Getter(변수의 값 읽어옴)와 Setter(변수에 값 저장) 메소드를 선언하도록 하겠습니다.
6-1. Vaca 클래스 내부 위에서 마우스 우클릭하여 보이는 메뉴에서 Generate를 클릭합니다.
6-2. 이후 보이는 메뉴에서 Getter를 선택합니다.
6-3. Ctrl를 누른채 마우스 왼쪽 클릭하여 모든 변수를 선택하고 엔터를 누릅니다.
6-4. 다음처럼 2개의 Getter 메소드가 생성됩니다.
package com.tistory.webnautes.usearrayadapterwithlistview;
public class Voca { private String english_name; private String korean_name;
public String getEnglish_name() { return english_name; }
public String getKorean_name() { return korean_name; } } |
6-5. Getter 메소드를 생성했던 방법대로 (6.1 ~ 6.3) Setter 메소드를 생성합니다. 6-2에서 Getter 대신에 Setter를 선택하면 됩니다.
package com.tistory.webnautes.usearrayadapterwithlistview;
public class Voca { private String english_name; private String korean_name;
public String getEnglish_name() { return english_name; }
public String getKorean_name() { return korean_name; }
public void setEnglish_name(String english_name) { this.english_name = english_name; }
public void setKorean_name(String korean_name) { this.korean_name = korean_name; } } |
6-6. 한번에 데이터를 입력할 수 있도록 생성자 함수를 추가합니다.
package com.tistory.webnautes.usearrayadapterwithlistview;
public class Voca { private String english_name; private String korean_name;
public String getEnglish_name() { return english_name; }
public String getKorean_name() { return korean_name; }
public void setEnglish_name(String english_name) { this.english_name = english_name; }
public void setKorean_name(String korean_name) { this.korean_name = korean_name; }
public Voca(String _english_name, String _korean_name){ english_name = _english_name; korean_name = _korean_name; } } |
list_item.xml
1. ListView의 한 줄(row)에 여러 개의 컬럼(column)을 보여주기위해 필요한 레이아웃 파일입니다.
app > res > layout을 선택하고 마우스 우클릭하여 보이는 메뉴에서 New > Layout resource file을 선택합니다.
2. File name에 list_item, Root element에 LinearLayout을 적고 OK 버튼을 클릭합니다.
3. 생성된 XML 파일에 추가 코드를 입력합니다. android:orientation 속성을 "horizontal"로 수정합니다.
영어 단어와 한국어 단어를 표시할 TextView외에 getView의 동작원리를 살펴보기 위한 TextView를 더 추가했습니다.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:minHeight="?android:attr/listPreferredItemHeight" android:gravity="center_vertical" android:layout_width="match_parent" android:layout_height="wrap_content">
<TextView android:id="@+id/textview_list_english" android:layout_weight="5" android:layout_width="0dp" android:gravity="center" android:layout_height="wrap_content"/>
<TextView android:id="@+id/textview_list_korean" android:layout_weight="5" android:layout_width="0dp" android:gravity="center" android:layout_height="wrap_content" />
TextView android:id="@+id/textview_list_item_index" android:layout_weight="4" android:layout_width="0dp" android:gravity="center" android:layout_height="wrap_content" />
<TextView android:id="@+id/textview_list_item_tag" android:layout_weight="4" android:layout_width="0dp" android:gravity="center" android:layout_height="wrap_content" />
<TextView android:id="@+id/textview_list_item_status" android:layout_weight="3" android:layout_width="0dp" android:gravity="center" android:layout_height="wrap_content" />
</LinearLayout> |
VocaAdapter.java
1. 데이터를 저장하고 있는 ArrayList와 데이터를 화면에 보여주는 ListView간의 데이터 전달을 위해서 사용되는 Custom Adapter를 생성합니다.
앞에서 진행했던 방법대로 VocaAdapter 클래스를 생성합니다. ( 2-1의 2, 3번 참고)
package com.tistory.webnautes.usearrayadapterwithlistview;
public class VocaAdapter { } |
2. 간단히 구현하기 위해서 BaseAdapter 클래스대신에 ArrayAdapter 클래스를 상속받도록 합니다.
괄호<> 안에 Adapter에서 가져올 데이터를 위한 클래스를 적어줍니다. 여기에선 앞에서 정의한 Vaca 클래스를 적어주었습니다.
package com.tistory.webnautes.usearrayadapterwithlistview;
import android.widget.ArrayAdapter;
public class VocaAdapter extends ArrayAdapter<Voca> { } |
3. 생성자 메소드 VocaAdapter 와 getView 메소드를 생성합니다.
UserViewHolder는 XML 레이아웃 파일의 구성요소에 접근하는 시간을 단축시키기 위해 setTag, getTag와 함께 사용됩니다.
package com.tistory.webnautes.usearrayadapterwithlistview;
import android.content.Context; import android.support.annotation.NonNull; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView;
import java.util.List;
// VocaAdapter는 Voca 객체 리스트에 있는 데이터를 ListView의 row 레이아웃으로 제공 할 수 있는 ArrayAdapter 입니다. public class VocaAdapter extends ArrayAdapter<Voca> {
private Context context; private List mList; private ListView mListView;
class UserViewHolder { public TextView korean_name; public TextView english_name; public TextView itemIndex; public TextView itemTag; public TextView itemStatus; }
// VocaAdapter를 초기화하기 위한 생성자로 아규먼트로 전달받은 context와 list를 내부 저장 공간에 저장합니다. public VocaAdapter(Context context, // 레이아웃 XML 파일을 뷰(view) 객체로 바꾸는데 사용할 컨택스트(Context) List<Voca> list, // ListView에 보여줄 데이터인 Voca 객체 리스트 // 본 예제에서는 3개의 TextView에 Voca 객체의 데이터를 넣어서 ListVIew의 row view로 사용되도록 리턴합니다. ListView listview ) { super(context, 0, list);
this.context = context; this.mList = list; this.mListView = listview; }
// ListView의 한 줄(row)이 렌더링(rendering)될 때 호출되는 메소드로 row를 위한 view를 리턴합니다. // 한 줄(Row)를 위한 뷰(View)를 재사용하여 ListIView의 효율성을 올립니다. @NonNull @Override public View getView(int position, // LIstView에 보여지게 되는 데이터인 Voca 객체 리스트의 인덱스
View convertView, // 주어진 데이터를 보여주기 위해 사용될 한 줄(row)을 위한 뷰(View) // 값이 null인 경우에만 새로 생성하고 그 외에는 재사용됩니다.
@NonNull ViewGroup parentViewGroup // XML 레이아웃 파일을 View 객체로 변환하기 위해 사용되는 부모 뷰그룹 ) {
View rowView = convertView; // 코드 가독성을 위해서 rowView 변수를 사용합니다. UserViewHolder viewHolder; String Status;
// 기존에 생성했던 뷰라면 재사용하고 그렇지 않으면 XML 파일을 새로 view 객체로 변환합니다. if (rowView == null) {
// 레이아웃을 정의한 XML 파일(R.layout.list_item)을 읽어서 계층 구조의 뷰 객체(rowView)로 변환합니다. // rowView 객체는 3개의 TextView로 구성되어 있습니다. // // 다음 한줄로 구현도 가능합니다. // rowView = LayoutInflater.from(getContext()).inflate(R.layout.list_item, parentViewGroup, false); LayoutInflater layoutInflater = LayoutInflater.from(context); rowView = layoutInflater.inflate(R.layout.list_item, parentViewGroup, false);
// view holder의 구성 요소의 값과 한 줄을 구성하는 레이아웃을 연결함. // // rowView(=R.layout.list_item)에서 주어진 ID(R.id.textview_list_english)를 가진 뷰를 찾습니다. // 찾는 뷰의 타입에 따라 findViewById 리턴 결과를 타입 변환해줘야 합니다. viewHolder = new UserViewHolder(); viewHolder.english_name = (TextView) rowView.findViewById( R.id.textview_list_english // 한 줄에 대한 레이아웃 파일(R.layout.list_item)의 구성요소, ); viewHolder.korean_name = (TextView) rowView.findViewById(R.id.textview_list_korean); viewHolder.itemIndex = (TextView) rowView.findViewById(R.id.textview_list_item_index); viewHolder.itemTag = (TextView) rowView.findViewById(R.id.textview_list_item_tag); viewHolder.itemStatus = (TextView) rowView.findViewById(R.id.textview_list_item_status);
rowView.setTag(viewHolder);
Status = "created"; } else {
viewHolder = (UserViewHolder) rowView.getTag();
Status = "reused"; }
// 태그 분석을 위한 코드 시작 String Tag = rowView.getTag().toString(); int idx = Tag.indexOf("@"); String tag = Tag.substring(idx + 1);
//Voca 객체 리스트의 position 위치에 있는 Voca 객체를 가져옵니다. Voca voca = (Voca) mList.get(position);
//현재 선택된 Vocal 객체를 화면에 보여주기 위해서 앞에서 미리 찾아 놓은 뷰에 데이터를 집어넣습니다. viewHolder.english_name.setText(voca.getEnglish_name()); viewHolder.korean_name.setText(voca.getKorean_name()); viewHolder.itemIndex.setText("Voca[" + position + "]"); viewHolder.itemTag.setText(tag); viewHolder.itemStatus.setText(Status);
Log.d("@@@", "Voca[" + position + "]" + " row view is " + Status + ", tag = " + tag);
// 화면에 보여질 뷰를 리턴하여 ListView의 특정 줄로 보여지게 합니다. //본 예제에서는 5개의 TextView 구성되어 있습니다. return rowView; } } |
MainActivity.java
package com.tistory.webnautes.usearrayadapterwithlistview;
import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.widget.AbsListView; import android.widget.ListView;
import java.util.ArrayList; import java.util.List;
public class MainActivity extends AppCompatActivity {
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
// 1. 레이아웃을 정의한 레이아웃 리소스(R.layout)을 사용하여 현재 액티비티의 화면을 구성하도록 합니다. setContentView(R.layout.activity_main);
// 2. 레이아웃 파일에 정의된 ListView를 자바 코드에서 사용할 수 있도록 합니다. // findViewById 메소드는 레이아웃 파일의 android:id 속성을 이용하여 뷰 객체를 찾아 리턴합니다. final ListView listview = (ListView)findViewById(R.id.listview);
// 3. 실제로 문자열 데이터를 저장하는데 사용할 ArrayList 객체를 생성합니다. final List<Voca> voca = new ArrayList<>();
// 4. ArrayList 객체에 데이터(=Voca 객체)를 넣습니다. for( int i = 0; i< 100; i++) { voca.add(new Voca("apple", "사과")); voca.add(new Voca("orange", "오렌지")); voca.add(new Voca("banana", "바나나")); voca.add(new Voca("pineapple", "파인애플")); voca.add(new Voca("mango", "망고")); voca.add(new Voca("grape", "포도")); voca.add(new Voca("strawberry", "딸기")); voca.add(new Voca("pear", "배")); voca.add(new Voca("grapefruit", "자몽")); voca.add(new Voca("watermelon", "수박")); }
// 5. ArrayList 객체와 ListView 객체를 연결하기 위해 ArrayAdapter객체를 사용합니다. // 우선 ArrayList 객체를 ArrayAdapter 객체에 연결합니다. final VocaAdapter vocaAdapter = new VocaAdapter(this, voca, listview);
// 6. ListView 객체에 adapter 객체를 연결합니다. listview.setAdapter(vocaAdapter);
// 참고 http://kanais2.tistory.com/294 listview.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override public void onScrollStateChanged(AbsListView view, int scrollState) { Log.d("@@@Changed", "mListView.getFirstVisiblePosition()="+ listview.getFirstVisiblePosition() ); Log.d("@@@Changed", "mListView.getLastVisiblePosition()="+ listview.getLastVisiblePosition() ); }
@Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
} });
} }
|
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity">
<ListView android:id="@+id/listview" android:layout_width="match_parent" android:layout_height="match_parent" />
</LinearLayout> |
참고
[1] https://github.com/codepath/android_guides/wiki/Using-an-ArrayAdapter-with-ListView
[2] http://android.amberfog.com/?p=296
[3] https://lucasr.org/2012/04/05/performance-tips-for-androids-listview/
[4] https://github.com/udacity/ud839_CustomAdapter_Example