비트맵을 사용자 인터페이스(UI)에 표시하기

이번 강좌에서는 앞서 진행된 모든 강좌들을 통합적으로 다룹니다. 백그라운드 쓰레드와 비트캡 캐시를 사용하여 동시성과 구성변화를 처리하며 어떻게 다수의 비트맵을 ViewPagerGridView 요소로 불러오는지 살핍니다.

ViewPager에 비트맵 불러오기

밀어내기 패턴은 (Swipe View Pattern) 은 이미지 갤러리에서 세부적인 뷰들을 보여주기에 매우 훌륭한 방법입니다. 이 패턴을 PagerAdapter를 이용하여 ViewPager에 적용할 수 있습니다.그러나 메모리 사용량을 낮추기 위해서는 자동으로 Fragment를 생성, 파괴하는 FragmentStatePagerAdapter 사용이 더 적절합니다.

참고: 적은 수의 이미지를 사용하고, 메모리 한계를 넘어서지 않는다는 확신이 있을 경우에는 일반 PagerAdapter또는 FragmentPagerAdapter를 사용하는것이 더 적합할 수 있습니다.

Fragment PagerAdapter에는 크게 FragmentPagerAdapter와 FragmentStatePagerAdapter가 있습니다. FragmentPagerAdapter의 경우 화면에서 Fragment를 제거하지만, Fragment 객체를 메모리에서 해제하지는 않습니다. 그로인해 더 연속적인 사용자 경험 (UX)를 만들어 낼 수 있습니다. FragmentStatePagerAdapter의 경우 화면에서 Fragment가 사라질 경우 온전히 Fragment객체를 메모리에서 해제함으로써 더 효율적인 메모리 관리가 가능하도록 합니다. 다량의 이미지를 보이는 ViewPager 사용의 경우 과한 메모리 사용을 방지하기 위해서 FragmentStatePagerAdapter를 사요하는 것이 더 권장됩니다.

아래의 ViewPager는 ImageView를 하위 객체로 가지고있습닏다. 주 액티비티가 (아마 MainActivity)VIewPager와 Adapter를 모두 가지고있습니다.

public class ImageDetailActivity extends FragmentActivity {
    public static final String EXTRA_IMAGE = "extra_image";

    private ImagePagerAdapter mAdapter;
    private ViewPager mPager;

    // ViewPager의 Adapter를 위한 이미지 세트 
    public final static Integer[] imageResIds = new Integer[] {
            R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
            R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
            R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.image_detail_pager); // 현재 액티비티에는 ViewPager만 있습니다.

        mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), imageResIds.length);
        mPager = (ViewPager) findViewById(R.id.pager);
        mPager.setAdapter(mAdapter);
    }

    public static class ImagePagerAdapter extends FragmentStatePagerAdapter {
        private final int mSize;

        public ImagePagerAdapter(FragmentManager fm, int size) {
            super(fm);
            mSize = size;
        }

        @Override
        public int getCount() {
            return mSize;
        }

        @Override
        public Fragment getItem(int position) {
            return ImageDetailFragment.newInstance(position);
        }
    }
}

아래의 소스코드는 ImageView를 가지고 있는 Fragment의 세부사항입니다. 이것은 아마도 완벽한 접근법으로 생각되겠습니다만, 적용에 있어서 단점이 있습니다. 보이시나요? 이를 어떻게 개선할 수 있을까요?

public class ImageDetailFragment extends Fragment {
    private static final String IMAGE_DATA_EXTRA = "resId";
    private int mImageNum;
    private ImageView mImageView;

    static ImageDetailFragment newInstance(int imageNum) {
        final ImageDetailFragment f = new ImageDetailFragment();
        final Bundle args = new Bundle();
        args.putInt(IMAGE_DATA_EXTRA, imageNum);
        f.setArguments(args);
        return f;
    }

    public ImageDetailFragment() {}

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        // image_detail_fragment.xml contains just an ImageView
        final View v = inflater.inflate(R.layout.image_detail_fragment, container, false);
        mImageView = (ImageView) v.findViewById(R.id.imageView);
        return v;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        final int resId = ImageDetailActivity.imageResIds[mImageNum];
        mImageView.setImageResource(resId); // 이미지 뷰에 이미지 적용
    }
}

사용자 인터페이스 쓰레드에서 리소스로부터 이미지를 읽어오는 작업을 하고 있는 문제를 발견하셨길 바랍니다. 이것은 애플리케이션이 작동을 멈추게 할 수 도 있습니다. '사용자 인터페이스 쓰레드 밖에서 비트맵 처리하기'에서 다룬 비동기를(AsyncTask)를 사용하여 백그라운드 쓰레드에서 이미지 불러오겠습니다.

public class ImageDetailActivity extends FragmentActivity {
    ...

    public void loadBitmap(int resId, ImageView imageView) {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }

    ... // BitmapWorkerTask 클래스를 포함합니다.

}

public class ImageDetailFragment extends Fragment {
    ...

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (ImageDetailActivity.class.isInstance(getActivity())) {
            final int resId = ImageDetailActivity.imageResIds[mImageNum];
            ((ImageDetailActivity) getActivity()).loadBitmap(resId, mImageView);
        }
    }
}

추가적인 작업을(예를 들면 네트워크에서 전달된 이미지의 크기 조정)은 BitmapWorkerTask(앞서 '사용자 인터페이스 쓰레드 밖에서 비트맵 처리하기'에서 만든 쓰레드)에서 사용자 인터페이스에 영향을 주지 않고 처리할 수 있습니다. 만약 백그라운드 쓰레드가 단지 디스크에서 바로 이미지를 불러오는 일 이상의 것을 한다면, '비트맵 캐싱하기'에서 다룬 메모리 또는 디스크 캐시를 추가하는 것이 좋습니다.

public class ImageDetailActivity extends FragmentActivity {
    ...
    private LruCache<String, Bitmap> mMemoryCache;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        ...
        // 비트맵 캐싱하기에서 다룬 메모리 캐시를 초기화합니다.
    }

    public void loadBitmap(int resId, ImageView imageView) {
        final String imageKey = String.valueOf(resId);

        final Bitmap bitmap = mMemoryCache.get(imageKey);
        if (bitmap != null) {
            mImageView.setImageBitmap(bitmap);
        } else {
        //이미 메모리 캐시에 저장되어있을 경우 해당 비트맵을 사용하고 그렇지 않을경우 새로 생성합니다.
            mImageView.setImageResource(R.drawable.image_placeholder);
            BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
            task.execute(resId);
        }
    }

    ... // 비트맵 캐싱하기에서 다룬 메모리 캐시가 들어간 BitmapWorkerTask가 들어갑니다.
}

이를 통해 반응적인 ViewPager를 최소한의 이미지 불러오기 지연과 백그라운드 처리로 사용할 수 있게 되었습니다.

GridView에 비트맵 불러오기

격자형태의 리스트(Grild list building block)는 이미지 데이터 셋을 보일 때 유용하며, GridView사용으로 이용할 수 있습니다. GridView에서는 많은 이미지들이 한 번에 화면에 나타 날 수 있으며, 사용자가 스크롤링을 할 때, 이에 맞추어 나타날 준비를 할 수 있습니다. 이러한 종류의 조절 방식을 적용할 때, 사용자 인터페이스가 유연해야 하며, 잔여 메모리 관리가 이루어져야 하고, 동시성 또한 적절히 다루어져야 합니다. (GridView가 하위 뷰를 재활용하는 방식 때문입니다.)

시작하기에 앞서, ImageView를 하위뷰로 갖으며 Fragment에 들어가있는 표준 GridView를 생성합니다. 아래의 소스코드는 완벽히 합리적인 접근으로 보일 수 있습니다. 하지만 더 발전할 수 없을까요?

public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
    private ImageAdapter mAdapter;

    // 그리드 뷰 어댑터를 위한 이미지 데이터 셋입니다.
    public final static Integer[] imageResIds = new Integer[] {
            R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
            R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
            R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};

    public ImageGridFragment() {}

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mAdapter = new ImageAdapter(getActivity());
    }

    @Override
    public View onCreateView(
            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        final View v = inflater.inflate(R.layout.image_grid_fragment, container, false);
        final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
        mGridView.setAdapter(mAdapter);
        mGridView.setOnItemClickListener(this);
        return v;
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
        final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
        i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position);
        startActivity(i);
    }

    private class ImageAdapter extends BaseAdapter {
        private final Context mContext;

        public ImageAdapter(Context context) {
            super();
            mContext = context;
        }

        @Override
        public int getCount() {
            return imageResIds.length;
        }

        @Override
        public Object getItem(int position) {
            return imageResIds[position];
        }

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

        @Override
        public View getView(int position, View convertView, ViewGroup container) {
            ImageView imageView;
            if (convertView == null) { //만약 기존 데이터를 재활용되는 경우가 아니라면 새롭게 만듭니다.
                imageView = new ImageView(mContext);
                imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
                imageView.setLayoutParams(new GridView.LayoutParams(
                        LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
            } else {
                imageView = (ImageView) convertView;
            }
            imageView.setImageResource(imageResIds[position]); // ImageView에 이미지를 적용합니다.
            return imageView;
        }
    }
}

이러한 적용은 사용자 인터페이스 (UI)에서 이루어지고있다는 문제가 있습니다.작거나 간단한 이미지일 경우 잘 작동할 수 있으나(시스템 리소스를 불러오고 캐싱해야 하기에), 만약 어떠한 추가적인 과정이 이루어져야 한다면, 사용자 인터페이스는 멈추게 됩니다.

위에서 사용한 비동시 처리과정과 캐싱 방법이 다시 한번 적용될 수 있습니다. 그러나, GridView가 하위 뷰들을 재활용하는 것과 관련하여 야기될 수 있는 동시성 문제를 조심해야합니다. 이를 다루기 위해서, 사용자 인터페이스(UI)쓰레드 밖에서 비트맵 처리하기 강좌에서 다룬 기술을 적용할 수 있습니다. 아래는 이러한 문제를 해결한 소스코드입니다:

public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
    ...

    private class ImageAdapter extends BaseAdapter {
        ...

        @Override
        public View getView(int position, View convertView, ViewGroup container) {
            ...
            loadBitmap(imageResIds[position], imageView)
            return imageView;
        }
    }

    public void loadBitmap(int resId, ImageView imageView) {
        if (cancelPotentialWork(resId, imageView)) {
            final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
            final AsyncDrawable asyncDrawable =
                    new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
            imageView.setImageDrawable(asyncDrawable);
            task.execute(resId);
        }
    }

    static class AsyncDrawable extends BitmapDrawable {
        private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

        public AsyncDrawable(Resources res, Bitmap bitmap,
                BitmapWorkerTask bitmapWorkerTask) {
            super(res, bitmap);
            bitmapWorkerTaskReference =
                new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
        }

        public BitmapWorkerTask getBitmapWorkerTask() {
            return bitmapWorkerTaskReference.get();
        }
    }

    public static boolean cancelPotentialWork(int data, ImageView imageView) {
        final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

        if (bitmapWorkerTask != null) {
            final int bitmapData = bitmapWorkerTask.data;
            //앞서 진행되는 과정과 비교한 후 적절히 처리합니다. 자세한 사항은 사용자 인터페이스 쓰레드 밖에서 비트맵 처리하기를 
            //참고해 주십시오
            if (bitmapData != data) {
                bitmapWorkerTask.cancel(true);
            } else {
                return false;
            }
        }
        // 사전에 진행되던 작업이 없다면 계속 진행합니다.
        return true;
    }

    private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
       if (imageView != null) {
           final Drawable drawable = imageView.getDrawable();
           if (drawable instanceof AsyncDrawable) {
               final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
               return asyncDrawable.getBitmapWorkerTask();
           }
        }
        return null;
    }

    ... // 향상된 BitmapWorkerTast를 적용합니다.

이것으로 사용자 인터페이스의 부드러운 흐름을 방해하지 않으면서 이미지를 불러오고 처리하는 유연성을 확보할 수 있습니다. 백그라운드 작업에서 네트워크에서 이미지를 불러오거나, 디지털 카메라 사진의 크기를 재조정하고 작업이 완료었을 때 이미지를 보일 수 있습니다.

완벽한 예제는 별첨된 샘플 애플리케이션을 확인해 주십시오. 샘플 다운로드

results matching ""

    No results matching ""