【Android Developers Training】 60. 在你的UI中显示位图

注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好。

原文链接:http://developer.android.com/training/displaying-bitmaps/display-bitmap.html


这节课程将结合之前所有课程所学习的知识,向你展示如何使用后台线程和位图缓存,在ViewPagerGridView中展示多幅图片,并解决并发和配置变更的问题。


一). 实现向一个ViewPager加载位图

该滑动视图模式(swipe view pattern)用来作为图库的阅览室再好不过的了。你可以通过PagerAdapter来支持ViewPager实现这个阅览模式。然而, 一个更合适的支持适配器是其子类FragmentStatePagerAdapter,它自动销毁ViewPager中的Fragments,并保存其状态(当它从屏幕消失时),从而保持内存使用不会太高。

Note:

如果你有更小数量的图片,并确信他们能满足应用的内存限制,那么使用平常的PagerAdapterFragmentPagerAdapter会更合适一些。

下面是一个具有ImageViewViewPager实现。主activity持有ViewPager和它的适配器:

public class ImageDetailActivity extends FragmentActivity {

    public static final String EXTRA_IMAGE = "extra_image";



    private ImagePagerAdapter mAdapter;

    private ViewPager mPager;



    // A static dataset to back the 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); // Contains just a 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);

        }

    }

}

之后是详细Fragment的实现,它持有子ImageView。这看起来像是一个完美的实现,但是从中你能看出缺陷来吗?有什么办法可以改进它?

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;

    }



    // Empty constructor, required as per Fragment docs

    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); // Load image into ImageView

    }

}

很好,你发现了问题所在:图片时在UI线程上进行读取的,因此可能会导致应用停止响应从而崩溃。使用一个在Processing Bitmaps Off the UI Thread(博客链接:http://www.cnblogs.com/jdneo/p/3521195.html中说的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);

    }



    ... // include BitmapWorkerTask class

}



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];

            // Call out to ImageDetailActivity to load the bitmap in a background thread

            ((ImageDetailActivity) getActivity()).loadBitmap(resId, mImageView);

        }

    }

}

任何额外的处理(比如改变尺寸或者从网络获取图片)可以在BitmapWorkerTask中发生而不会影响到主UI的响应性。如果后台线程所做的不仅仅是直接从磁盘读取图片,那么添加一个内存缓存或磁盘缓存也是有益处的(博客链接:http://www.cnblogs.com/jdneo/p/3522538.html)。下面是内存缓存的修改代码:

public class ImageDetailActivity extends FragmentActivity {

    ...

    private LruCache<String, Bitmap> mMemoryCache;



    @Override

    public void onCreate(Bundle savedInstanceState) {

        ...

        // initialize LruCache as per Use a Memory Cache section

    }



    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);

        }

    }



    ... // include updated BitmapWorkerTask from Use a Memory Cache section

}

将所有代码片段放在一起,组成了响应良好的ViewPager实现,它有很小的图片加载延迟,并且有能力在后台根据你的需要处理图片。


二). 实现向一个GridView加载位图

grid list building block对于展示图片数据集是很有用的,并且可以通过一个GridView组件来实现。GridView组件允许同一时间在屏幕上显示许多图片,同时还有很多备用图片(当用户上下滑动页面时,备用图片将会被显示)。当实现这个控制类型时,你必须保证UI的流畅性,内存使用可控,并发处理正确(根据GridView回收它的子View)。

我们首先来看一下一个标准的GridView实现,它有一个置于Fragment中的子ImageView。还是像之前一样,请读者思考这一看上去完美的实现是否有进一步提升的空间?

public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {

    private ImageAdapter mAdapter;



    // A static dataset to back the GridView 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};



    // Empty constructor as per Fragment docs

    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) { // if it's not recycled, initialize some attributes

                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]); // Load image into ImageView

            return imageView;

        }

    }

}

再一次地,这个实现的问题在于图片设置是在UI线程中执行的。虽然这样做对于简单,尺寸小的图片来说没有问题(当然还和系统加载和缓存的资源有关),如果还有其他额外的处理要做,那么你的UI会戛然而止。

之前章节的异步处理和缓存的方法可以在这里实现。然而,你也需要警惕并发的问题,因为GridView会回收它的子View。要处理这个问题,使用在Processing Bitmaps Off the UI Thread课程(博客链接:http://www.cnblogs.com/jdneo/p/3521195.html)中所讲的知识。下面是升级后的解决方案:

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) {

                // Cancel previous task

                bitmapWorkerTask.cancel(true);

            } else {

                // The same work is already in progress

                return false;

            }

        }

        // No task associated with the ImageView, or an existing task was cancelled

        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;

    }



    ... // include updated BitmapWorkerTask class

Note:

同样的代码也可以应用于ListView

这样的实现对于如何处理和加载图片保留了足够的弹性,从而不会引起UI的卡顿。在后台任务中,你可以从网络上读取图片或者将大尺寸的数码照片进行缩放,在任务完成后,图像会显示出来。

要看这系列课程的完整代码样例,请下载http://www.cnblogs.com/jdneo/p/3512517.html中的样例代码

你可能感兴趣的:(developer)