본문 바로가기

Android/Guide

[CustomerListView] 커스텀 리스트뷰

앱을 개발하다보면 은근히 많이 사용되는 것이 리스트이다.

리스트에 단순히 텍스트 하나 덜렁있는 경우가 없기에 대부분 커스텀 리스트뷰를 만들게 된다.


리스트뷰가 만들어져 화면에 보여지는 구조는 아래와 같다.

하나의 리스트에 보여질 정보는 단순 텍스트일 수도 있고 이미지, 버튼을 포함할 수도 있다 (Data)

리스트에 보여지기 위해 화면 layout에 data를 할당 해준다 (Adapter)

할당된 정보를 리스트에 adapter를 연결하여 리스트를 보여준다 (selection widget)


DATA                                                


Data = layout + class 로 구성된다. 만들어진 데이터를 화면에 보여줘야 하니..

아래 샘플은 유투브와 같은 형태이며, 상단에 이미지(video_thumnail_imv)와 하단에 제목(video_title_txv)과 설명(video_desc_txv)으로 구성된다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.CardView
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:padding="10dp"
android:id="@+id/video_thumbnail_imv"
android:layout_width="match_parent"
android:layout_height="240dp"
android:layout_alignParentTop="true"
android:scaleType="fitCenter"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/video_thumbnail_imv"
android:paddingLeft="30dp"
android:paddingRight="30dp"
android:orientation="vertical">
<TextView
android:id="@+id/video_title_txv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="left"
android:singleLine="true"
android:textColor="@android:color/black"
android:textStyle="bold" />
<TextView
android:id="@+id/video_desc_txv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="10dp"
android:paddingTop="5dp"
android:gravity="left"
android:text="Video Description"
android:maxLines="2"
android:ellipsize="end"
android:textColor="@android:color/darker_gray" />
</LinearLayout>
</RelativeLayout>
</android.support.v7.widget.CardView>
</LinearLayout>

위 레이아웃에 보여질 정보클래스는 아래처럼 이미지URL, 타이블, 설명과 화면에는 보이지 않지만 key가 되는 ID를 가지고 있으며

나머지는 Getter, Setter이다.

public class YoutubeResult {
private String kind;
private String videoId;
private String title;
private String description;
private String thumbnailURL;

public YoutubeResult() {
}

public String getKind() {
return kind;
}

public String getVideoId() {
return videoId;
}

public String getTitle() {
return title;
}

public String getDescription() {
return description;
}

public String getThumbnailURL() {
return thumbnailURL;
}

public void setKind(String kind) {
this.kind = kind;
}

public void setVideoId(String videoId) {
this.videoId = videoId;
}

public void setTitle(String title) {
this.title = title;
}

public void setDescription(String description) {
this.description = description;
}

public void setThumbnailURL(String thumbnailURL) {
this.thumbnailURL = thumbnailURL;
}
}



ADAPTER                                          


BaseAdapter를 상속받아 사용하여 여기서의 핵심은 getView 함수와 그 안에 ViewHolder 이다.

리스트는 위에서 만든 하나의 아이템(레이아웃)을 아래로 이어서 주르륵 보여주는 개념이다.


화면을 넘기면 첫번째 아이템이 사라지고 새로운 아이템이 맨 아래에 보여진다. 

이 부분이 중요한데 성능을 위해 안드로이드는 화면에서 사라진 아이템을 재사용하는 매커니즘을 가진다.

그리고 이 작업을 해주는 것이 getView이고 좀 더 나은 성능을 위해 ViewHolder를 사용한다. ViewHolder는 좀 더 아래에서 다시 설명한다.

아래 로직에서 getView를 보면 파라메터중 convertView가 널인지를 체크한다. (위 그림참조)

재사용할 뷰가 있다는 것은 아이템(레이아웃)이 존재하므로 그림과 내용만 갈아끼운다.

재사용뷰가 없다면 inflate 명령을 통해 신규 레이아웃을 가져오고 각 widget을 선언하고 생성해준다.


그 다음은 YoutubeResult 담겨진 정보를 이용하여 그림과 정보를 셋팅하고 convertView를 리턴한다

이러면 사용하는 리스트뷰는 리턴된 convertView를 화면에 보여줌으로서 신규 아이템이 보이게 될 것이다.


참고로 아래소스에서 이미지를 셋팅할때 Picasso 라이브러리를 사용했다. 이런 라이브러리도 나중에 설명을 ㅎ

public class YoutubeAdapter extends BaseAdapter {
private Context mContext = null;
private List<YoutubeResult> mVideoList = null;
private LayoutInflater mLayoutInflater = null;

public YoutubeAdapter(Context context) {
mContext = context;
mLayoutInflater = LayoutInflater.from(mContext);
}

public void setmVideoList(List<YoutubeResult> mVideoList) {
this.mVideoList = mVideoList;
}

@Override
public int getCount() {
return (mVideoList==null)?(0):(mVideoList.size());
}

@Override
public Object getItem(int i) {
return (mVideoList!=null && mVideoList.size()>i)?(mVideoList.get(i)):(null);
}

@Override
public long getItemId(int i) {
return i;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder mHolder;
if (convertView != null) {
mHolder = (ViewHolder)convertView.getTag();
} else {
mHolder = new ViewHolder();
convertView = mLayoutInflater.inflate(R.layout.view_video_item, null);
mHolder.mVideoThumbnail = (ImageView)convertView.findViewById(R.id.video_thumbnail_imv);
mHolder.mVideoTitleTxv = (TextView)convertView.findViewById(R.id.video_title_txv);
mHolder.mVideoDescTxv = (TextView)convertView.findViewById(R.id.video_desc_txv);
convertView.setTag(mHolder);
}

// Set the data
YoutubeResult result = mVideoList.get(position);
mHolder.mVideoTitleTxv.setText(result.getTitle());
mHolder.mVideoDescTxv.setText(result.getDescription());

//Load images
Picasso.with(mContext).load(result.getThumbnailURL()).into(mHolder.mVideoThumbnail);

return convertView;
}

private class ViewHolder {
private TextView mVideoTitleTxv = null;
private TextView mVideoDescTxv = null;
private ImageView mVideoThumbnail = null;
}
}



여기서 잠깐 성능에 대해 얘기하자면, inflate 매커니즘은 생각보다 많은 메모리를 사용하기에 속도에 엄청 영향을 준다.


convertView를 재사용하지 않고 언제나 inflate로 신규 생성해도 된다.

사용자가 화면에서 리스트를 스크롤 하면 매번 새롭게 생성될 것이고 데이터가 많을수록 말도 안되고 버벅거림을 경험할 수 있다.

결국 ANR을 발생하면서 앱이 다이할 것이다.


누군가가 좀 더 성능을 향상시킬 방법이 없을까를 고민한 것이 ViewHolder이다. 

"매번 아이템에서 사용될 화면 widget들을 findViewById를 해야 하나? 이미 존재한다면 있는 것을 사용하자 메모리를 위해.."


또 누군가가 더 좋은 생각을 한다면 새로운 패턴이 나오것지...ㅎ



LISTVIEW                                          


만들어진 data, adapter를 이용하여 리스트뷰에 보이게 해보자.

getView를 보면 YoutubeResult List 데이터가 먼저 만들어져야 한다. 해당 데이터가 고정된 값이라면 사전에 할당을 해놓아도 되고

네트워크를 통해 만들어져야 하는 데이터라면 AsyncTask를 사용하게 될 것이다.


나 또한 아래처럼 YoutubeAsyncTask를 통해 데이터를 로딩하고 완료되면 adapter의 setVidoeList 함수를 이용하여 데이터를 생성한다.

이러면 이제 getView에서 position에 맞게 데이터를 가져올 수 있을 것이다.


그 다음은 메인레이아웃에 선언한 listview에 adapter를 setAdapter 함수를 통해 할당한다.

adapter가 이미 존재한다면 notifyDataSetChaged()를 통해 리프레쉬를 시켜준다.


참고로 마지막 줄은 smoothScrollToPostion(0)는 리스트를 갱신한 후 자연스럽게 스르르륵 리스트를 맨 상단으로 움직여준다.

mYtServiceTask = new YoutubeAsyncTask(mTrendSearch, new AsyncTaskCallback() {
@Override
public void onSuccess(List<YoutubeResult> result) {
if (mLoadingDialog.isShowing()){
mLoadingDialog.dismiss();
}
if (result != null) {
if (mYtAdapter == null) {
mYtAdapter = new YoutubeAdapter(getApplicationContext());
mYtAdapter.setmVideoList(result);
mYtVideoLsv.setAdapter(mYtAdapter);
} else {
mYtAdapter.setmVideoList(result);
mYtAdapter.notifyDataSetChanged();
}
mYtVideoLsv.smoothScrollToPosition(0); //move to top
}
}
});
mYtServiceTask.execute();
<ListView
android:id="@+id/yt_video_lsv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:dividerHeight="1dp">
</ListView>