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