【Google官方教程】第四課:在UI中顯示Bitmap

yangxi_001發表於2013-11-26

在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 

相關文章