Alexander Lucas | 97842ff | 2014-03-07 14:56:55 -0800 | [diff] [blame^] | 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) { |
| 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 | */ |
| 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 | } |