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      */
ImageGridFragment()69     public ImageGridFragment() {}
70 
71     @Override
onCreate(Bundle savedInstanceState)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
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onResume()156     public void onResume() {
157         super.onResume();
158         mImageFetcher.setExitTasksEarly(false);
159         mAdapter.notifyDataSetChanged();
160     }
161 
162     @Override
onPause()163     public void onPause() {
164         super.onPause();
165         mImageFetcher.setPauseWork(false);
166         mImageFetcher.setExitTasksEarly(true);
167         mImageFetcher.flushCache();
168     }
169 
170     @Override
onDestroy()171     public void onDestroy() {
172         super.onDestroy();
173         mImageFetcher.closeCache();
174     }
175 
176     @TargetApi(VERSION_CODES.JELLY_BEAN)
177     @Override
onItemClick(AdapterView<?> parent, View v, int position, long id)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
onCreateOptionsMenu(Menu menu, MenuInflater inflater)194     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
195         inflater.inflate(R.menu.main_menu, menu);
196     }
197 
198     @Override
onOptionsItemSelected(MenuItem item)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 
ImageAdapter(Context context)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
getCount()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
getItem(int position)249         public Object getItem(int position) {
250             return position < mNumColumns ?
251                     null : Images.imageThumbUrls[position - mNumColumns];
252         }
253 
254         @Override
getItemId(int position)255         public long getItemId(int position) {
256             return position < mNumColumns ? 0 : position - mNumColumns;
257         }
258 
259         @Override
getViewTypeCount()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
getItemViewType(int position)266         public int getItemViewType(int position) {
267             return (position < mNumColumns) ? 1 : 0;
268         }
269 
270         @Override
hasStableIds()271         public boolean hasStableIds() {
272             return true;
273         }
274 
275         @Override
getView(int position, View convertView, ViewGroup container)276         public View getView(int position, View convertView, ViewGroup container) {
277             //BEGIN_INCLUDE(load_gridview_item)
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;
308             //END_INCLUDE(load_gridview_item)
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          */
setItemHeight(int height)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 
setNumColumns(int numColumns)328         public void setNumColumns(int numColumns) {
329             mNumColumns = numColumns;
330         }
331 
getNumColumns()332         public int getNumColumns() {
333             return mNumColumns;
334         }
335     }
336 }
337