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