http://my.oschina.net/ryanhoo/blog/88484
译者:Ryan Hoo
来源:https://developer.android.com/develop/index.html
译者按: 在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。
01 |
public 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的实现类。看起来这似乎是非常合理的方法,但是你能否看到这个方案的缺点呢?应该如何改善它呢?
01 |
public 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”一课中说的,将图片缓存到内存或者硬盘是有利于程序优化的。这里是对内存缓存的一些额外修改:
01 |
public 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中。我们再一次思考这个问题,这个方法看起来似乎非常完美且合乎情理,但是有没有办法让它便得更好呢?
01 |
public 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”一课中提到的技巧。这里是更新的解决方案:
01 |
public 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