1
/*
2
* Copyright (C) 2012 The Android Open Source Project
3
*
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
* you may not use this file except in compliance with the License.
6
* You may obtain a copy of the License at
7
*
8
* http://www.apache.org/licenses/LICENSE-2.0
9
*
10
* Unless required by applicable law or agreed to in writing, software
11
* distributed under the License is distributed on an "AS IS" BASIS,
12
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
* See the License for the specific language governing permissions and
14
* limitations under the License.
15
*/
16
17
package com.example.android.displayingbitmaps.ui;
18
19
import android.annotation.TargetApi;
20
import android.app.ActivityOptions;
21
import android.content.Context;
22
import android.content.Intent;
23
import android.os.Build.VERSION_CODES;
24
import android.os.Bundle;
25
import android.support.v4.app.Fragment;
26
import android.util.TypedValue;
27
import android.view.LayoutInflater;
28
import android.view.Menu;
29
import android.view.MenuInflater;
30
import android.view.MenuItem;
31
import android.view.View;
32
import android.view.ViewGroup;
33
import android.view.ViewGroup.LayoutParams;
34
import android.view.ViewTreeObserver;
35
import android.widget.AbsListView;
36
import android.widget.AdapterView;
37
import android.widget.BaseAdapter;
38
import android.widget.GridView;
39
import android.widget.ImageView;
40
import android.widget.Toast;
41
42
import com.example.android.common.logger.Log;
43
import com.example.android.displayingbitmaps.BuildConfig;
44
import com.example.android.displayingbitmaps.R;
45
import com.example.android.displayingbitmaps.provider.Images;
46
import com.example.android.displayingbitmaps.util.ImageCache;
47
import com.example.android.displayingbitmaps.util.ImageFetcher;
48
import com.example.android.displayingbitmaps.util.Utils;
49
50
/**
51
* The main fragment that powers the ImageGridActivity screen. Fairly straight forward GridView
52
* implementation with the key addition being the ImageWorker class w/ImageCache to load children
53
* asynchronously, keeping the UI nice and smooth and caching thumbnails for quick retrieval. The
54
* cache is retained over configuration changes like orientation change so the images are populated
55
* quickly if, for example, the user rotates the device.
56
*/
57
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
58
private static final String TAG = "ImageGridFragment";
59
private static final String IMAGE_CACHE_DIR = "thumbs";
60
61
private int mImageThumbSize;
62
private int mImageThumbSpacing;
63
private ImageAdapter mAdapter;
64
private ImageFetcher mImageFetcher;
65
66
/**
67
* Empty constructor as per the Fragment documentation
68
*/
69
public ImageGridFragment() {}
70
71
@Override
72
public void onCreate(Bundle savedInstanceState) {
73
super.onCreate(savedInstanceState);
74
setHasOptionsMenu(true);
75
76
mImageThumbSize = getResources().getDimensionPixelSize(R.dimen.image_thumbnail_size);
77
mImageThumbSpacing = getResources().getDimensionPixelSize(R.dimen.image_thumbnail_spacing);
78
79
mAdapter = new ImageAdapter(getActivity());
80
81
ImageCache.ImageCacheParams cacheParams =
82
new ImageCache.ImageCacheParams(getActivity(), IMAGE_CACHE_DIR);
83
84
cacheParams.setMemCacheSizePercent(0.25f); // Set memory cache to 25% of app memory
85
86
// The ImageFetcher takes care of loading images into our ImageView children asynchronously
87
mImageFetcher = new ImageFetcher(getActivity(), mImageThumbSize);
88
mImageFetcher.setLoadingImage(R.drawable.empty_photo);
89
mImageFetcher.addImageCache(getActivity().getSupportFragmentManager(), cacheParams);
90
}
91
92
@Override
93
public View onCreateView(
94
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
95
96
final View v = inflater.inflate(R.layout.image_grid_fragment, container, false);
97
final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
98
mGridView.setAdapter(mAdapter);
99
mGridView.setOnItemClickListener(this);
100
mGridView.setOnScrollListener(new AbsListView.OnScrollListener() {
101
@Override
102
public void onScrollStateChanged(AbsListView absListView, int scrollState) {
103
// Pause fetcher to ensure smoother scrolling when flinging
104
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
105
// Before Honeycomb pause image loading on scroll to help with performance
106
if (!Utils.hasHoneycomb()) {
107
mImageFetcher.setPauseWork(true);
108
}
109
} else {
110
mImageFetcher.setPauseWork(false);
111
}
112
}
113
114
@Override
115
public void onScroll(AbsListView absListView, int firstVisibleItem,
116
int visibleItemCount, int totalItemCount) {
117
}
118
});
119
120
// This listener is used to get the final width of the GridView and then calculate the
121
// number of columns and the width of each column. The width of each column is variable
122
// as the GridView has stretchMode=columnWidth. The column width is used to set the height
123
// of each view so we get nice square thumbnails.
124
mGridView.getViewTreeObserver().addOnGlobalLayoutListener(
125
new ViewTreeObserver.OnGlobalLayoutListener() {
126
@TargetApi(VERSION_CODES.JELLY_BEAN)
127
@Override
128
public void onGlobalLayout() {
129
if (mAdapter.getNumColumns() == 0) {
130
final int numColumns = (int) Math.floor(
131
mGridView.getWidth() / (mImageThumbSize + mImageThumbSpacing));
132
if (numColumns > 0) {
133
final int columnWidth =
134
(mGridView.getWidth() / numColumns) - mImageThumbSpacing;
135
mAdapter.setNumColumns(numColumns);
136
mAdapter.setItemHeight(columnWidth);
137
if (BuildConfig.DEBUG) {
138
Log.d(TAG, "onCreateView - numColumns set to " + numColumns);
139
}
140
if (Utils.hasJellyBean()) {
141
mGridView.getViewTreeObserver()
142
.removeOnGlobalLayoutListener(this);
143
} else {
144
mGridView.getViewTreeObserver()
145
.removeGlobalOnLayoutListener(this);
146
}
147
}
148
}
149
}
150
});
151
152
return v;
153
}
154
155
@Override
156
public void onResume() {
157
super.onResume();
158
mImageFetcher.setExitTasksEarly(false);
159
mAdapter.notifyDataSetChanged();
160
}
161
162
@Override
163
public void onPause() {
164
super.onPause();
165
mImageFetcher.setPauseWork(false);
166
mImageFetcher.setExitTasksEarly(true);
167
mImageFetcher.flushCache();
168
}
169
170
@Override
171
public void onDestroy() {
172
super.onDestroy();
173
mImageFetcher.closeCache();
174
}
175
176
@TargetApi(VERSION_CODES.JELLY_BEAN)
177
@Override
178
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
179
final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
180
i.putExtra(ImageDetailActivity.EXTRA_IMAGE, (int) id);
181
if (Utils.hasJellyBean()) {
182
// makeThumbnailScaleUpAnimation() looks kind of ugly here as the loading spinner may
183
// show plus the thumbnail image in GridView is cropped. so using
184
// makeScaleUpAnimation() instead.
185
ActivityOptions options =
186
ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.getWidth(), v.getHeight());
187
getActivity().startActivity(i, options.toBundle());
188
} else {
189
startActivity(i);
190
}
191
}
192
193
@Override
194
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
195
inflater.inflate(R.menu.main_menu, menu);
196
}
197
198
@Override
199
public boolean onOptionsItemSelected(MenuItem item) {
200
switch (item.getItemId()) {
201
case R.id.clear_cache:
202
mImageFetcher.clearCache();
203
Toast.makeText(getActivity(), R.string.clear_cache_complete_toast,
204
Toast.LENGTH_SHORT).show();
205
return true;
206
}
207
return super.onOptionsItemSelected(item);
208
}
209
210
/**
211
* The main adapter that backs the GridView. This is fairly standard except the number of
212
* columns in the GridView is used to create a fake top row of empty views as we use a
213
* transparent ActionBar and don't want the real top row of images to start off covered by it.
214
*/
215
private class ImageAdapter extends BaseAdapter {
216
217
private final Context mContext;
218
private int mItemHeight = 0;
219
private int mNumColumns = 0;
220
private int mActionBarHeight = 0;
221
private GridView.LayoutParams mImageViewLayoutParams;
222
223
public ImageAdapter(Context context) {
224
super();
225
mContext = context;
226
mImageViewLayoutParams = new GridView.LayoutParams(
227
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
228
// Calculate ActionBar height
229
TypedValue tv = new TypedValue();
230
if (context.getTheme().resolveAttribute(
231
android.R.attr.actionBarSize, tv, true)) {
232
mActionBarHeight = TypedValue.complexToDimensionPixelSize(
233
tv.data, context.getResources().getDisplayMetrics());
234
}
235
}
236
237
@Override
238
public int getCount() {
239
// If columns have yet to be determined, return no items
240
if (getNumColumns() == 0) {
241
return 0;
242
}
243
244
// Size + number of columns for top empty row
245
return Images.imageThumbUrls.length + mNumColumns;
246
}
247
248
@Override
249
public Object getItem(int position) {
250
return position < mNumColumns ?
251
null : Images.imageThumbUrls[position - mNumColumns];
252
}
253
254
@Override
255
public long getItemId(int position) {
256
return position < mNumColumns ? 0 : position - mNumColumns;
257
}
258
259
@Override
260
public int getViewTypeCount() {
261
// Two types of views, the normal ImageView and the top row of empty views
262
return 2;
263
}
264
265
@Override
266
public int getItemViewType(int position) {
267
return (position < mNumColumns) ? 1 : 0;
268
}
269
270
@Override
271
public boolean hasStableIds() {
272
return true;
273
}
274
275
@Override
276
public View getView(int position, View convertView, ViewGroup container) {
278
// First check if this is the top row
279
if (position < mNumColumns) {
280
if (convertView == null) {
281
convertView = new View(mContext);
282
}
283
// Set empty view with height of ActionBar
284
convertView.setLayoutParams(new AbsListView.LayoutParams(
285
LayoutParams.MATCH_PARENT, mActionBarHeight));
286
return convertView;
287
}
288
289
// Now handle the main ImageView thumbnails
290
ImageView imageView;
291
if (convertView == null) { // if it's not recycled, instantiate and initialize
292
imageView = new RecyclingImageView(mContext);
293
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
294
imageView.setLayoutParams(mImageViewLayoutParams);
295
} else { // Otherwise re-use the converted view
296
imageView = (ImageView) convertView;
297
}
298
299
// Check the height matches our calculated column width
300
if (imageView.getLayoutParams().height != mItemHeight) {
301
imageView.setLayoutParams(mImageViewLayoutParams);
302
}
303
304
// Finally load the image asynchronously into the ImageView, this also takes care of
305
// setting a placeholder image while the background thread runs
306
mImageFetcher.loadImage(Images.imageThumbUrls[position - mNumColumns], imageView);
307
return imageView;
309
}
310
311
/**
312
* Sets the item height. Useful for when we know the column width so the height can be set
313
* to match.
314
*
315
* @param height
316
*/
317
public void setItemHeight(int height) {
318
if (height == mItemHeight) {
319
return;
320
}
321
mItemHeight = height;
322
mImageViewLayoutParams =
323
new GridView.LayoutParams(LayoutParams.MATCH_PARENT, mItemHeight);
324
mImageFetcher.setImageSize(height);
325
notifyDataSetChanged();
326
}
327
328
public void setNumColumns(int numColumns) {
329
mNumColumns = numColumns;
330
}
331
332
public int getNumColumns() {
333
return mNumColumns;
334
}
335
}
336
}