1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
|
page.title=Displaying Bitmaps in Your UI
parent.title=Displaying Bitmaps Efficiently
parent.link=index.html
trainingnavtop=true
@jd:body
<div id="tb-wrapper">
<div id="tb">
<h2>This lesson teaches you to</h2>
<ol>
<li><a href="#viewpager">Load Bitmaps into a ViewPager Implementation</a></li>
<li><a href="#gridview">Load Bitmaps into a GridView Implementation</a></li>
</ol>
<h2>You should also read</h2>
<ul>
<li><a href="{@docRoot}design/patterns/swipe-views.html">Android Design: Swipe Views</a></li>
<li><a href="{@docRoot}design/building-blocks/grid-lists.html">Android Design: Grid Lists</a></li>
</ul>
<h2>Try it out</h2>
<div class="download-box">
<a href="{@docRoot}shareables/training/BitmapFun.zip" class="button">Download the sample</a>
<p class="filename">BitmapFun.zip</p>
</div>
</div>
</div>
<p></p>
<p>This lesson brings together everything from previous lessons, showing you how to load multiple
bitmaps into {@link android.support.v4.view.ViewPager} and {@link android.widget.GridView}
components using a background thread and bitmap cache, while dealing with concurrency and
configuration changes.</p>
<h2 id="viewpager">Load Bitmaps into a ViewPager Implementation</h2>
<p>The <a href="{@docRoot}design/patterns/swipe-views.html">swipe view pattern</a> is an excellent
way to navigate the detail view of an image gallery. You can implement this pattern using a {@link
android.support.v4.view.ViewPager} component backed by a {@link
android.support.v4.view.PagerAdapter}. However, a more suitable backing adapter is the subclass
{@link android.support.v4.app.FragmentStatePagerAdapter} which automatically destroys and saves
state of the {@link android.app.Fragment Fragments} in the {@link android.support.v4.view.ViewPager}
as they disappear off-screen, keeping memory usage down.</p>
<p class="note"><strong>Note:</strong> If you have a smaller number of images and are confident they
all fit within the application memory limit, then using a regular {@link
android.support.v4.view.PagerAdapter} or {@link android.support.v4.app.FragmentPagerAdapter} might
be more appropriate.</p>
<p>Here’s an implementation of a {@link android.support.v4.view.ViewPager} with {@link
android.widget.ImageView} children. The main activity holds the {@link
android.support.v4.view.ViewPager} and the adapter:</p>
<pre>
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);
}
}
}
</pre>
<p>Here is an implementation of the details {@link android.app.Fragment} which holds the {@link android.widget.ImageView} children. This might seem like a perfectly reasonable approach, but can
you see the drawbacks of this implementation? How could it be improved?</p>
<pre>
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];
<strong>mImageView.setImageResource(resId);</strong> // Load image into ImageView
}
}
</pre>
<p>Hopefully you noticed the issue: the images are being read from resources on the UI thread,
which can lead to an application hanging and being force closed. Using an
{@link android.os.AsyncTask} as described in the <a href="process-bitmap.html">Processing Bitmaps
Off the UI Thread</a> lesson, it’s straightforward to move image loading and processing to a
background thread:</p>
<pre>
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 <a href="process-bitmap.html#BitmapWorkerTask">{@code BitmapWorkerTask}</a> 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);
}
}
}
</pre>
<p>Any additional processing (such as resizing or fetching images from the network) can take place
in the <a href="process-bitmap.html#BitmapWorkerTask">{@code BitmapWorkerTask}</a> without affecting
responsiveness of the main UI. If the background thread is doing more than just loading an image
directly from disk, it can also be beneficial to add a memory and/or disk cache as described in the
lesson <a href="cache-bitmap.html#memory-cache">Caching Bitmaps</a>. Here's the additional
modifications for a memory cache:</p>
<pre>
public class ImageDetailActivity extends FragmentActivity {
...
private LruCache<String, Bitmap> mMemoryCache;
@Override
public void onCreate(Bundle savedInstanceState) {
...
// initialize LruCache as per <a href="cache-bitmap.html#memory-cache">Use a Memory Cache</a> 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 <a href="cache-bitmap.html#memory-cache">Use a Memory Cache</a> section
}
</pre>
<p>Putting all these pieces together gives you a responsive {@link
android.support.v4.view.ViewPager} implementation with minimal image loading latency and the ability
to do as much or as little background processing on your images as needed.</p>
<h2 id="gridview">Load Bitmaps into a GridView Implementation</h2>
<p>The <a href="{@docRoot}design/building-blocks/grid-lists.html">grid list building block</a> is
useful for showing image data sets and can be implemented using a {@link android.widget.GridView}
component in which many images can be on-screen at any one time and many more need to be ready to
appear if the user scrolls up or down. When implementing this type of control, you must ensure the
UI remains fluid, memory usage remains under control and concurrency is handled correctly (due to
the way {@link android.widget.GridView} recycles its children views).</p>
<p>To start with, here is a standard {@link android.widget.GridView} implementation with {@link
android.widget.ImageView} children placed inside a {@link android.app.Fragment}. Again, this might
seem like a perfectly reasonable approach, but what would make it better?</p>
<pre>
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;
}
<strong>imageView.setImageResource(imageResIds[position]);</strong> // Load image into ImageView
return imageView;
}
}
}
</pre>
<p>Once again, the problem with this implementation is that the image is being set in the UI thread.
While this may work for small, simple images (due to system resource loading and caching), if any
additional processing needs to be done, your UI grinds to a halt.</p>
<p>The same asynchronous processing and caching methods from the previous section can be implemented
here. However, you also need to wary of concurrency issues as the {@link android.widget.GridView}
recycles its children views. To handle this, use the techniques discussed in the <a
href="process-bitmap.html#concurrency">Processing Bitmaps Off the UI Thread</a> lesson. Here is the
updated
solution:</p>
<pre>
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
...
private class ImageAdapter extends BaseAdapter {
...
@Override
public View getView(int position, View convertView, ViewGroup container) {
...
<strong>loadBitmap(imageResIds[position], imageView)</strong>
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 <a href="process-bitmap.html#BitmapWorkerTaskUpdated">{@code BitmapWorkerTask}</a> class
</pre>
<p class="note"><strong>Note:</strong> The same code can easily be adapted to work with {@link
android.widget.ListView} as well.</p>
<p>This implementation allows for flexibility in how the images are processed and loaded without
impeding the smoothness of the UI. In the background task you can load images from the network or
resize large digital camera photos and the images appear as the tasks finish processing.</p>
<p>For a full example of this and other concepts discussed in this lesson, please see the included
sample application.</p>
|