【Google官方教程】第四课:在UI中显示Bitmap

来源:互联网 发布:罗大佑 童年 知乎 编辑:程序博客网 时间:2024/05/28 16:18

在Google最新的文档中,提供了一系列含金量相当高的教程。因为种种原因而鲜为人知,真是可惜!Ryan将会细心整理,将之翻译成中文,希望对开发者有所帮助。

        本系列是Google关于展示大Bitmap(位图)的官方演示,可以有效的解决内存限制,更加有效的加载并显示图片,同时避免让人头疼的OOM(Out Of Memory)。

-------------------------------------------------------------------------------------

译文:

        这节课将我们前面几节课学习的东西都整合起来,向你展示如何使用后台线程和Bitmap缓存加载多个Bitmap(位图)到ViewPager和GridView组件中,并学习如何处理并发和配置变化问题。

实现加载Bitmap到ViewPager 

        滑动浏览模式(Swipe View Pattern)是一种很好的浏览详细图片的方式。你可以使用ViewPager组件配合PagerAdapter(适配器)来实现这种模式。然而,更加合适的适配器是FragmentStatePagerAdapter,它可以在ViewPager退出屏幕的时候自动销毁并存储Fragments的状态,使得内存依然保留下来。

        注意如果你只有少量的图片,并且确信它们不会超出程序的内存限制,使用常规的PagerAdapter或者FragmentPagerAdapter或许更加合适。

        这里有一个包含ImageView的ViewPager的实现类,Main Activity(主活动)持有这个ViewPager和Adapter。

01public class ImageDetailActivity extends FragmentActivity {
02    public static final String EXTRA_IMAGE = "extra_image";
03 
04    private ImagePagerAdapter mAdapter;
05    private ViewPager mPager;
06 
07    // A static dataset to back the ViewPager adapter
08    public final static Integer[] imageResIds = new Integer[] {
09            R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
10            R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
11            R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};
12 
13    @Override
14    public void onCreate(Bundle savedInstanceState) {
15        super.onCreate(savedInstanceState);
16        setContentView(R.layout.image_detail_pager); // Contains just a ViewPager
17 
18        mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), imageResIds.length);
19        mPager = (ViewPager) findViewById(R.id.pager);
20        mPager.setAdapter(mAdapter);
21    }
22 
23    public static class ImagePagerAdapter extends FragmentStatePagerAdapter {
24        private final int mSize;
25 
26        public ImagePagerAdapter(FragmentManager fm, int size) {
27            super(fm);
28            mSize = size;
29        }
30 
31        @Override
32        public int getCount() {
33            return mSize;
34        }
35 
36        @Override
37        public Fragment getItem(int position) {
38            return ImageDetailFragment.newInstance(position);
39        }
40    }
41}

        这里有一个用来持有ImageView并显示详细信息的Fragment的实现类。看起来这似乎是非常合理的方法,但是你能否看到这个方案的缺点呢?应该如何改善它呢?

01public class ImageDetailFragment extends Fragment {
02    private static final String IMAGE_DATA_EXTRA = "resId";
03    private int mImageNum;
04    private ImageView mImageView;
05 
06    static ImageDetailFragment newInstance(int imageNum) {
07        final ImageDetailFragment f = new ImageDetailFragment();
08        final Bundle args = new Bundle();
09        args.putInt(IMAGE_DATA_EXTRA, imageNum);
10        f.setArguments(args);
11        return f;
12    }
13 
14    // Empty constructor, required as per Fragment docs
15    public ImageDetailFragment() {}
16 
17    @Override
18    public void onCreate(Bundle savedInstanceState) {
19        super.onCreate(savedInstanceState);
20        mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
21    }
22 
23    @Override
24    public View onCreateView(LayoutInflater inflater, ViewGroup container,
25            Bundle savedInstanceState) {
26        // image_detail_fragment.xml contains just an ImageView
27        final View v = inflater.inflate(R.layout.image_detail_fragment, container, false);
28        mImageView = (ImageView) v.findViewById(R.id.imageView);
29        return v;
30    }
31 
32    @Override
33    public void onActivityCreated(Bundle savedInstanceState) {
34        super.onActivityCreated(savedInstanceState);
35        final int resId = ImageDetailActivity.imageResIds[mImageNum];
36        mImageView.setImageResource(resId); // Load image into ImageView
37    }
38}

        希望你能注意到:这些图片是在UI线程从资源中读取过来的,而这极有可能导致应用挂起甚至被强制关闭。使用在“非UI线程处理Bitmap”一课中提到的AsyncTask,直接将图片加载和处理移到后台线程中。

        任何额外的处理(例如调整大小或者从网络获取图片)可以放在BitmapWorkerTask中而不会影响到主UI线程的响应性。如果后台线程做的不仅仅是直接从硬盘直接加载图片,那么如“缓存Bitmap”一课中说的,将图片缓存到内存或者硬盘是有利于程序优化的。这里是对内存缓存的一些额外修改:

01public class ImageDetailActivity extends FragmentActivity {
02    ...
03    private LruCache<String, Bitmap> mMemoryCache;
04 
05    @Override
06    public void onCreate(Bundle savedInstanceState) {
07        ...
08        // initialize LruCache as per Use a Memory Cache section
09    }
10 
11    public void loadBitmap(int resId, ImageView imageView) {
12        final String imageKey = String.valueOf(resId);
13 
14        final Bitmap bitmap = mMemoryCache.get(imageKey);
15        if (bitmap != null) {
16            mImageView.setImageBitmap(bitmap);
17        else {
18            mImageView.setImageResource(R.drawable.image_placeholder);
19            BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
20            task.execute(resId);
21        }
22    }
23 
24    ... // include updated BitmapWorkerTask from Use a Memory Cache section
25}

        将上面的代码片段整合在一起会让你的ViewPager具备优良的响应性能,可以实现最小的加载延迟,根据你的图片加载需要或多或少的进行后台处理。

实现加载Bitmap到GridView

        网格列表控件(Grid List Building Block)对于显示图片数据集非常有用,也可以使用GridView组件来实现,如果用户上下滚动的话,有很多图片处于就绪状态,随时可以显示在屏幕上。如果要实现这种类型的控制,你必须确保UI保持流畅性,内存使用处于控制之中而且并发也要被正确地处理(取决于GridView回收子视图的方式)。

        首先,这里有一个标准的GridView实现,将ImageView子控件存放在Fragment中。我们再一次思考这个问题,这个方法看起来似乎非常完美且合乎情理,但是有没有办法让它便得更好呢?

01public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
02    private ImageAdapter mAdapter;
03 
04    // A static dataset to back the GridView adapter
05    public final static Integer[] imageResIds = new Integer[] {
06            R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
07            R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
08            R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};
09 
10    // Empty constructor as per Fragment docs
11    public ImageGridFragment() {}
12 
13    @Override
14    public void onCreate(Bundle savedInstanceState) {
15        super.onCreate(savedInstanceState);
16        mAdapter = new ImageAdapter(getActivity());
17    }
18 
19    @Override
20    public View onCreateView(
21            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
22        final View v = inflater.inflate(R.layout.image_grid_fragment, container, false);
23        final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
24        mGridView.setAdapter(mAdapter);
25        mGridView.setOnItemClickListener(this);
26        return v;
27    }
28 
29    @Override
30    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
31        final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
32        i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position);
33        startActivity(i);
34    }
35 
36    private class ImageAdapter extends BaseAdapter {
37        private final Context mContext;
38 
39        public ImageAdapter(Context context) {
40            super();
41            mContext = context;
42        }
43 
44        @Override
45        public int getCount() {
46            return imageResIds.length;
47        }
48 
49        @Override
50        public Object getItem(int position) {
51            return imageResIds[position];
52        }
53 
54        @Override
55        public long getItemId(int position) {
56            return position;
57        }
58 
59        @Override
60        public View getView(int position, View convertView, ViewGroup container) {
61            ImageView imageView;
62            if (convertView == null) { // if it's not recycled, initialize some attributes
63                imageView = new ImageView(mContext);
64                imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
65                imageView.setLayoutParams(new GridView.LayoutParams(
66                        LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
67            else {
68                imageView = (ImageView) convertView;
69            }
70            imageView.setImageResource(imageResIds[position]); // Load image into ImageView
71            return imageView;
72        }
73    }
74}

        当然,问题还是这个方法在UI线程中处理图片。这种方式或许是和处理小而简单的图片(系统资源的加载和缓存),如果需要做任何的处理,UI就会被阻塞(甚至引起ANR(Application Not Responding))。

        和前一节相同的处理方式,我们在异步线程中进行处理和缓存。然而,考虑到GridView回收子视图的方式,你需要谨慎处理并发问题。可以使用“在非UI线程中处理Bitmap”一课中提到的技巧。这里是更新的解决方案:

01public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
02    ...
03 
04    private class ImageAdapter extends BaseAdapter {
05        ...
06 
07        @Override
08        public View getView(int position, View convertView, ViewGroup container) {
09            ...
10            loadBitmap(imageResIds[position], imageView)
11            return imageView;
12        }
13    }
14 
15    public void loadBitmap(int resId, ImageView imageView) {
16        if (cancelPotentialWork(resId, imageView)) {
17            final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
18            final AsyncDrawable asyncDrawable =
19                    new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
20            imageView.setImageDrawable(asyncDrawable);
21            task.execute(resId);
22        }
23    }
24 
25    static class AsyncDrawable extends BitmapDrawable {
26        private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
27 
28        public AsyncDrawable(Resources res, Bitmap bitmap,
29                BitmapWorkerTask bitmapWorkerTask) {
30            super(res, bitmap);
31            bitmapWorkerTaskReference =
32                new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
33        }
34 
35        public BitmapWorkerTask getBitmapWorkerTask() {
36            return bitmapWorkerTaskReference.get();
37        }
38    }
39 
40    public static boolean cancelPotentialWork(int data, ImageView imageView) {
41        final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
42 
43        if (bitmapWorkerTask != null) {
44            final int bitmapData = bitmapWorkerTask.data;
45            if (bitmapData != data) {
46                // Cancel previous task
47                bitmapWorkerTask.cancel(true);
48            else {
49                // The same work is already in progress
50                return false;
51            }
52        }
53        // No task associated with the ImageView, or an existing task was cancelled
54        return true;
55    }
56 
57    private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
58       if (imageView != null) {
59           final Drawable drawable = imageView.getDrawable();
60           if (drawable instanceof AsyncDrawable) {
61               final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
62               return asyncDrawable.getBitmapWorkerTask();
63           }
64        }
65        return null;
66    }
67 
68    ... // include updated BitmapWorkerTask class

        注意:相同的代码也可以很好的适配ListView。

        这里的实现方法允许灵活地处理和加载图片,并且不会影响UI的流畅性。在后台线程中,你可以从网络加载图片,调整大幅的数码相机照片的大小,并在处理任务结束的时候将图片显示在UI界面中。

        要了解在本节课中讨论到的概念和完整代码,请参阅示例程序。

        BitmapFun:http://vdisk.weibo.com/s/hNgFB