들어가면서 |
안녕하세요. Bucket Place의 모바일 개발자 Cloud Travel입니다.
오늘은 현재까지 또는 앞으로도 안드로이드 어플을 개발할 때 가장 많이 해야할 작업인 Custom ListView를 만드는 방법에 대해서 알아봅시다. 더 나아가 Tag를 사용하여 각각의 리스트마다의 데이터를 가져와 행동을 할 수 있게 정의 하는 방법을 소개하겠습니다. 저와 함께 이 글을 끝까지 보신다면, 여러 분도 Custom ListView를 자유자제로 사용이 가능할 것이라고 굳게 믿습니다. ^^~ 그럼 시작해볼까요?
목표 |
오늘의 목표는 다음의 화면으로 구성되는 리스트뷰 만드는 것입니다.
위에서 통화 버튼(오른쪽)을 클릭시에 리스트에 입력된 전화번화를 Toast로 띄어 주는 것이 목표입니다.
Custom ListView의 제작단계는 다음과 같습니다.
1. Custom ListView Layout 정의하기
2. ListView의 한 Item의 정보를 저장할 Class 정의하기
3. ListView를 그려 줄 Adapter 정의하기
4. 사용하기
순차적으로 전행해보도록 합시다.
레이아웃 정의하기 |
자신이 머릿속으로 생각한 리스트뷰의 한 아이템의 레이아웃을 res/layout 위치에 정의하여 작성하면 됩니다.
레이아웃을 작성할 때, Default 값을 미리 넣어 주는 것이 좋습니다. '-'
저의 경우는 목표와 같은 리스트 아이탬 레이아웃을 만들기 위해서 list_item_layout.xml 을 생성하여 작성하였습니다.
list_item_layout.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp"> <ImageView android:id="@+id/user_icon" android:layout_width="60dp" android:layout_height="60dp" android:layout_alignParentLeft="true" android:background="@drawable/default_people" android:contentDescription="@string/icon" /> <LinearLayout android:layout_width="wrap_content" android:layout_height="60dp" android:layout_toRightOf="@id/user_icon" android:layout_marginLeft="10dp" android:orientation="vertical"> <TextView android:id="@+id/user_name" android:layout_width="match_parent" android:layout_height="0dp" android:gravity="center_vertical" android:layout_weight="1" android:textSize="18sp" /> <TextView android:id="@+id/user_phone_number" android:layout_width="match_parent" android:layout_height="0dp" android:gravity="center_vertical" android:layout_weight="1" android:textSize="18sp" /> </LinearLayout> <ImageButton android:id="@+id/btn_send" android:layout_width="40dp" android:layout_height="40dp" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:background="@drawable/btn_send" android:contentDescription="@string/send" /> </RelativeLayout>
Item 정보를 저장 클래스 생성 |
이제 리스트뷰의 아이템 하나하나의 정보를 저장할 클래스를 src/[PackageName] 에 생성해 주면 됩니다. 현재의 목표를 이루기 위해서 리스트뷰에 가져와야 할 정보들은 사용자아이콘, 이름 그리고 전화번호가 있습니다. 이를 저장할 하나의 클래스를 생성해주면 됩니다. 이 클래스는 정보를 저장(Set)할 생성자와 정보를 가져올 Getter를 만들어 주셔야 합니다.
저는 목표를 이루기 위해서 User.java 를 생성하였습니다.
public class User { private Drawable mUserIcon; private String mUserName; private String mUserPhoneNumber; User(Drawable userIcon, String userName, String userPhoneNumber){ mUserIcon = userIcon; mUserName = userName; mUserPhoneNumber = userPhoneNumber; } public Drawable getUserIcon() { return mUserIcon; } public String getUserName() { return mUserName; } public String getUserPhoneNumber() { return mUserPhoneNumber; } }
Adapter 생성하기 |
Adapter에는 "맞추다. 맞추어 붙이다" 라는 뜻이 있습니다. 즉, 지금 생성할 Adpater는 ListView에 생성한 Layout을 맞추어 붙여주는 녀석이라고 생각하면 됩니다. Adapter의 행동은 다음과 같습니다.
1. 리스트의 한 아이템에 대한 정보를 가지고 있는 클레스 인스턴스를 받아옵니다.
2. inflater를 사용하여 미리 정의해둔 하나의 아이템 레이아웃을 가져 옵니다.
3. "1"에서 가져온 객체의 정보를 "2"에서 가져온 레이아웃에 넣어 줍니다.4. 뷰를 반환해준다.
Adapter 클래스를 src/[PackageName] 에 생성해줍니다. Adapter를 생성하기 위해서는 적합한 adapter를 상속받아야 합니다. 생성자를 생성해주시고, getView함수를 작성해주면 됩니다. BaseAdapter를 사용한다면 데이터를 저장할 배열을 하나 만들고 배열에 정보를 저장하는 함수를 생성합니다. 또 자동생성된 함수의 몇몇 부분을 체워 줍니다. 소스코드에 자세한 주석을 달았습니다. 확인해주세요^^.
목표를 향해서 UserAdapter.java 클래스를 생성하였습니다.
/** * UserAdapter - Custom ListView를 구현하기 위해 하나의 아이템 정보와 레이아웃을 가져와서 합친다. * * @author Cloud Travel */ public class UserAdapter extends BaseAdapter implements OnClickListener{ // Activity에서 가져온 객체정보를 저장할 변수 private User mUser; private Context mContext; // ListView 내부 View들을 가르킬 변수들 private ImageView imgUserIcon; private TextView tvUserName; private TextView tvUserPhoneNumber; private ImageButton btnSend; // 리스트 아이템 데이터를 저장할 배열 private ArrayList<User> mUserData; public UserAdapter(Context context) { super(); mContext = context; mUserData = new ArrayList<User>(); } @Override /** * @return 아이템의 총 개수를 반환 */ public int getCount() { // TODO Auto-generated method stub return mUserData.size(); } @Override /** * @return 선택된 아이템을 반환 */ public User getItem(int position) { // TODO Auto-generated method stub return mUserData.get(position); } @Override public long getItemId(int position) { // TODO Auto-generated method stub return 0; } @Override /** * getView * * @param position - 현재 몇 번째로 아이템이 추가되고 있는지 정보를 갖고 있다. * @param convertView - 현재 사용되고 있는 어떤 레이아웃을 가지고 있는지 정보를 갖고 있다. * @param parent - 현재 뷰의 부모를 지칭하지만 특별히 사용되지는 않는다. * @return 리스트 아이템이 저장된 convertView */ public View getView(int position, View convertView, ViewGroup parent) { // TODO Auto-generated method stub View v = convertView; // 리스트 아이템이 새로 추가될 경우에는 v가 null값이다. // view는 어느 정도 생성된 뒤에는 재사용이 일어나기 때문에 효율을 위해서 해준다. if (v == null) { // inflater를 이용하여 사용할 레이아웃을 가져옵니다. v = ((LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE)) .inflate(R.layout.list_item_layout, null); } // 레이아웃이 메모리에 올라왔기 때문에 이를 이용하여 포함된 뷰들을 참조할 수 있습니다. imgUserIcon = (ImageView) v.findViewById(R.id.user_icon); tvUserName = (TextView) v.findViewById(R.id.user_name); tvUserPhoneNumber = (TextView) v.findViewById(R.id.user_phone_number); btnSend = (ImageButton) v.findViewById(R.id.btn_send); // 받아온 position 값을 이용하여 배열에서 아이템을 가져온다. mUser = getItem(position); // 데이터의 실존 여부를 판별합니다. if ( mUser != null ){ // 데이터가 있다면 갖고 있는 정보를 뷰에 알맞게 배치시킵니다. if ( mUser.getUserIcon() != null ){ imgUserIcon.setImageDrawable(mUser.getUserIcon()); } tvUserName.setText(mUser.getUserName()); tvUserPhoneNumber.setText(mUser.getUserPhoneNumber()); btnSend.setOnClickListener(this); } // 완성된 아이템 뷰를 반환합니다. return v; } // 데이터를 추가하는 것을 위해서 만들어 준다. public void add(User user){ mUserData.add(user); } @Override public void onClick(View v) { // TODO Auto-generated method stub switch (v.getId()){ case R.id.btn_send: Toast.makeText(mContext, tvUserPhoneNumber.getText(), Toast.LENGTH_SHORT).show(); break; } } }
사용하기 |
자 이제 필요한 작업이 모두 끝났습니다. 이제 사용하기만 하면 됩니다. 사용법은 다음과 같습니다. 사용할 Activity 또는 fragment의 레이아웃에 리스트뷰 하나를 추가시켜 줍니다.
저의 경우에는 activity_main.xml 에 리스트뷰를 추가하였습니다.
<RelativeLayout 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" > <ListView android:id="@+id/user_list" android:layout_width="match_parent" android:layout_height="match_parent" android:text="@string/hello_world" /> </RelativeLayout>
다음으로는 Activity 또는 Fragment에서 다음의 과정을 해주시면 됩니다.
1. Adapter 생성
2. ListView 참조 후 Adapter 붙이기
3. 데이터 추가
4. 데이터 변경이 끝났다는 것을 Adapter에게 알려준다.
저는 MainActivity.java를 다음과 같이 작성하였습니다.
public class MainActivity extends Activity { private ListView userList; private UserAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Adapter 생성 adapter = new UserAdapter(getApplicationContext()); // 리스트뷰 참조 및 Adapter달기 userList = (ListView) findViewById(R.id.user_list); userList.setAdapter(adapter); // Data 추가 User u1 = new User(getResources().getDrawable(R.drawable.test_user_icon1), "김씨", "010-1234-5678"); adapter.add(u1); User u2 = new User(getResources().getDrawable(R.drawable.test_user_icon2), "이씨", "010-8765-4321"); adapter.add(u2); User u3 = new User(getResources().getDrawable(R.drawable.test_user_icon3), "박씨", "010-0000-0000"); adapter.add(u3); // Data가 변경 되있음을 알려준다. adapter.notifyDataSetChanged(); } }
다음은 실행 결과 입니다.
무엇인가 이상한 점이 있지 않습니까? 아이템들의 어떠한 버튼을 클릭해도 맨마지막에 추가된 아이템에 대해서만 실행이 됩니다. 이는 생각해보면 자연스러운 일입니다. getView에 의해서 위젯이 계속 덮어 씌어지기 때문입니다. 이를 방지하기 위해선 어떻게 할까요?
Tag 사용 |
위와 같은 현상을 방지하기 위해서는 Tag사용이 필요합니다. Tag의 사용처는 무궁무진하지만, Custom ListView 입장에서는 다음과 같은 역할을 해줍니다.
1. 지속적으로 생성되는 데이터를 뷰와 함께 Tag를 이용하여 저장시킨다. (getView() - setTag)
2. 리스너 등 반응이 필요한 지점에서 뷰와 함께 Tag를 이용하여 해당 데이터를 가져와 사용한다. (Listener - getTag)
말로 풀어 쓴다면 다음과 같습니다.
각 리스트 아이템의 뷰에 대한 이벤트 처리시, 아이템을 추가할 때 사용한 데이터가 필요로 한다면 setTag를 이용하여 뷰에 데이터를 링크시킨다. 이벤트가 발생한다면, getTag()를 이용하여 링크된 데이터를 가져와 사용한다.
바뀐 UserAdapter 클래스를 보도록 합시다. ( 변경된 라인 : 81~82 / 107~108 / 112 )
/** * UserAdapter - Custom ListView를 구현하기 위해 하나의 아이템 정보와 레이아웃을 가져와서 합친다. * * @author Cloud Travel */ public class UserAdapter extends BaseAdapter implements OnClickListener{ // Activity에서 가져온 객체정보를 저장할 변수 private User mUser; private Context mContext; // ListView 내부 View들을 가르킬 변수들 private ImageView imgUserIcon; private TextView tvUserName; private TextView tvUserPhoneNumber; private ImageButton btnSend; // 리스트 아이템 데이터를 저장할 배열 private ArrayList<User> mUserData; public UserAdapter(Context context) { super(); mContext = context; mUserData = new ArrayList<User>(); } @Override /** * @return 아이템의 총 개수를 반환 */ public int getCount() { // TODO Auto-generated method stub return mUserData.size(); } @Override /** * @return 선택된 아이템을 반환 */ public User getItem(int position) { // TODO Auto-generated method stub return mUserData.get(position); } @Override public long getItemId(int position) { // TODO Auto-generated method stub return 0; } @Override /** * getView * * @param position - 현재 몇 번째로 아이템이 추가되고 있는지 정보를 갖고 있다. * @param convertView - 현재 사용되고 있는 어떤 레이아웃을 가지고 있는지 정보를 갖고 있다. * @param parent - 현재 뷰의 부모를 지칭하지만 특별히 사용되지는 않는다. * @return 리스트 아이템이 저장된 convertView */ public View getView(int position, View convertView, ViewGroup parent) { // TODO Auto-generated method stub View v = convertView; // 리스트 아이템이 새로 추가될 경우에는 v가 null값이다. // view는 어느 정도 생성된 뒤에는 재사용이 일어나기 때문에 효율을 위해서 해준다. if (v == null) { // inflater를 이용하여 사용할 레이아웃을 가져옵니다. v = ((LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE)) .inflate(R.layout.list_item_layout, null); // 레이아웃이 메모리에 올라왔기 때문에 이를 이용하여 포함된 뷰들을 참조할 수 있습니다. imgUserIcon = (ImageView) v.findViewById(R.id.user_icon); tvUserName = (TextView) v.findViewById(R.id.user_name); tvUserPhoneNumber = (TextView) v.findViewById(R.id.user_phone_number); btnSend = (ImageButton) v.findViewById(R.id.btn_send); } // 받아온 position 값을 이용하여 배열에서 아이템을 가져온다. mUser = getItem(position); // Tag를 이용하여 데이터와 뷰를 묶습니다. btnSend.setTag(mUser); // 데이터의 실존 여부를 판별합니다. if ( mUser != null ){ // 데이터가 있다면 갖고 있는 정보를 뷰에 알맞게 배치시킵니다. if ( mUser.getUserIcon() != null ){ imgUserIcon.setImageDrawable(mUser.getUserIcon()); } tvUserName.setText(mUser.getUserName()); tvUserPhoneNumber.setText(mUser.getUserPhoneNumber()); btnSend.setOnClickListener(this); } // 완성된 아이템 뷰를 반환합니다. return v; } // 데이터를 추가하는 것을 위해서 만들어 준다. public void add(User user){ mUserData.add(user); } @Override public void onClick(View v) { // TODO Auto-generated method stub // Tag를 이용하여 Data를 가져옵니다. User clickItem = (User)v.getTag(); switch (v.getId()){ case R.id.btn_send: Toast.makeText(mContext, clickItem.getUserPhoneNumber(), Toast.LENGTH_SHORT).show(); break; } } }
이제 실행을 해보면 원하는 데로 잘 작동하는 것을 볼 수 있습니다.
마무리 |
리스트뷰 사용에 있어 80%의 활용법을 모두 보여 준 것 같습니다. setTag와 getTag는 소스코드 분석을 하여서 꼭 마스터 하기를 바랍니다. 만약, 저의 설명이 부족했다면 댓글 또는 방명록을 이용하여 질문을 주시거나 여러 사이트를 참조하길 바랍니다.
리스트뷰에서 다루지 않은 이야기가 하나 있다면 Holder를 사용하는 것입니다. Holder는 getView사용시 메모리 사용에 효율을 제공해 줍니다. 저도 아직 확신하면서 사용하는 부분이 아니기 때문에 설명은 아직 하지 않으려고 합니다. 여러가지 Test를 더 한뒤에 해볼 수 있겠죠...?
마무리로 목표를 위해 생성된 완성된 소스코드를 첨부해 드립니다.
많은 공부하시고 질문은 댓글과 방명록으로 받겠습니다.