Tag Archives: ListView

[Android] 동적으로 다음페이지를 로딩하는 ListView 구현

아이폰의 수많은 UITableView를 활용하는 어플리케이션을 보면 참 퀄리티 높게 잘 만든것이 자동으로 리스트의 가장 아래로 내려가면 알아서 다음페이지를 로딩하는 기능이 아닐까 싶습니다. 안드로이드에서도 요즘은 많은 어플리케이션이 해당 기능을 구현하고 있습니다. 안드로이드에서는 리스트뷰와 데이터간에 Adapter라는 디자인패턴을 활용하고 있어 아이폰의 그것과는 같은 기능이라도 구현하는 방식이 다릅니다.

안드로이드에서는 좀 더 적극적으로 Adapter를 활용하여 이 기능을 구현해야 합니다. 어찌보면 조잡하고 어찌보면 더 쉽게 구현할 수 있습니다. 길게 이야기할것 없이 예제 소스를 보여드리겠습니다.

public class DynamicListViewActivity extends Activity implements OnScrollListener
{
  private static final String LOG = "DynamicListViewActivity";
  private CustomAdapter mAdapter;
  private ListView mListView;
  private LayoutInflater mInflater;
  private ArrayList<String> mRowList;
  private boolean mLockListView;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // 멤버 변수 초기화
        mRowList = new ArrayList<String>();
        mLockListView = true;

        // 어댑터와 리스트뷰 초기화
        mAdapter = new CustomAdapter(this, R.layout.row, mRowList);
        mListView = (ListView) findViewById(R.id.listView);

        // 푸터를 등록합니다. setAdapter 이전에 해야 합니다. 
        mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        mListView.addFooterView(mInflater.inflate(R.layout.footer, null));

        // 스크롤 리스너를 등록합니다. onScroll에 추가구현을 해줍니다.
        mListView.setOnScrollListener(this);
        mListView.setAdapter(mAdapter);

        // 데미데이터를 추가하기 위해 임의로 만든 메서드 호출
        addItems(50);
  }

  @Override
  public void onScroll(AbsListView view, int firstVisibleItem,
    int visibleItemCount, int totalItemCount)
  {
    // 현재 가장 처음에 보이는 셀번호와 보여지는 셀번호를 더한값이
    // 전체의 숫자와 동일해지면 가장 아래로 스크롤 되었다고 가정합니다.
    int count = totalItemCount - visibleItemCount;

    if(firstVisibleItem >= count && totalItemCount != 0
      && mLockListView == false)
    {
      Log.i(LOG, "Loading next items");
      addItems(50);
    }  
  }

  @Override
  public void onScrollStateChanged(AbsListView view, int scrollState)
  {
  }

  /**
   * 임의의 방법으로 더미 아이템을 추가합니다.
   * 
   * @param size
   */
  private void addItems(final int size)
  {
    // 아이템을 추가하는 동안 중복 요청을 방지하기 위해 락을 걸어둡니다.
    mLockListView = true;

    Runnable run = new Runnable()
    {
      @Override
      public void run()
      {
        for(int i = 0 ; i < size ; i++)
        {
          mRowList.add("Item " + i);
        }

        // 모든 데이터를 로드하여 적용하였다면 어댑터에 알리고
        // 리스트뷰의 락을 해제합니다.
        mAdapter.notifyDataSetChanged();
        mLockListView = false;
      }
    };

    // 속도의 딜레이를 구현하기 위한 꼼수
    Handler handler = new Handler();
    handler.postDelayed(run, 5000);
  }
}

여기서 주목할 부분은 onScroll 메서드 입니다. 스크롤이 일어날때마다 해당 메서드가 호출이 되며 위의 소스에서는 가장 마지막셀이 디스플레이 되었는지를 검사하게 됩니다. 마지막 셀이 나왔다면 현재 리스트가 Lock상태인지를 체크 합니다. 여기서 쓰이는 멤버 변수가 mLockListView 입니다.

해당 변수를 사용하여 리스트에 데이터가 변화하는 순간에는 스크롤 이벤트를 막아 이벤트의 중복 요청을 막게 됩니다. 위에서 Inflater를 활용하여 FooterView를 붙이는 과정이 있는데요 이것이 사용자로 하여금 페이지 로딩중임을 알리게 되는 중요한 요소입니다.

사용자 삽입 이미지

[Android] ListView 구현시에 뷰홀더(ViewHolder) 사용하기

 

대부분의 안드로이드 관련 책을 보면 ListView를 구현시에 Row를 캐시하는것에 대해 언급이 되어있습니다. 하지만 ViewHolder를 쓰는 방법에 대해서는 언급된 책이 별로 없더군요. 저도 지난번 안드로이드 개발자랩에 가서 이것의 존재를 알게 되었습니다;;

ViewHolder란, 이름 그대로 뷰들을 홀더에 꼽아놓듯이 보관하는 객체를 말합니다. 각각의 Row를 그려낼 때 그 안의 위젯들의 속성을 변경하기 위해 findViewById를 호출하는데 이것의 비용이 큰것을 줄이기 위해 사용합니다.

public class ForStudyAdapter extends BaseAdapter
{
  private Context mContext;
  private LayoutInflater mInflater;
  private ArrayList<Person> mItemList;
  private int mLayout;

  public ForStudyAdapter(Context context, int layout, ArrayList<Person> itemList)
  {
    this.mContext = context;
    this.mLayout = layout;
    this.mItemList = itemList;
    this.mInflater = (LayoutInflater) context.getSystemService
    (Context.LAYOUT_INFLATER_SERVICE);
  }

  @Override
  public int getCount()
  {
    return mItemList.size();
  }

  @Override
  public Object getItem(int position)
  {
    return mItemList.get(position);
  }

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

  @Override
  public View getView(int position, View convertView, ViewGroup parent)
  {
    PersonViewHolder viewHolder;

    // 캐시된 뷰가 없을 경우 새로 생성하고 뷰홀더를 생성한다
    if(convertView == null)
    {
        convertView = mInflater.inflate(mLayout, parent, false);

        viewHolder = new PersonViewHolder();
        viewHolder.icon = (ImageView) convertView.findViewById(R.id.iconImage);
        viewHolder.name = (TextView) convertView.findViewById(R.id.name);
        viewHolder.address = (TextView) convertView.findViewById(R.id.address);
        viewHolder.phone = (TextView) convertView.findViewById(R.id.phone);

        convertView.setTag(viewHolder);
    }
    // 캐시된 뷰가 있을 경우 저장된 뷰홀더를 사용한다
    else
    {
        viewHolder = (PersonViewHolder) convertView.getTag();
    }

    viewHolder.name.setText(mItemList.get(position).getName());
    viewHolder.address.setText(mItemList.get(position).getAddress());
    viewHolder.phone.setText(mItemList.get(position).getPhone());

    return convertView;
  }
}

위에서 ViewHolder를 구현한 부분은 getView하나만 보시면 됩니다. 생성된 viewHolder의 경우 다음과 같이 전체가 public으로 구현된 간단한 클래스 하나면 됩니다.

public class PersonViewHolder
{
  public ImageView icon;
  public TextView name;
  public TextView address;
  public TextView phone;
}

여기서 조금 특이한 점은 대부분이 맴버변수는 private으로 선언한 뒤에 getter/setter를 사용하는 방식을 취하지 않고 맴버변수에 직접적으로 접근을 한다는 점입니다. [이글]을 참고해보시면 메서드내에서 맴버변수(필드)에 접근하는것조차 상대적으로 비용이 크다는 언급이 나옵니다.

결론적으로 실행에 드는 비용을 줄일려고 ViewHolder를 사용하므로 ViewHolder내에서도 메서드 호출의 숫자까지 줄이는것이 중요해 보입니다. 결론적으로 viewHolder에서 Row내의 요소 위젯들을 직접적으로 가지고 있으므로 바로바로 값을 변경할 수 있습니다.

실제로 안드로이드 개발자랩에서 보여준 데모에서는 많은 Row를 가진 ListView라도 매우 빠르게 동작하더군요.