Android 복수 선택이 가능한 갤러리를 만들자 3

안녕하세요 지헌입니다.
이번시간에는 복수선택 갤러리의 속도를 개선시켜 보겠습니다.

지난시간까지 우리는 슬로어댑터 기법을 사용해 스크롤시에 버벅임은 해결했습니다.
그러나 아직도 이미지를 선택할 때의 느린 속도는 그대로였죠
이것은 이미지를 선택할때마다 체크박스의 처리를 위해
화면을 다시 그려줘야 하고 그럴 경우 이미지 어댑터의 getView()에서
매번 실제 파일을 불러와 비트맵 객체를 만들어야만 했기 때문에 그렇습니다.

이번에는 이 선택하는 과정에서의 로직을 개선해서 선택 동작의 속도를
개선해보도록 하겠습니다.

속도 개선을 위해 가장 쉽게 생각 할 수 있는 것이 캐쉬의 사용입니다.
매번 실제 파일을 읽어서 비트맵 객체를 만들지 않고 현재 보이는 화면의
이미지들을 캐쉬에 담아두고 이것을 이용한다면 훨씬 속도가 빨라질것입니다.

캐쉬 로직은 사실 여러가지가 있겠습니다만 이번에는 가장 널리 사용되고
있는 로직중 하나인 LRU캐쉬를 사용하겠습니다.

LRU 는 Least Recently Used 의 약자로서 캐쉬에서 제거할 대상을 선택하는
기준이 최근에 가장 적게 사용된 항목 이라는 뜻입니다.
즉 이넘의 동작 방법은 캐쉬의 내용이 히트 될때마다 히트된 내용을 최 상단
(맨앞)으로 이동시켜주고 새로운 항목이 캐쉬에 들어오면 캐쉬유지의 기준값
보다 더 많아질 경우 가장 적게 사용된 내용을 제거해 주는 기법입니다.

이 캐쉬 로직의 구현도 뭐 복잡하고 거창하게 할수도 있겠지만
인터넷에서 돌아다니는 간단하고 명료하게 구현된 좋은 예제를 구해서
적용할 수 있었습니다.

그래서 이번 포스팅에서는
LRUCache.java --> 새로 생성된 클래스
Main.java --> 수정할 클래스

이렇게 2개의 파일을 다루게 될것입니다.

먼저 캐쉬 클래스입니다.
******************************** LRUCache.java *********************************
package com.jeehun.android.mygallery;


import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

/-*
 *
 * @author Sun Jee HUn
 *
 * @param <K>
 * @param <V>
 *-
public class LRUCache<K, V>
{
  private static final float mHashTableLoadFactor = 0.75f;
  private LinkedHashMap<K, V> mMap;
  private int mCacheSize;

  public LRUCache(int cacheSize)
  {
    this.mCacheSize = cacheSize;
    int hashTableCapacity = (int) Math.ceil(cacheSize / mHashTableLoadFactor) + 1;
    mMap = new LinkedHashMap<K, V>(hashTableCapacity, mHashTableLoadFactor, true)
    {
      private static final long serialVersionUID = 1;

      @Override
      protected boolean removeEldestEntry(Map.Entry<K, V> eldest)
      {
        return size() > LRUCache.this.mCacheSize;
      }
    };
  }

  public synchronized boolean containKey(String key)
  {
    return mMap.containsKey(key);
  }
 
  public synchronized V get(K key)
  {
    return mMap.get(key);
  }

  public synchronized void put(K key, V value)
  {
    mMap.put(key, value);
  }

  public synchronized int usedEntries()
  {
    return mMap.size();
  }

  public synchronized Collection<Map.Entry<K, V>> getAll()
  {
    return new ArrayList<Map.Entry<K, V>>(mMap.entrySet());
  }
}
*********************************************************************************

다음은 Main.java 클래스의 수정입니다.
크게 수정된 부분은 없구요
LRUCache 클래스를 사용하고 있고 이를 이용해 getView()에서 먼저 캐쉬에서
Bitmap 객체를 불러오고 있으면 이를 사용하고 없으면 실제 파일을 불러서
Bitmap 객체를 만들어 뿌리고 이 객체를 캐쉬에 넣어주고 있습니다.
********************************* Main.java **************************************
package com.jeehun.android.mygallery;

import java.io.File;
import java.util.ArrayList;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.AbsListView.OnScrollListener;

public class Main extends Activity implements ListView.OnScrollListener, GridView.OnItemClickListener
{
  boolean mBusy = false;
  ProgressDialog mLoagindDialog;
  GridView mGvImageList;
  ImageAdapter mListAdapter;
  ArrayList<ThumbImageInfo> mThumbImageInfoList;
 
  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.image_list_view);
   
    mThumbImageInfoList = new ArrayList<ThumbImageInfo>();
    mGvImageList = (GridView) findViewById(R.id.gvImageList);
    mGvImageList.setOnScrollListener(this);
    mGvImageList.setOnItemClickListener(this);
   
    new DoFindImageList().execute();
  }
 
  private long findThumbList()
  {
    long returnValue = 0;
   
    // Select 하고자 하는 컬럼
    String[] projection = { MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA };
   
    // 쿼리 수행
    Cursor imageCursor = managedQuery(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, MediaStore.Images.Media.DATE_ADDED + " desc ");

    if (imageCursor != null && imageCursor.getCount() > 0)
    {
      // 컬럼 인덱스
      int imageIDCol = imageCursor.getColumnIndex(MediaStore.Images.Media._ID);
      int imageDataCol = imageCursor.getColumnIndex(MediaStore.Images.Media.DATA);

      // 커서에서 이미지의 ID와 경로명을 가져와서 ThumbImageInfo 모델 클래스를 생성해서
      // 리스트에 더해준다.
      while (imageCursor.moveToNext())
      {
        ThumbImageInfo thumbInfo = new ThumbImageInfo();

        thumbInfo.setId(imageCursor.getString(imageIDCol));
        thumbInfo.setData(imageCursor.getString(imageDataCol));
        thumbInfo.setCheckedState(false);
       
        mThumbImageInfoList.add(thumbInfo);
        returnValue++;
      }
    }
    imageCursor.close();
    return returnValue;
  }
 
  // 화면에 이미지들을 뿌려준다.
  private void updateUI()
  {
    mListAdapter = new ImageAdapter (this, R.layout.image_cell, mThumbImageInfoList);
    mGvImageList.setAdapter(mListAdapter);
  }
 
  public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
  {}
 
  // 스크롤 상태를 판단한다.
  // 스크롤 상태가 IDLE 인 경우(mBusy == false)에만 이미지 어댑터의 getView에서
  // 이미지들을 출력한다.
  public void onScrollStateChanged(AbsListView view, int scrollState)
  {
    switch (scrollState)
    {
      case OnScrollListener.SCROLL_STATE_IDLE:
        mBusy = false;
        mListAdapter.notifyDataSetChanged();
        break;
      case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
        mBusy = true;
        break;
      case OnScrollListener.SCROLL_STATE_FLING:
        mBusy = true;
        break;
    }
  }
 
  // 아이템 체크시 현재 체크상태를 가져와서 반대로 변경(true -> false, false -> true)시키고
  // 그 결과를 다시 ArrayList의 같은 위치에 담아준다
  // 그리고 어댑터의 notifyDataSetChanged() 메서드를 호출하면 리스트가 현재 보이는
  // 부분의 화면을 다시 그리기 시작하는데(getView 호출) 이러면서 변경된 체크상태를
  // 파악하여 체크박스에 체크/언체크를 처리한다.
  @Override
  public void onItemClick(AdapterView<?> arg0, View arg1, int position, long arg3)
  {
    ImageAdapter adapter = (ImageAdapter) arg0.getAdapter();
    ThumbImageInfo rowData = (ThumbImageInfo)adapter.getItem(position);
    boolean curCheckState = rowData.getCheckedState();
   
    rowData.setCheckedState(!curCheckState);
   
    mThumbImageInfoList.set(position, rowData);
    adapter.notifyDataSetChanged();
  }
 
  // ***************************************************************************************** //
  // Image Adapter Class
  // ***************************************************************************************** //
  static class ImageViewHolder
  {
    ImageView ivImage;
    CheckBox chkImage;
  }
 
  private class ImageAdapter extends BaseAdapter
  {
    static final int VISIBLE = 0x00000000;
    static final int INVISIBLE = 0x00000004;
    private Context mContext;
    private int mCellLayout;
    private LayoutInflater mLiInflater;
    private ArrayList<ThumbImageInfo> mThumbImageInfoList;
    @SuppressWarnings("unchecked")
    private LRUCache mCache; // 캐쉬
   
    public ImageAdapter(Context c, int cellLayout, ArrayList<ThumbImageInfo> thumbImageInfoList)
    {
      mContext = c;
      mCellLayout = cellLayout;
      mThumbImageInfoList = thumbImageInfoList;
     
      mLiInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
     
      // 캐쉬 초기화 : 캐쉬의 최대 보관 크기 30개
      mCache = new LRUCache<String, Bitmap>(30);
    }
   
    public int getCount()
    {
      return mThumbImageInfoList.size();
    }

    public Object getItem(int position)
    {
      return mThumbImageInfoList.get(position);
    }

    public long getItemId(int position)
    {
      return position;
    }
   
    @SuppressWarnings("unchecked")
    public View getView(int position, View convertView, ViewGroup parent)
    {
      if (convertView == null)
      {
        convertView = mLiInflater.inflate(mCellLayout, parent, false);
        ImageViewHolder holder = new ImageViewHolder();
       
        holder.ivImage = (ImageView) convertView.findViewById(R.id.ivImage);
        holder.chkImage = (CheckBox) convertView.findViewById(R.id.chkImage);
       
        convertView.setTag(holder);
      }

      final ImageViewHolder holder = (ImageViewHolder) convertView.getTag();
     
      if (((ThumbImageInfo) mThumbImageInfoList.get(position)).getCheckedState())
        holder.chkImage.setChecked(true);
      else
        holder.chkImage.setChecked(false);

      if (!mBusy)
      {
        try
        {
          String path = ((ThumbImageInfo) mThumbImageInfoList.get(position)).getData();
          Bitmap bmp = (Bitmap) mCache.get(path); // 일단 캐쉬에서 Bitmap 객체를 가져온다.
         
          // bmp 객체를 캐쉬에서 가져 왔다면 그것을 그대로 사용하고
          if (bmp != null)
          {
            holder.ivImage.setImageBitmap(bmp);
          }
          // 캐쉬에 내용이 없다면 실제 파일로 Bitmap객체를 만들어 주고 이를
          // 캐쉬에 넣어준다.
          else
          {
            BitmapFactory.Options option = new BitmapFactory.Options();
           
            if (new File(path).length() > 100000)
              option.inSampleSize = 10;
            else
              option.inSampleSize = 2;
           
            bmp = BitmapFactory.decodeFile(path, option);
            holder.ivImage.setImageBitmap(bmp); 
           
            mCache.put(path, bmp); // 캐쉬에 넣어준다.
          }
         
          holder.ivImage.setVisibility(VISIBLE);
          setProgressBarIndeterminateVisibility(false);
        }
        catch (Exception e)
        {
          e.printStackTrace();
          setProgressBarIndeterminateVisibility(false);
        }
      }
      else
      {
        setProgressBarIndeterminateVisibility(true);
        holder.ivImage.setVisibility(INVISIBLE);
      }
       
      return convertView;
    }
  }
  // ***************************************************************************************** //
  // Image Adapter Class End
  // ***************************************************************************************** //
 
  // ***************************************************************************************** //
  // AsyncTask Class
  // ***************************************************************************************** //
  private class DoFindImageList extends AsyncTask<String, Integer, Long>
  {
    @Override
    protected void onPreExecute()
    {
      mLoagindDialog = ProgressDialog.show(Main.this, null, "로딩중...", true, true);
      super.onPreExecute();
    }
   
    @Override
    protected Long doInBackground(String... arg0)
    {
      long returnValue = 0;
      returnValue = findThumbList();
      return returnValue;
    }

    @Override
    protected void onPostExecute(Long result)
    {
      updateUI();
      mLoagindDialog.dismiss();
    }

    @Override
    protected void onCancelled()
    {
      super.onCancelled();
    }
  }
  // ***************************************************************************************** //
  // AsyncTask Class End
  // ***************************************************************************************** //
}
*********************************************************************************

여기까지 코딩하시고 실행시키시면
결과는 지난시간의 내용과 같지만 이미지를 선택했을때 훨씬 빠르게 체크박스가
체크/언체크 되는것을 보실 수 있습니다.

한가지
mCache = new LRUCache<String, Bitmap>(30);
이부분에서 캐쉬에 최대 보관될 Bitmap 객체의 숫자를 정해주는데요
이 숫자가 너무 적으면 캐쉬의 효과가 별로 나타나지 않구요 너무 많다면
모바일 기기의 메모리 한계로 (정확히는 달빅 vm의 힙 사이즈 문제)
앱이 죽을 수도 있습니다.

테스트해보니 30정도면 적당했던것 같습니다.

전체소스 : MyGallery_03.zip

전체 소스 내용은 위의 첨부파일을 참고하시구요
다음 시간에는 마지막으로 이미지 선택의 결과를 처리하고 끝내도록 하겠습니다.

도움이 되셨다면 리플 하나씩 남겨주시면 감사드리겠습니다. ^^

by 선지헌 | 2011/07/01 00:40 | Android | 트랙백 | 덧글(24)

트랙백 주소 : http://jeehun.egloos.com/tb/4079888
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Commented at 2011/07/11 10:46
비공개 덧글입니다.
Commented by 선지헌 at 2011/07/12 19:46
API데모에 비슷한게 나와 있는것으로 압니다. API데모의 View쪽을 살펴보시기 바랍니다. 그리드뷰쪽 예제에서 그런식으로 뿌리는것 같던데요
Commented at 2012/02/04 22:07
비공개 덧글입니다.
Commented at 2012/03/26 02:41
비공개 덧글입니다.
Commented by 미니잉리 at 2013/04/24 10:27
감사합니다 ~~~~ 과제 수행하는데 도움이 되었습니다 !!!!! 복받으실꺼에요 !!
Commented by 우왕굿 at 2013/07/12 16:12
진짜 최고에요!! 감사해요!!~^^
Commented by 와우우!! at 2013/07/19 15:23
정말 많은 도움 됫네요~ 감사합니다~~!!!! ^ㅡ^
혹시 기회 되면 이미지 선택 결과도 어떻게 되는지 알고 싶네요 ㅎㅎㅎ;;
암튼 정말 잘 봤습니다~!!!
Commented at 2013/07/25 12:49
비공개 덧글입니다.
Commented at 2013/09/07 22:11
비공개 덧글입니다.
Commented at 2013/09/26 18:01
비공개 덧글입니다.
Commented at 2014/02/07 22:59
비공개 덧글입니다.
Commented by at 2014/03/03 15:19
오감사합니다!! 열심히해서 님과같은개발자가되겠습니다><
Commented at 2014/03/03 22:11
비공개 덧글입니다.
Commented by 알파브이 at 2014/03/05 01:04
안녕하세요.~~
몇일 해결이 안되는 분이 있어 여쭤봅니다.

제가 새로운 액티비티에 이미지뷰 img1/img2/img3 3를 만들었습니다.
마이 이미지 3개를 체크후에 ok버튼을 클릭하면 이미지뷰에 이미지가 보이게 하는건데 새로운 액티비티에 적용할려면 db에 저장후에 불려 와야 하는건지
아니면 putextera로 보내주고 받게 해야하는건지??

만약 두번째이면 어디에 어떻게 코딩을 해야 하는지 좀 알려 주세요.. 제가 완전 초보라 부탁드립니다.
Commented by 김흥준 at 2014/08/16 08:48
도움이 많이 되었습니다. 감사합니다.

여쭈어 볼것이 있는데 안드로이드 정복 예제의 경우에는

1. 미디어 스캐닝을 사용하는 방법을 사용하고
2. URI를 사용하여 setImageURI(uri) 로 화면에 사진을 띄우는데요

이 방법으로 했을때는 슬라이드 넘길때 늦게 뜨거나 한건없더라구요

작성자님 소스는 약간 늦게 뜨는이유로 체크박스와 이미지 홀더는 그대로 사용하고 화면에 뿌리는 방식만 바꾸려고 하는데 잘안되네요...

제가 워낙초보라서.. ㅜㅠ

Q. uri를 이용하지않고 setImgBitmap으로 그려야만 하는는 이유가 있나요??
Commented by 김흥준 at 2014/08/17 13:56
ㄴ 하다보니 속도문제는 해결됬네요.
bmp에 이미지를 저장할 때 섬네일을 getThumbnail로 얻어와서 저장했더니 속도가 빨라졌네요.

정말 많은 도움이 되었습니다. 감사합니다
Commented by 은아월 at 2014/11/13 16:53
감사합니다 !!
Commented by 빠구리 at 2015/03/12 15:35
감사 드리고 정보 많이 유익 했습니다.
Commented by 김진우 at 2015/04/23 20:11
덕분에 쉴수있겟네요. 고맙습니다.
Commented at 2015/05/27 09:09
비공개 덧글입니다.
Commented by 초짜공부중 at 2015/06/22 11:14
감사합니다. 많이 알아갑니다.~
Commented by 궁금이 at 2015/08/07 18:02
사진 파일의 이름을 가져오는 메소드가 어느것인가요??
파일 이름 가져와서 서버로 전송시키는 프로그램 개발중입니다!
Commented by ㅋㅌㅊ at 2015/10/15 11:44
혹시 알아내셧으면 헬프부탁드립니다
Commented by roro at 2016/08/25 16:19
4탄은 왜안나오나욤

:         :

:

비공개 덧글

◀ 이전 페이지다음 페이지 ▶