Sample app for Android U class: Displaying Bitmaps Efficiently

Change-Id: I21da4e60f719b7159ada83894cca8d108bedd3d5
This commit is contained in:
Adam Koch
2012-03-29 19:17:22 -04:00
parent 64cd8f35fb
commit 2bab0137e0
30 changed files with 2644 additions and 0 deletions

View File

@@ -0,0 +1,147 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.provider;
import com.example.android.bitmapfun.util.ImageWorker.ImageWorkerAdapter;
/**
* Some simple test data to use for this sample app.
*/
public class Images {
/**
* This are PicasaWeb URLs and could potentially change. Ideally the PicasaWeb API should be
* used to fetch the URLs.
*/
public final static String[] imageUrls = new String[] {
"https://lh6.googleusercontent.com/-jZgveEqb6pg/T3R4kXScycI/AAAAAAAAAE0/xQ7CvpfXDzc/s1024/sample_image_01.jpg",
"https://lh4.googleusercontent.com/-K2FMuOozxU0/T3R4lRAiBTI/AAAAAAAAAE8/a3Eh9JvnnzI/s1024/sample_image_02.jpg",
"https://lh5.googleusercontent.com/-SCS5C646rxM/T3R4l7QB6xI/AAAAAAAAAFE/xLcuVv3CUyA/s1024/sample_image_03.jpg",
"https://lh6.googleusercontent.com/-f0NJR6-_Thg/T3R4mNex2wI/AAAAAAAAAFI/45oug4VE8MI/s1024/sample_image_04.jpg",
"https://lh3.googleusercontent.com/-n-xcJmiI0pg/T3R4mkSchHI/AAAAAAAAAFU/EoiNNb7kk3A/s1024/sample_image_05.jpg",
"https://lh3.googleusercontent.com/-X43vAudm7f4/T3R4nGSChJI/AAAAAAAAAFk/3bna6D-2EE8/s1024/sample_image_06.jpg",
"https://lh5.googleusercontent.com/-MpZneqIyjXU/T3R4nuGO1aI/AAAAAAAAAFg/r09OPjLx1ZY/s1024/sample_image_07.jpg",
"https://lh6.googleusercontent.com/-ql3YNfdClJo/T3XvW9apmFI/AAAAAAAAAL4/_6HFDzbahc4/s1024/sample_image_08.jpg",
"https://lh5.googleusercontent.com/-Pxa7eqF4cyc/T3R4oasvPEI/AAAAAAAAAF0/-uYDH92h8LA/s1024/sample_image_09.jpg",
"https://lh4.googleusercontent.com/-Li-rjhFEuaI/T3R4o-VUl4I/AAAAAAAAAF8/5E5XdMnP1oE/s1024/sample_image_10.jpg",
"https://lh5.googleusercontent.com/-_HU4fImgFhA/T3R4pPVIwWI/AAAAAAAAAGA/0RfK_Vkgth4/s1024/sample_image_11.jpg",
"https://lh6.googleusercontent.com/-0gnNrVjwa0Y/T3R4peGYJwI/AAAAAAAAAGU/uX_9wvRPM9I/s1024/sample_image_12.jpg",
"https://lh3.googleusercontent.com/-HBxuzALS_Zs/T3R4qERykaI/AAAAAAAAAGQ/_qQ16FaZ1q0/s1024/sample_image_13.jpg",
"https://lh4.googleusercontent.com/-cKojDrARNjQ/T3R4qfWSGPI/AAAAAAAAAGY/MR5dnbNaPyY/s1024/sample_image_14.jpg",
"https://lh3.googleusercontent.com/-WujkdYfcyZ8/T3R4qrIMGUI/AAAAAAAAAGk/277LIdgvnjg/s1024/sample_image_15.jpg",
"https://lh6.googleusercontent.com/-FMHR7Vy3PgI/T3R4rOXlEKI/AAAAAAAAAGs/VeXrDNDBkaw/s1024/sample_image_16.jpg",
"https://lh4.googleusercontent.com/-mrR0AJyNTH0/T3R4rZs6CuI/AAAAAAAAAG0/UE1wQqCOqLA/s1024/sample_image_17.jpg",
"https://lh6.googleusercontent.com/-z77w0eh3cow/T3R4rnLn05I/AAAAAAAAAG4/BaerfWoNucU/s1024/sample_image_18.jpg",
"https://lh5.googleusercontent.com/-aWVwh1OU5Bk/T3R4sAWw0yI/AAAAAAAAAHE/4_KAvJttFwA/s1024/sample_image_19.jpg",
"https://lh6.googleusercontent.com/-q-js52DMnWQ/T3R4tZhY2sI/AAAAAAAAAHM/A8kjp2Ivdqg/s1024/sample_image_20.jpg",
"https://lh5.googleusercontent.com/-_jIzvvzXKn4/T3R4t7xpdVI/AAAAAAAAAHU/7QC6eZ10jgs/s1024/sample_image_21.jpg",
"https://lh3.googleusercontent.com/-lnGi4IMLpwU/T3R4uCMa7vI/AAAAAAAAAHc/1zgzzz6qTpk/s1024/sample_image_22.jpg",
"https://lh5.googleusercontent.com/-fFCzKjFPsPc/T3R4u0SZPFI/AAAAAAAAAHk/sbgjzrktOK0/s1024/sample_image_23.jpg",
"https://lh4.googleusercontent.com/-8TqoW5gBE_Y/T3R4vBS3NPI/AAAAAAAAAHs/EZYvpNsaNXk/s1024/sample_image_24.jpg",
"https://lh6.googleusercontent.com/-gc4eQ3ySdzs/T3R4vafoA7I/AAAAAAAAAH4/yKii5P6tqDE/s1024/sample_image_25.jpg",
"https://lh5.googleusercontent.com/--NYOPCylU7Q/T3R4vjAiWkI/AAAAAAAAAH8/IPNx5q3ptRA/s1024/sample_image_26.jpg",
"https://lh6.googleusercontent.com/-9IJM8so4vCI/T3R4vwJO2yI/AAAAAAAAAIE/ljlr-cwuqZM/s1024/sample_image_27.jpg",
"https://lh4.googleusercontent.com/-KW6QwOHfhBs/T3R4w0RsQiI/AAAAAAAAAIM/uEFLVgHPFCk/s1024/sample_image_28.jpg",
"https://lh4.googleusercontent.com/-z2557Ec1ctY/T3R4x3QA2hI/AAAAAAAAAIk/9-GzPL1lTWE/s1024/sample_image_29.jpg",
"https://lh5.googleusercontent.com/-LaKXAn4Kr1c/T3R4yc5b4lI/AAAAAAAAAIY/fMgcOVQfmD0/s1024/sample_image_30.jpg",
"https://lh4.googleusercontent.com/-F9LRToJoQdo/T3R4yrLtyQI/AAAAAAAAAIg/ri9uUCWuRmo/s1024/sample_image_31.jpg",
"https://lh4.googleusercontent.com/-6X-xBwP-QpI/T3R4zGVboII/AAAAAAAAAIs/zYH4PjjngY0/s1024/sample_image_32.jpg",
"https://lh5.googleusercontent.com/-VdLRjbW4LAs/T3R4zXu3gUI/AAAAAAAAAIw/9aFp9t7mCPg/s1024/sample_image_33.jpg",
"https://lh6.googleusercontent.com/-gL6R17_fDJU/T3R4zpIXGjI/AAAAAAAAAI8/Q2Vjx-L9X20/s1024/sample_image_34.jpg",
"https://lh3.googleusercontent.com/-1fGH4YJXEzo/T3R40Y1B7KI/AAAAAAAAAJE/MnTsa77g-nk/s1024/sample_image_35.jpg",
"https://lh4.googleusercontent.com/-Ql0jHSrea-A/T3R403mUfFI/AAAAAAAAAJM/qzI4SkcH9tY/s1024/sample_image_36.jpg",
"https://lh5.googleusercontent.com/-BL5FIBR_tzI/T3R41DA0AKI/AAAAAAAAAJk/GZfeeb-SLM0/s1024/sample_image_37.jpg",
"https://lh4.googleusercontent.com/-wF2Vc9YDutw/T3R41fR2BCI/AAAAAAAAAJc/JdU1sHdMRAk/s1024/sample_image_38.jpg",
"https://lh6.googleusercontent.com/-ZWHiPehwjTI/T3R41zuaKCI/AAAAAAAAAJg/hR3QJ1v3REg/s1024/sample_image_39.jpg",
};
/**
* This are PicasaWeb thumbnail URLs and could potentially change. Ideally the PicasaWeb API
* should be used to fetch the URLs.
*/
public final static String[] imageThumbUrls = new String[] {
"https://lh6.googleusercontent.com/-jZgveEqb6pg/T3R4kXScycI/AAAAAAAAAE0/xQ7CvpfXDzc/s160-c/sample_image_01.jpg",
"https://lh4.googleusercontent.com/-K2FMuOozxU0/T3R4lRAiBTI/AAAAAAAAAE8/a3Eh9JvnnzI/s160-c/sample_image_02.jpg",
"https://lh5.googleusercontent.com/-SCS5C646rxM/T3R4l7QB6xI/AAAAAAAAAFE/xLcuVv3CUyA/s160-c/sample_image_03.jpg",
"https://lh6.googleusercontent.com/-f0NJR6-_Thg/T3R4mNex2wI/AAAAAAAAAFI/45oug4VE8MI/s160-c/sample_image_04.jpg",
"https://lh3.googleusercontent.com/-n-xcJmiI0pg/T3R4mkSchHI/AAAAAAAAAFU/EoiNNb7kk3A/s160-c/sample_image_05.jpg",
"https://lh3.googleusercontent.com/-X43vAudm7f4/T3R4nGSChJI/AAAAAAAAAFk/3bna6D-2EE8/s160-c/sample_image_06.jpg",
"https://lh5.googleusercontent.com/-MpZneqIyjXU/T3R4nuGO1aI/AAAAAAAAAFg/r09OPjLx1ZY/s160-c/sample_image_07.jpg",
"https://lh6.googleusercontent.com/-ql3YNfdClJo/T3XvW9apmFI/AAAAAAAAAL4/_6HFDzbahc4/s160-c/sample_image_08.jpg",
"https://lh5.googleusercontent.com/-Pxa7eqF4cyc/T3R4oasvPEI/AAAAAAAAAF0/-uYDH92h8LA/s160-c/sample_image_09.jpg",
"https://lh4.googleusercontent.com/-Li-rjhFEuaI/T3R4o-VUl4I/AAAAAAAAAF8/5E5XdMnP1oE/s160-c/sample_image_10.jpg",
"https://lh5.googleusercontent.com/-_HU4fImgFhA/T3R4pPVIwWI/AAAAAAAAAGA/0RfK_Vkgth4/s160-c/sample_image_11.jpg",
"https://lh6.googleusercontent.com/-0gnNrVjwa0Y/T3R4peGYJwI/AAAAAAAAAGU/uX_9wvRPM9I/s160-c/sample_image_12.jpg",
"https://lh3.googleusercontent.com/-HBxuzALS_Zs/T3R4qERykaI/AAAAAAAAAGQ/_qQ16FaZ1q0/s160-c/sample_image_13.jpg",
"https://lh4.googleusercontent.com/-cKojDrARNjQ/T3R4qfWSGPI/AAAAAAAAAGY/MR5dnbNaPyY/s160-c/sample_image_14.jpg",
"https://lh3.googleusercontent.com/-WujkdYfcyZ8/T3R4qrIMGUI/AAAAAAAAAGk/277LIdgvnjg/s160-c/sample_image_15.jpg",
"https://lh6.googleusercontent.com/-FMHR7Vy3PgI/T3R4rOXlEKI/AAAAAAAAAGs/VeXrDNDBkaw/s160-c/sample_image_16.jpg",
"https://lh4.googleusercontent.com/-mrR0AJyNTH0/T3R4rZs6CuI/AAAAAAAAAG0/UE1wQqCOqLA/s160-c/sample_image_17.jpg",
"https://lh6.googleusercontent.com/-z77w0eh3cow/T3R4rnLn05I/AAAAAAAAAG4/BaerfWoNucU/s160-c/sample_image_18.jpg",
"https://lh5.googleusercontent.com/-aWVwh1OU5Bk/T3R4sAWw0yI/AAAAAAAAAHE/4_KAvJttFwA/s160-c/sample_image_19.jpg",
"https://lh6.googleusercontent.com/-q-js52DMnWQ/T3R4tZhY2sI/AAAAAAAAAHM/A8kjp2Ivdqg/s160-c/sample_image_20.jpg",
"https://lh5.googleusercontent.com/-_jIzvvzXKn4/T3R4t7xpdVI/AAAAAAAAAHU/7QC6eZ10jgs/s160-c/sample_image_21.jpg",
"https://lh3.googleusercontent.com/-lnGi4IMLpwU/T3R4uCMa7vI/AAAAAAAAAHc/1zgzzz6qTpk/s160-c/sample_image_22.jpg",
"https://lh5.googleusercontent.com/-fFCzKjFPsPc/T3R4u0SZPFI/AAAAAAAAAHk/sbgjzrktOK0/s160-c/sample_image_23.jpg",
"https://lh4.googleusercontent.com/-8TqoW5gBE_Y/T3R4vBS3NPI/AAAAAAAAAHs/EZYvpNsaNXk/s160-c/sample_image_24.jpg",
"https://lh6.googleusercontent.com/-gc4eQ3ySdzs/T3R4vafoA7I/AAAAAAAAAH4/yKii5P6tqDE/s160-c/sample_image_25.jpg",
"https://lh5.googleusercontent.com/--NYOPCylU7Q/T3R4vjAiWkI/AAAAAAAAAH8/IPNx5q3ptRA/s160-c/sample_image_26.jpg",
"https://lh6.googleusercontent.com/-9IJM8so4vCI/T3R4vwJO2yI/AAAAAAAAAIE/ljlr-cwuqZM/s160-c/sample_image_27.jpg",
"https://lh4.googleusercontent.com/-KW6QwOHfhBs/T3R4w0RsQiI/AAAAAAAAAIM/uEFLVgHPFCk/s160-c/sample_image_28.jpg",
"https://lh4.googleusercontent.com/-z2557Ec1ctY/T3R4x3QA2hI/AAAAAAAAAIk/9-GzPL1lTWE/s160-c/sample_image_29.jpg",
"https://lh5.googleusercontent.com/-LaKXAn4Kr1c/T3R4yc5b4lI/AAAAAAAAAIY/fMgcOVQfmD0/s160-c/sample_image_30.jpg",
"https://lh4.googleusercontent.com/-F9LRToJoQdo/T3R4yrLtyQI/AAAAAAAAAIg/ri9uUCWuRmo/s160-c/sample_image_31.jpg",
"https://lh4.googleusercontent.com/-6X-xBwP-QpI/T3R4zGVboII/AAAAAAAAAIs/zYH4PjjngY0/s160-c/sample_image_32.jpg",
"https://lh5.googleusercontent.com/-VdLRjbW4LAs/T3R4zXu3gUI/AAAAAAAAAIw/9aFp9t7mCPg/s160-c/sample_image_33.jpg",
"https://lh6.googleusercontent.com/-gL6R17_fDJU/T3R4zpIXGjI/AAAAAAAAAI8/Q2Vjx-L9X20/s160-c/sample_image_34.jpg",
"https://lh3.googleusercontent.com/-1fGH4YJXEzo/T3R40Y1B7KI/AAAAAAAAAJE/MnTsa77g-nk/s160-c/sample_image_35.jpg",
"https://lh4.googleusercontent.com/-Ql0jHSrea-A/T3R403mUfFI/AAAAAAAAAJM/qzI4SkcH9tY/s160-c/sample_image_36.jpg",
"https://lh5.googleusercontent.com/-BL5FIBR_tzI/T3R41DA0AKI/AAAAAAAAAJk/GZfeeb-SLM0/s160-c/sample_image_37.jpg",
"https://lh4.googleusercontent.com/-wF2Vc9YDutw/T3R41fR2BCI/AAAAAAAAAJc/JdU1sHdMRAk/s160-c/sample_image_38.jpg",
"https://lh6.googleusercontent.com/-ZWHiPehwjTI/T3R41zuaKCI/AAAAAAAAAJg/hR3QJ1v3REg/s160-c/sample_image_39.jpg",
};
/**
* Simple static adapter to use for images.
*/
public final static ImageWorkerAdapter imageWorkerUrlsAdapter = new ImageWorkerAdapter() {
@Override
public Object getItem(int num) {
return Images.imageUrls[num];
}
@Override
public int getSize() {
return Images.imageUrls.length;
}
};
/**
* Simple static adapter to use for image thumbnails.
*/
public final static ImageWorkerAdapter imageThumbWorkerUrlsAdapter = new ImageWorkerAdapter() {
@Override
public Object getItem(int num) {
return Images.imageThumbUrls[num];
}
@Override
public int getSize() {
return Images.imageThumbUrls.length;
}
};
}

View File

@@ -0,0 +1,203 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.ui;
import android.annotation.SuppressLint;
import android.app.ActionBar;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;
import android.util.DisplayMetrics;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.WindowManager.LayoutParams;
import android.widget.Toast;
import com.example.android.bitmapfun.R;
import com.example.android.bitmapfun.provider.Images;
import com.example.android.bitmapfun.util.DiskLruCache;
import com.example.android.bitmapfun.util.ImageCache;
import com.example.android.bitmapfun.util.ImageFetcher;
import com.example.android.bitmapfun.util.ImageResizer;
import com.example.android.bitmapfun.util.ImageWorker;
import com.example.android.bitmapfun.util.Utils;
public class ImageDetailActivity extends FragmentActivity implements OnClickListener {
private static final String IMAGE_CACHE_DIR = "images";
public static final String EXTRA_IMAGE = "extra_image";
private ImagePagerAdapter mAdapter;
private ImageResizer mImageWorker;
private ViewPager mPager;
@SuppressLint("NewApi")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.image_detail_pager);
// Fetch screen height and width, to use as our max size when loading images as this
// activity runs full screen
final DisplayMetrics displaymetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
final int height = displaymetrics.heightPixels;
final int width = displaymetrics.widthPixels;
final int longest = height > width ? height : width;
// The ImageWorker takes care of loading images into our ImageView children asynchronously
mImageWorker = new ImageFetcher(this, longest);
mImageWorker.setAdapter(Images.imageWorkerUrlsAdapter);
mImageWorker.setImageCache(ImageCache.findOrCreateCache(this, IMAGE_CACHE_DIR));
mImageWorker.setImageFadeIn(false);
// Set up ViewPager and backing adapter
mAdapter = new ImagePagerAdapter(getSupportFragmentManager(),
mImageWorker.getAdapter().getSize());
mPager = (ViewPager) findViewById(R.id.pager);
mPager.setAdapter(mAdapter);
mPager.setPageMargin((int) getResources().getDimension(R.dimen.image_detail_pager_margin));
// Set up activity to go full screen
getWindow().addFlags(LayoutParams.FLAG_FULLSCREEN);
// Enable some additional newer visibility and ActionBar features to create a more immersive
// photo viewing experience
if (Utils.hasActionBar()) {
final ActionBar actionBar = getActionBar();
// Enable "up" navigation on ActionBar icon and hide title text
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowTitleEnabled(false);
// Start low profile mode and hide ActionBar
mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
actionBar.hide();
// Hide and show the ActionBar as the visibility changes
mPager.setOnSystemUiVisibilityChangeListener(
new View.OnSystemUiVisibilityChangeListener() {
@Override
public void onSystemUiVisibilityChange(int vis) {
if ((vis & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0) {
actionBar.hide();
} else {
actionBar.show();
}
}
});
}
// Set the current item based on the extra passed in to this activity
final int extraCurrentItem = getIntent().getIntExtra(EXTRA_IMAGE, -1);
if (extraCurrentItem != -1) {
mPager.setCurrentItem(extraCurrentItem);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// Home or "up" navigation
final Intent intent = new Intent(this, ImageGridActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return true;
case R.id.clear_cache:
final ImageCache cache = mImageWorker.getImageCache();
if (cache != null) {
mImageWorker.getImageCache().clearCaches();
DiskLruCache.clearCache(this, ImageFetcher.HTTP_CACHE_DIR);
Toast.makeText(this, R.string.clear_cache_complete,
Toast.LENGTH_SHORT).show();
}
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.main_menu, menu);
return true;
}
/**
* Called by the ViewPager child fragments to load images via the one ImageWorker
*
* @return
*/
public ImageWorker getImageWorker() {
return mImageWorker;
}
/**
* The main adapter that backs the ViewPager. A subclass of FragmentStatePagerAdapter as there
* could be a large number of items in the ViewPager and we don't want to retain them all in
* memory at once but create/destroy them on the fly.
*/
private class ImagePagerAdapter extends FragmentStatePagerAdapter {
private final int mSize;
public ImagePagerAdapter(FragmentManager fm, int size) {
super(fm);
mSize = size;
}
@Override
public int getCount() {
return mSize;
}
@Override
public Fragment getItem(int position) {
return ImageDetailFragment.newInstance(position);
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
final ImageDetailFragment fragment = (ImageDetailFragment) object;
// As the item gets destroyed we try and cancel any existing work.
fragment.cancelWork();
super.destroyItem(container, position, object);
}
}
/**
* Set on the ImageView in the ViewPager children fragments, to enable/disable low profile mode
* when the ImageView is touched.
*/
@SuppressLint("NewApi")
@Override
public void onClick(View v) {
final int vis = mPager.getSystemUiVisibility();
if ((vis & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0) {
mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
} else {
mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
}
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.ui;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.example.android.bitmapfun.R;
import com.example.android.bitmapfun.util.ImageWorker;
import com.example.android.bitmapfun.util.Utils;
/**
* This fragment will populate the children of the ViewPager from {@link ImageDetailActivity}.
*/
public class ImageDetailFragment extends Fragment {
private static final String IMAGE_DATA_EXTRA = "resId";
private int mImageNum;
private ImageView mImageView;
private ImageWorker mImageWorker;
/**
* Factory method to generate a new instance of the fragment given an image number.
*
* @param imageNum The image number within the parent adapter to load
* @return A new instance of ImageDetailFragment with imageNum extras
*/
public static ImageDetailFragment newInstance(int imageNum) {
final ImageDetailFragment f = new ImageDetailFragment();
final Bundle args = new Bundle();
args.putInt(IMAGE_DATA_EXTRA, imageNum);
f.setArguments(args);
return f;
}
/**
* Empty constructor as per the Fragment documentation
*/
public ImageDetailFragment() {}
/**
* Populate image number from extra, use the convenience factory method
* {@link ImageDetailFragment#newInstance(int)} to create this fragment.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate and locate the main ImageView
final View v = inflater.inflate(R.layout.image_detail_fragment, container, false);
mImageView = (ImageView) v.findViewById(R.id.imageView);
return v;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Use the parent activity to load the image asynchronously into the ImageView (so a single
// cache can be used over all pages in the ViewPager
if (ImageDetailActivity.class.isInstance(getActivity())) {
mImageWorker = ((ImageDetailActivity) getActivity()).getImageWorker();
mImageWorker.loadImage(mImageNum, mImageView);
}
// Pass clicks on the ImageView to the parent activity to handle
if (OnClickListener.class.isInstance(getActivity()) && Utils.hasActionBar()) {
mImageView.setOnClickListener((OnClickListener) getActivity());
}
}
/**
* Cancels the asynchronous work taking place on the ImageView, called by the adapter backing
* the ViewPager when the child is destroyed.
*/
public void cancelWork() {
ImageWorker.cancelWork(mImageView);
mImageView.setImageDrawable(null);
mImageView = null;
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.ui;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentTransaction;
/**
* Simple FragmentActivity to hold the main {@link ImageGridFragment} and not much else.
*/
public class ImageGridActivity extends FragmentActivity {
private static final String TAG = "ImageGridFragment";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getSupportFragmentManager().findFragmentByTag(TAG) == null) {
final FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.add(android.R.id.content, new ImageGridFragment(), TAG);
ft.commit();
}
}
}

View File

@@ -0,0 +1,300 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.ui;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.Toast;
import com.example.android.bitmapfun.BuildConfig;
import com.example.android.bitmapfun.R;
import com.example.android.bitmapfun.provider.Images;
import com.example.android.bitmapfun.util.DiskLruCache;
import com.example.android.bitmapfun.util.ImageCache;
import com.example.android.bitmapfun.util.ImageCache.ImageCacheParams;
import com.example.android.bitmapfun.util.ImageFetcher;
import com.example.android.bitmapfun.util.ImageResizer;
import com.example.android.bitmapfun.util.Utils;
/**
* The main fragment that powers the ImageGridActivity screen. Fairly straight forward GridView
* implementation with the key addition being the ImageWorker class w/ImageCache to load children
* asynchronously, keeping the UI nice and smooth and caching thumbnails for quick retrieval. The
* cache is retained over configuration changes like orientation change so the images are populated
* quickly as the user rotates the device.
*/
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
private static final String TAG = "ImageGridFragment";
private static final String IMAGE_CACHE_DIR = "thumbs";
private int mImageThumbSize;
private int mImageThumbSpacing;
private ImageAdapter mAdapter;
private ImageResizer mImageWorker;
/**
* Empty constructor as per the Fragment documentation
*/
public ImageGridFragment() {}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
mImageThumbSize = getResources().getDimensionPixelSize(R.dimen.image_thumbnail_size);
mImageThumbSpacing = getResources().getDimensionPixelSize(R.dimen.image_thumbnail_spacing);
mAdapter = new ImageAdapter(getActivity());
ImageCacheParams cacheParams = new ImageCacheParams(IMAGE_CACHE_DIR);
// Allocate a third of the per-app memory limit to the bitmap memory cache. This value
// should be chosen carefully based on a number of factors. Refer to the corresponding
// Android Training class for more discussion:
// http://developer.android.com/training/displaying-bitmaps/
// In this case, we aren't using memory for much else other than this activity and the
// ImageDetailActivity so a third lets us keep all our sample image thumbnails in memory
// at once.
cacheParams.memCacheSize = 1024 * 1024 * Utils.getMemoryClass(getActivity()) / 3;
// The ImageWorker takes care of loading images into our ImageView children asynchronously
mImageWorker = new ImageFetcher(getActivity(), mImageThumbSize);
mImageWorker.setAdapter(Images.imageThumbWorkerUrlsAdapter);
mImageWorker.setLoadingImage(R.drawable.empty_photo);
mImageWorker.setImageCache(ImageCache.findOrCreateCache(getActivity(), cacheParams));
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View v = inflater.inflate(R.layout.image_grid_fragment, container, false);
final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
mGridView.setAdapter(mAdapter);
mGridView.setOnItemClickListener(this);
// This listener is used to get the final width of the GridView and then calculate the
// number of columns and the width of each column. The width of each column is variable
// as the GridView has stretchMode=columnWidth. The column width is used to set the height
// of each view so we get nice square thumbnails.
mGridView.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (mAdapter.getNumColumns() == 0) {
final int numColumns = (int) Math.floor(
mGridView.getWidth() / (mImageThumbSize + mImageThumbSpacing));
if (numColumns > 0) {
final int columnWidth =
(mGridView.getWidth() / numColumns) - mImageThumbSpacing;
mAdapter.setNumColumns(numColumns);
mAdapter.setItemHeight(columnWidth);
if (BuildConfig.DEBUG) {
Log.d(TAG, "onCreateView - numColumns set to " + numColumns);
}
}
}
}
});
return v;
}
@Override
public void onResume() {
super.onResume();
mImageWorker.setExitTasksEarly(false);
mAdapter.notifyDataSetChanged();
}
@Override
public void onPause() {
super.onPause();
mImageWorker.setExitTasksEarly(true);
}
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
i.putExtra(ImageDetailActivity.EXTRA_IMAGE, (int) id);
startActivity(i);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.main_menu, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.clear_cache:
final ImageCache cache = mImageWorker.getImageCache();
if (cache != null) {
mImageWorker.getImageCache().clearCaches();
DiskLruCache.clearCache(getActivity(), ImageFetcher.HTTP_CACHE_DIR);
Toast.makeText(getActivity(), R.string.clear_cache_complete,
Toast.LENGTH_SHORT).show();
}
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* The main adapter that backs the GridView. This is fairly standard except the number of
* columns in the GridView is used to create a fake top row of empty views as we use a
* transparent ActionBar and don't want the real top row of images to start off covered by it.
*/
private class ImageAdapter extends BaseAdapter {
private final Context mContext;
private int mItemHeight = 0;
private int mNumColumns = 0;
private int mActionBarHeight = -1;
private GridView.LayoutParams mImageViewLayoutParams;
public ImageAdapter(Context context) {
super();
mContext = context;
mImageViewLayoutParams = new GridView.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
@Override
public int getCount() {
// Size of adapter + number of columns for top empty row
return mImageWorker.getAdapter().getSize() + mNumColumns;
}
@Override
public Object getItem(int position) {
return position < mNumColumns ?
null : mImageWorker.getAdapter().getItem(position - mNumColumns);
}
@Override
public long getItemId(int position) {
return position < mNumColumns ? 0 : position - mNumColumns;
}
@Override
public int getViewTypeCount() {
// Two types of views, the normal ImageView and the top row of empty views
return 2;
}
@Override
public int getItemViewType(int position) {
return (position < mNumColumns) ? 1 : 0;
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public View getView(int position, View convertView, ViewGroup container) {
// First check if this is the top row
if (position < mNumColumns) {
if (convertView == null) {
convertView = new View(mContext);
}
// Calculate ActionBar height
if (mActionBarHeight < 0) {
TypedValue tv = new TypedValue();
if (mContext.getTheme().resolveAttribute(
android.R.attr.actionBarSize, tv, true)) {
mActionBarHeight = TypedValue.complexToDimensionPixelSize(
tv.data, mContext.getResources().getDisplayMetrics());
} else {
// No ActionBar style (pre-Honeycomb or ActionBar not in theme)
mActionBarHeight = 0;
}
}
// Set empty view with height of ActionBar
convertView.setLayoutParams(new AbsListView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, mActionBarHeight));
return convertView;
}
// Now handle the main ImageView thumbnails
ImageView imageView;
if (convertView == null) { // if it's not recycled, instantiate and initialize
imageView = new ImageView(mContext);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageView.setLayoutParams(mImageViewLayoutParams);
} else { // Otherwise re-use the converted view
imageView = (ImageView) convertView;
}
// Check the height matches our calculated column width
if (imageView.getLayoutParams().height != mItemHeight) {
imageView.setLayoutParams(mImageViewLayoutParams);
}
// Finally load the image asynchronously into the ImageView, this also takes care of
// setting a placeholder image while the background thread runs
mImageWorker.loadImage(position - mNumColumns, imageView);
return imageView;
}
/**
* Sets the item height. Useful for when we know the column width so the height can be set
* to match.
*
* @param height
*/
public void setItemHeight(int height) {
if (height == mItemHeight) {
return;
}
mItemHeight = height;
mImageViewLayoutParams =
new GridView.LayoutParams(LayoutParams.MATCH_PARENT, mItemHeight);
mImageWorker.setImageSize(height);
notifyDataSetChanged();
}
public void setNumColumns(int numColumns) {
mNumColumns = numColumns;
}
public int getNumColumns() {
return mNumColumns;
}
}
}

View File

@@ -0,0 +1,336 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.util.Log;
import com.example.android.bitmapfun.BuildConfig;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
/**
* A simple disk LRU bitmap cache to illustrate how a disk cache would be used for bitmap caching. A
* much more robust and efficient disk LRU cache solution can be found in the ICS source code
* (libcore/luni/src/main/java/libcore/io/DiskLruCache.java) and is preferable to this simple
* implementation.
*/
public class DiskLruCache {
private static final String TAG = "DiskLruCache";
private static final String CACHE_FILENAME_PREFIX = "cache_";
private static final int MAX_REMOVALS = 4;
private static final int INITIAL_CAPACITY = 32;
private static final float LOAD_FACTOR = 0.75f;
private final File mCacheDir;
private int cacheSize = 0;
private int cacheByteSize = 0;
private final int maxCacheItemSize = 64; // 64 item default
private long maxCacheByteSize = 1024 * 1024 * 5; // 5MB default
private CompressFormat mCompressFormat = CompressFormat.JPEG;
private int mCompressQuality = 70;
private final Map<String, String> mLinkedHashMap =
Collections.synchronizedMap(new LinkedHashMap<String, String>(
INITIAL_CAPACITY, LOAD_FACTOR, true));
/**
* A filename filter to use to identify the cache filenames which have CACHE_FILENAME_PREFIX
* prepended.
*/
private static final FilenameFilter cacheFileFilter = new FilenameFilter() {
@Override
public boolean accept(File dir, String filename) {
return filename.startsWith(CACHE_FILENAME_PREFIX);
}
};
/**
* Used to fetch an instance of DiskLruCache.
*
* @param context
* @param cacheDir
* @param maxByteSize
* @return
*/
public static DiskLruCache openCache(Context context, File cacheDir, long maxByteSize) {
if (!cacheDir.exists()) {
cacheDir.mkdir();
}
if (cacheDir.isDirectory() && cacheDir.canWrite()
&& Utils.getUsableSpace(cacheDir) > maxByteSize) {
return new DiskLruCache(cacheDir, maxByteSize);
}
return null;
}
/**
* Constructor that should not be called directly, instead use
* {@link DiskLruCache#openCache(Context, File, long)} which runs some extra checks before
* creating a DiskLruCache instance.
*
* @param cacheDir
* @param maxByteSize
*/
private DiskLruCache(File cacheDir, long maxByteSize) {
mCacheDir = cacheDir;
maxCacheByteSize = maxByteSize;
}
/**
* Add a bitmap to the disk cache.
*
* @param key A unique identifier for the bitmap.
* @param data The bitmap to store.
*/
public void put(String key, Bitmap data) {
synchronized (mLinkedHashMap) {
if (mLinkedHashMap.get(key) == null) {
try {
final String file = createFilePath(mCacheDir, key);
if (writeBitmapToFile(data, file)) {
put(key, file);
flushCache();
}
} catch (final FileNotFoundException e) {
Log.e(TAG, "Error in put: " + e.getMessage());
} catch (final IOException e) {
Log.e(TAG, "Error in put: " + e.getMessage());
}
}
}
}
private void put(String key, String file) {
mLinkedHashMap.put(key, file);
cacheSize = mLinkedHashMap.size();
cacheByteSize += new File(file).length();
}
/**
* Flush the cache, removing oldest entries if the total size is over the specified cache size.
* Note that this isn't keeping track of stale files in the cache directory that aren't in the
* HashMap. If the images and keys in the disk cache change often then they probably won't ever
* be removed.
*/
private void flushCache() {
Entry<String, String> eldestEntry;
File eldestFile;
long eldestFileSize;
int count = 0;
while (count < MAX_REMOVALS &&
(cacheSize > maxCacheItemSize || cacheByteSize > maxCacheByteSize)) {
eldestEntry = mLinkedHashMap.entrySet().iterator().next();
eldestFile = new File(eldestEntry.getValue());
eldestFileSize = eldestFile.length();
mLinkedHashMap.remove(eldestEntry.getKey());
eldestFile.delete();
cacheSize = mLinkedHashMap.size();
cacheByteSize -= eldestFileSize;
count++;
if (BuildConfig.DEBUG) {
Log.d(TAG, "flushCache - Removed cache file, " + eldestFile + ", "
+ eldestFileSize);
}
}
}
/**
* Get an image from the disk cache.
*
* @param key The unique identifier for the bitmap
* @return The bitmap or null if not found
*/
public Bitmap get(String key) {
synchronized (mLinkedHashMap) {
final String file = mLinkedHashMap.get(key);
if (file != null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache hit");
}
return BitmapFactory.decodeFile(file);
} else {
final String existingFile = createFilePath(mCacheDir, key);
if (new File(existingFile).exists()) {
put(key, existingFile);
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache hit (existing file)");
}
return BitmapFactory.decodeFile(existingFile);
}
}
return null;
}
}
/**
* Checks if a specific key exist in the cache.
*
* @param key The unique identifier for the bitmap
* @return true if found, false otherwise
*/
public boolean containsKey(String key) {
// See if the key is in our HashMap
if (mLinkedHashMap.containsKey(key)) {
return true;
}
// Now check if there's an actual file that exists based on the key
final String existingFile = createFilePath(mCacheDir, key);
if (new File(existingFile).exists()) {
// File found, add it to the HashMap for future use
put(key, existingFile);
return true;
}
return false;
}
/**
* Removes all disk cache entries from this instance cache dir
*/
public void clearCache() {
DiskLruCache.clearCache(mCacheDir);
}
/**
* Removes all disk cache entries from the application cache directory in the uniqueName
* sub-directory.
*
* @param context The context to use
* @param uniqueName A unique cache directory name to append to the app cache directory
*/
public static void clearCache(Context context, String uniqueName) {
File cacheDir = getDiskCacheDir(context, uniqueName);
clearCache(cacheDir);
}
/**
* Removes all disk cache entries from the given directory. This should not be called directly,
* call {@link DiskLruCache#clearCache(Context, String)} or {@link DiskLruCache#clearCache()}
* instead.
*
* @param cacheDir The directory to remove the cache files from
*/
private static void clearCache(File cacheDir) {
final File[] files = cacheDir.listFiles(cacheFileFilter);
for (int i=0; i<files.length; i++) {
files[i].delete();
}
}
/**
* Get a usable cache directory (external if available, internal otherwise).
*
* @param context The context to use
* @param uniqueName A unique directory name to append to the cache dir
* @return The cache dir
*/
public static File getDiskCacheDir(Context context, String uniqueName) {
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
// otherwise use internal cache dir
final String cachePath =
Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED ||
!Utils.isExternalStorageRemovable() ?
Utils.getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}
/**
* Creates a constant cache file path given a target cache directory and an image key.
*
* @param cacheDir
* @param key
* @return
*/
public static String createFilePath(File cacheDir, String key) {
try {
// Use URLEncoder to ensure we have a valid filename, a tad hacky but it will do for
// this example
return cacheDir.getAbsolutePath() + File.separator +
CACHE_FILENAME_PREFIX + URLEncoder.encode(key.replace("*", ""), "UTF-8");
} catch (final UnsupportedEncodingException e) {
Log.e(TAG, "createFilePath - " + e);
}
return null;
}
/**
* Create a constant cache file path using the current cache directory and an image key.
*
* @param key
* @return
*/
public String createFilePath(String key) {
return createFilePath(mCacheDir, key);
}
/**
* Sets the target compression format and quality for images written to the disk cache.
*
* @param compressFormat
* @param quality
*/
public void setCompressParams(CompressFormat compressFormat, int quality) {
mCompressFormat = compressFormat;
mCompressQuality = quality;
}
/**
* Writes a bitmap to a file. Call {@link DiskLruCache#setCompressParams(CompressFormat, int)}
* first to set the target bitmap compression and format.
*
* @param bitmap
* @param file
* @return
*/
private boolean writeBitmapToFile(Bitmap bitmap, String file)
throws IOException, FileNotFoundException {
OutputStream out = null;
try {
out = new BufferedOutputStream(new FileOutputStream(file), Utils.IO_BUFFER_SIZE);
return bitmap.compress(mCompressFormat, mCompressQuality, out);
} finally {
if (out != null) {
out.close();
}
}
}
}

View File

@@ -0,0 +1,217 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.support.v4.app.FragmentActivity;
import android.support.v4.util.LruCache;
import android.util.Log;
import com.example.android.bitmapfun.BuildConfig;
import java.io.File;
/**
* This class holds our bitmap caches (memory and disk).
*/
public class ImageCache {
private static final String TAG = "ImageCache";
// Default memory cache size
private static final int DEFAULT_MEM_CACHE_SIZE = 1024 * 1024 * 5; // 5MB
// Default disk cache size
private static final int DEFAULT_DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
// Compression settings when writing images to disk cache
private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG;
private static final int DEFAULT_COMPRESS_QUALITY = 70;
// Constants to easily toggle various caches
private static final boolean DEFAULT_MEM_CACHE_ENABLED = true;
private static final boolean DEFAULT_DISK_CACHE_ENABLED = true;
private static final boolean DEFAULT_CLEAR_DISK_CACHE_ON_START = false;
private DiskLruCache mDiskCache;
private LruCache<String, Bitmap> mMemoryCache;
/**
* Creating a new ImageCache object using the specified parameters.
*
* @param context The context to use
* @param cacheParams The cache parameters to use to initialize the cache
*/
public ImageCache(Context context, ImageCacheParams cacheParams) {
init(context, cacheParams);
}
/**
* Creating a new ImageCache object using the default parameters.
*
* @param context The context to use
* @param uniqueName A unique name that will be appended to the cache directory
*/
public ImageCache(Context context, String uniqueName) {
init(context, new ImageCacheParams(uniqueName));
}
/**
* Find and return an existing ImageCache stored in a {@link RetainFragment}, if not found a new
* one is created with defaults and saved to a {@link RetainFragment}.
*
* @param activity The calling {@link FragmentActivity}
* @param uniqueName A unique name to append to the cache directory
* @return An existing retained ImageCache object or a new one if one did not exist.
*/
public static ImageCache findOrCreateCache(
final FragmentActivity activity, final String uniqueName) {
return findOrCreateCache(activity, new ImageCacheParams(uniqueName));
}
/**
* Find and return an existing ImageCache stored in a {@link RetainFragment}, if not found a new
* one is created using the supplied params and saved to a {@link RetainFragment}.
*
* @param activity The calling {@link FragmentActivity}
* @param cacheParams The cache parameters to use if creating the ImageCache
* @return An existing retained ImageCache object or a new one if one did not exist
*/
public static ImageCache findOrCreateCache(
final FragmentActivity activity, ImageCacheParams cacheParams) {
// Search for, or create an instance of the non-UI RetainFragment
final RetainFragment mRetainFragment = RetainFragment.findOrCreateRetainFragment(
activity.getSupportFragmentManager());
// See if we already have an ImageCache stored in RetainFragment
ImageCache imageCache = (ImageCache) mRetainFragment.getObject();
// No existing ImageCache, create one and store it in RetainFragment
if (imageCache == null) {
imageCache = new ImageCache(activity, cacheParams);
mRetainFragment.setObject(imageCache);
}
return imageCache;
}
/**
* Initialize the cache, providing all parameters.
*
* @param context The context to use
* @param cacheParams The cache parameters to initialize the cache
*/
private void init(Context context, ImageCacheParams cacheParams) {
final File diskCacheDir = DiskLruCache.getDiskCacheDir(context, cacheParams.uniqueName);
// Set up disk cache
if (cacheParams.diskCacheEnabled) {
mDiskCache = DiskLruCache.openCache(context, diskCacheDir, cacheParams.diskCacheSize);
mDiskCache.setCompressParams(cacheParams.compressFormat, cacheParams.compressQuality);
if (cacheParams.clearDiskCacheOnStart) {
mDiskCache.clearCache();
}
}
// Set up memory cache
if (cacheParams.memoryCacheEnabled) {
mMemoryCache = new LruCache<String, Bitmap>(cacheParams.memCacheSize) {
/**
* Measure item size in bytes rather than units which is more practical for a bitmap
* cache
*/
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return Utils.getBitmapSize(bitmap);
}
};
}
}
public void addBitmapToCache(String data, Bitmap bitmap) {
if (data == null || bitmap == null) {
return;
}
// Add to memory cache
if (mMemoryCache != null && mMemoryCache.get(data) == null) {
mMemoryCache.put(data, bitmap);
}
// Add to disk cache
if (mDiskCache != null && !mDiskCache.containsKey(data)) {
mDiskCache.put(data, bitmap);
}
}
/**
* Get from memory cache.
*
* @param data Unique identifier for which item to get
* @return The bitmap if found in cache, null otherwise
*/
public Bitmap getBitmapFromMemCache(String data) {
if (mMemoryCache != null) {
final Bitmap memBitmap = mMemoryCache.get(data);
if (memBitmap != null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Memory cache hit");
}
return memBitmap;
}
}
return null;
}
/**
* Get from disk cache.
*
* @param data Unique identifier for which item to get
* @return The bitmap if found in cache, null otherwise
*/
public Bitmap getBitmapFromDiskCache(String data) {
if (mDiskCache != null) {
return mDiskCache.get(data);
}
return null;
}
public void clearCaches() {
mDiskCache.clearCache();
mMemoryCache.evictAll();
}
/**
* A holder class that contains cache parameters.
*/
public static class ImageCacheParams {
public String uniqueName;
public int memCacheSize = DEFAULT_MEM_CACHE_SIZE;
public int diskCacheSize = DEFAULT_DISK_CACHE_SIZE;
public CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT;
public int compressQuality = DEFAULT_COMPRESS_QUALITY;
public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED;
public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED;
public boolean clearDiskCacheOnStart = DEFAULT_CLEAR_DISK_CACHE_ON_START;
public ImageCacheParams(String uniqueName) {
this.uniqueName = uniqueName;
}
}
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.util.Log;
import android.widget.Toast;
import com.example.android.bitmapfun.BuildConfig;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* A simple subclass of {@link ImageResizer} that fetches and resizes images fetched from a URL.
*/
public class ImageFetcher extends ImageResizer {
private static final String TAG = "ImageFetcher";
private static final int HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10MB
public static final String HTTP_CACHE_DIR = "http";
/**
* Initialize providing a target image width and height for the processing images.
*
* @param context
* @param imageWidth
* @param imageHeight
*/
public ImageFetcher(Context context, int imageWidth, int imageHeight) {
super(context, imageWidth, imageHeight);
init(context);
}
/**
* Initialize providing a single target image size (used for both width and height);
*
* @param context
* @param imageSize
*/
public ImageFetcher(Context context, int imageSize) {
super(context, imageSize);
init(context);
}
private void init(Context context) {
checkConnection(context);
}
/**
* Simple network connection check.
*
* @param context
*/
private void checkConnection(Context context) {
final ConnectivityManager cm =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
if (networkInfo == null || !networkInfo.isConnectedOrConnecting()) {
Toast.makeText(context, "No network connection found.", Toast.LENGTH_LONG).show();
Log.e(TAG, "checkConnection - no connection found");
}
}
/**
* The main process method, which will be called by the ImageWorker in the AsyncTask background
* thread.
*
* @param data The data to load the bitmap, in this case, a regular http URL
* @return The downloaded and resized bitmap
*/
private Bitmap processBitmap(String data) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "processBitmap - " + data);
}
// Download a bitmap, write it to a file
final File f = downloadBitmap(mContext, data);
if (f != null) {
// Return a sampled down version
return decodeSampledBitmapFromFile(f.toString(), mImageWidth, mImageHeight);
}
return null;
}
@Override
protected Bitmap processBitmap(Object data) {
return processBitmap(String.valueOf(data));
}
/**
* Download a bitmap from a URL, write it to a disk and return the File pointer. This
* implementation uses a simple disk cache.
*
* @param context The context to use
* @param urlString The URL to fetch
* @return A File pointing to the fetched bitmap
*/
public static File downloadBitmap(Context context, String urlString) {
final File cacheDir = DiskLruCache.getDiskCacheDir(context, HTTP_CACHE_DIR);
final DiskLruCache cache =
DiskLruCache.openCache(context, cacheDir, HTTP_CACHE_SIZE);
final File cacheFile = new File(cache.createFilePath(urlString));
if (cache.containsKey(urlString)) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "downloadBitmap - found in http cache - " + urlString);
}
return cacheFile;
}
if (BuildConfig.DEBUG) {
Log.d(TAG, "downloadBitmap - downloading - " + urlString);
}
Utils.disableConnectionReuseIfNecessary();
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
final InputStream in =
new BufferedInputStream(urlConnection.getInputStream(), Utils.IO_BUFFER_SIZE);
out = new BufferedOutputStream(new FileOutputStream(cacheFile), Utils.IO_BUFFER_SIZE);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return cacheFile;
} catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmap - " + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
if (out != null) {
try {
out.close();
} catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmap - " + e);
}
}
}
return null;
}
}

View File

@@ -0,0 +1,198 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.util;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
import com.example.android.bitmapfun.BuildConfig;
/**
* A simple subclass of {@link ImageWorker} that resizes images from resources given a target width
* and height. Useful for when the input images might be too large to simply load directly into
* memory.
*/
public class ImageResizer extends ImageWorker {
private static final String TAG = "ImageWorker";
protected int mImageWidth;
protected int mImageHeight;
/**
* Initialize providing a single target image size (used for both width and height);
*
* @param context
* @param imageWidth
* @param imageHeight
*/
public ImageResizer(Context context, int imageWidth, int imageHeight) {
super(context);
setImageSize(imageWidth, imageHeight);
}
/**
* Initialize providing a single target image size (used for both width and height);
*
* @param context
* @param imageSize
*/
public ImageResizer(Context context, int imageSize) {
super(context);
setImageSize(imageSize);
}
/**
* Set the target image width and height.
*
* @param width
* @param height
*/
public void setImageSize(int width, int height) {
mImageWidth = width;
mImageHeight = height;
}
/**
* Set the target image size (width and height will be the same).
*
* @param size
*/
public void setImageSize(int size) {
setImageSize(size, size);
}
/**
* The main processing method. This happens in a background task. In this case we are just
* sampling down the bitmap and returning it from a resource.
*
* @param resId
* @return
*/
private Bitmap processBitmap(int resId) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "processBitmap - " + resId);
}
return decodeSampledBitmapFromResource(
mContext.getResources(), resId, mImageWidth, mImageHeight);
}
@Override
protected Bitmap processBitmap(Object data) {
return processBitmap(Integer.parseInt(String.valueOf(data)));
}
/**
* Decode and sample down a bitmap from resources to the requested width and height.
*
* @param res The resources object containing the image data
* @param resId The resource id of the image data
* @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions
* that are equal to or greater than the requested width and height
*/
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
/**
* Decode and sample down a bitmap from a file to the requested width and height.
*
* @param filename The full path of the file to decode
* @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions
* that are equal to or greater than the requested width and height
*/
public static synchronized Bitmap decodeSampledBitmapFromFile(String filename,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filename, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(filename, options);
}
/**
* Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding
* bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates
* the closest inSampleSize that will result in the final decoded bitmap having a width and
* height equal to or larger than the requested width and height. This implementation does not
* ensure a power of 2 is returned for inSampleSize which can be faster when decoding but
* results in a larger bitmap which isn't as useful for caching purposes.
*
* @param options An options object with out* params already populated (run through a decode*
* method with inJustDecodeBounds==true
* @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap
* @return The value to be used for inSampleSize
*/
public static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
if (width > height) {
inSampleSize = Math.round((float) height / (float) reqHeight);
} else {
inSampleSize = Math.round((float) width / (float) reqWidth);
}
// This offers some additional logic in case the image has a strange
// aspect ratio. For example, a panorama may have a much larger
// width than height. In these cases the total pixels might still
// end up being too large to fit comfortably in memory, so we should
// be more aggressive with sample down the image (=larger
// inSampleSize).
final float totalPixels = width * height;
// Anything more than 2x the requested pixels we'll sample down
// further.
final float totalReqPixelsCap = reqWidth * reqHeight * 2;
while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
inSampleSize++;
}
}
return inSampleSize;
}
}

View File

@@ -0,0 +1,364 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.util;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.ImageView;
import com.example.android.bitmapfun.BuildConfig;
import java.lang.ref.WeakReference;
/**
* This class wraps up completing some arbitrary long running work when loading a bitmap to an
* ImageView. It handles things like using a memory and disk cache, running the work in a background
* thread and setting a placeholder image.
*/
public abstract class ImageWorker {
private static final String TAG = "ImageWorker";
private static final int FADE_IN_TIME = 200;
private ImageCache mImageCache;
private Bitmap mLoadingBitmap;
private boolean mFadeInBitmap = true;
private boolean mExitTasksEarly = false;
protected Context mContext;
protected ImageWorkerAdapter mImageWorkerAdapter;
protected ImageWorker(Context context) {
mContext = context;
}
/**
* Load an image specified by the data parameter into an ImageView (override
* {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and disk
* cache will be used if an {@link ImageCache} has been set using
* {@link ImageWorker#setImageCache(ImageCache)}. If the image is found in the memory cache, it
* is set immediately, otherwise an {@link AsyncTask} will be created to asynchronously load the
* bitmap.
*
* @param data The URL of the image to download.
* @param imageView The ImageView to bind the downloaded image to.
*/
public void loadImage(Object data, ImageView imageView) {
Bitmap bitmap = null;
if (mImageCache != null) {
bitmap = mImageCache.getBitmapFromMemCache(String.valueOf(data));
}
if (bitmap != null) {
// Bitmap found in memory cache
imageView.setImageBitmap(bitmap);
} else if (cancelPotentialWork(data, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(mContext.getResources(), mLoadingBitmap, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(data);
}
}
/**
* Load an image specified from a set adapter into an ImageView (override
* {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and disk
* cache will be used if an {@link ImageCache} has been set using
* {@link ImageWorker#setImageCache(ImageCache)}. If the image is found in the memory cache, it
* is set immediately, otherwise an {@link AsyncTask} will be created to asynchronously load the
* bitmap. {@link ImageWorker#setAdapter(ImageWorkerAdapter)} must be called before using this
* method.
*
* @param data The URL of the image to download.
* @param imageView The ImageView to bind the downloaded image to.
*/
public void loadImage(int num, ImageView imageView) {
if (mImageWorkerAdapter != null) {
loadImage(mImageWorkerAdapter.getItem(num), imageView);
} else {
throw new NullPointerException("Data not set, must call setAdapter() first.");
}
}
/**
* Set placeholder bitmap that shows when the the background thread is running.
*
* @param bitmap
*/
public void setLoadingImage(Bitmap bitmap) {
mLoadingBitmap = bitmap;
}
/**
* Set placeholder bitmap that shows when the the background thread is running.
*
* @param resId
*/
public void setLoadingImage(int resId) {
mLoadingBitmap = BitmapFactory.decodeResource(mContext.getResources(), resId);
}
/**
* Set the {@link ImageCache} object to use with this ImageWorker.
*
* @param cacheCallback
*/
public void setImageCache(ImageCache cacheCallback) {
mImageCache = cacheCallback;
}
public ImageCache getImageCache() {
return mImageCache;
}
/**
* If set to true, the image will fade-in once it has been loaded by the background thread.
*
* @param fadeIn
*/
public void setImageFadeIn(boolean fadeIn) {
mFadeInBitmap = fadeIn;
}
public void setExitTasksEarly(boolean exitTasksEarly) {
mExitTasksEarly = exitTasksEarly;
}
/**
* Subclasses should override this to define any processing or work that must happen to produce
* the final bitmap. This will be executed in a background thread and be long running. For
* example, you could resize a large bitmap here, or pull down an image from the network.
*
* @param data The data to identify which image to process, as provided by
* {@link ImageWorker#loadImage(Object, ImageView)}
* @return The processed bitmap
*/
protected abstract Bitmap processBitmap(Object data);
public static void cancelWork(ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
bitmapWorkerTask.cancel(true);
if (BuildConfig.DEBUG) {
final Object bitmapData = bitmapWorkerTask.data;
Log.d(TAG, "cancelWork - cancelled work for " + bitmapData);
}
}
}
/**
* Returns true if the current work has been canceled or if there was no work in
* progress on this image view.
* Returns false if the work in progress deals with the same data. The work is not
* stopped in that case.
*/
public static boolean cancelPotentialWork(Object data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final Object bitmapData = bitmapWorkerTask.data;
if (bitmapData == null || !bitmapData.equals(data)) {
bitmapWorkerTask.cancel(true);
if (BuildConfig.DEBUG) {
Log.d(TAG, "cancelPotentialWork - cancelled work for " + data);
}
} else {
// The same work is already in progress.
return false;
}
}
return true;
}
/**
* @param imageView Any imageView
* @return Retrieve the currently active work task (if any) associated with this imageView.
* null if there is no such task.
*/
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}
/**
* The actual AsyncTask that will asynchronously process the image.
*/
private class BitmapWorkerTask extends AsyncTask<Object, Void, Bitmap> {
private Object data;
private final WeakReference<ImageView> imageViewReference;
public BitmapWorkerTask(ImageView imageView) {
imageViewReference = new WeakReference<ImageView>(imageView);
}
/**
* Background processing.
*/
@Override
protected Bitmap doInBackground(Object... params) {
data = params[0];
final String dataString = String.valueOf(data);
Bitmap bitmap = null;
// If the image cache is available and this task has not been cancelled by another
// thread and the ImageView that was originally bound to this task is still bound back
// to this task and our "exit early" flag is not set then try and fetch the bitmap from
// the cache
if (mImageCache != null && !isCancelled() && getAttachedImageView() != null
&& !mExitTasksEarly) {
bitmap = mImageCache.getBitmapFromDiskCache(dataString);
}
// If the bitmap was not found in the cache and this task has not been cancelled by
// another thread and the ImageView that was originally bound to this task is still
// bound back to this task and our "exit early" flag is not set, then call the main
// process method (as implemented by a subclass)
if (bitmap == null && !isCancelled() && getAttachedImageView() != null
&& !mExitTasksEarly) {
bitmap = processBitmap(params[0]);
}
// If the bitmap was processed and the image cache is available, then add the processed
// bitmap to the cache for future use. Note we don't check if the task was cancelled
// here, if it was, and the thread is still running, we may as well add the processed
// bitmap to our cache as it might be used again in the future
if (bitmap != null && mImageCache != null) {
mImageCache.addBitmapToCache(dataString, bitmap);
}
return bitmap;
}
/**
* Once the image is processed, associates it to the imageView
*/
@Override
protected void onPostExecute(Bitmap bitmap) {
// if cancel was called on this task or the "exit early" flag is set then we're done
if (isCancelled() || mExitTasksEarly) {
bitmap = null;
}
final ImageView imageView = getAttachedImageView();
if (bitmap != null && imageView != null) {
setImageBitmap(imageView, bitmap);
}
}
/**
* Returns the ImageView associated with this task as long as the ImageView's task still
* points to this task as well. Returns null otherwise.
*/
private ImageView getAttachedImageView() {
final ImageView imageView = imageViewReference.get();
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (this == bitmapWorkerTask) {
return imageView;
}
return null;
}
}
/**
* A custom Drawable that will be attached to the imageView while the work is in progress.
* Contains a reference to the actual worker task, so that it can be stopped if a new binding is
* required, and makes sure that only the last started worker process can bind its result,
* independently of the finish order.
*/
private static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
/**
* Called when the processing is complete and the final bitmap should be set on the ImageView.
*
* @param imageView
* @param bitmap
*/
private void setImageBitmap(ImageView imageView, Bitmap bitmap) {
if (mFadeInBitmap) {
// Transition drawable with a transparent drwabale and the final bitmap
final TransitionDrawable td =
new TransitionDrawable(new Drawable[] {
new ColorDrawable(android.R.color.transparent),
new BitmapDrawable(mContext.getResources(), bitmap)
});
// Set background to loading bitmap
imageView.setBackgroundDrawable(
new BitmapDrawable(mContext.getResources(), mLoadingBitmap));
imageView.setImageDrawable(td);
td.startTransition(FADE_IN_TIME);
} else {
imageView.setImageBitmap(bitmap);
}
}
/**
* Set the simple adapter which holds the backing data.
*
* @param adapter
*/
public void setAdapter(ImageWorkerAdapter adapter) {
mImageWorkerAdapter = adapter;
}
/**
* Get the current adapter.
*
* @return
*/
public ImageWorkerAdapter getAdapter() {
return mImageWorkerAdapter;
}
/**
* A very simple adapter for use with ImageWorker class and subclasses.
*/
public static abstract class ImageWorkerAdapter {
public abstract Object getItem(int num);
public abstract int getSize();
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.util;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
/**
* A simple non-UI Fragment that stores a single Object and is retained over configuration changes.
* In this sample it will be used to retain the ImageCache object.
*/
public class RetainFragment extends Fragment {
private static final String TAG = "RetainFragment";
private Object mObject;
/**
* Empty constructor as per the Fragment documentation
*/
public RetainFragment() {}
/**
* Locate an existing instance of this Fragment or if not found, create and
* add it using FragmentManager.
*
* @param fm The FragmentManager manager to use.
* @return The existing instance of the Fragment or the new instance if just
* created.
*/
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
// Check to see if we have retained the worker fragment.
RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG);
// If not retained (or first time running), we need to create and add it.
if (mRetainFragment == null) {
mRetainFragment = new RetainFragment();
fm.beginTransaction().add(mRetainFragment, TAG).commit();
}
return mRetainFragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Make sure this Fragment is retained over a configuration change
setRetainInstance(true);
}
/**
* Store a single object in this Fragment.
*
* @param object The object to store
*/
public void setObject(Object object) {
mObject = object;
}
/**
* Get the stored object.
*
* @return The stored object
*/
public Object getObject() {
return mObject;
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.util;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Environment;
import android.os.StatFs;
import java.io.File;
/**
* Class containing some static utility methods.
*/
public class Utils {
public static final int IO_BUFFER_SIZE = 8 * 1024;
private Utils() {};
/**
* Workaround for bug pre-Froyo, see here for more info:
* http://android-developers.blogspot.com/2011/09/androids-http-clients.html
*/
public static void disableConnectionReuseIfNecessary() {
// HTTP connection reuse which was buggy pre-froyo
if (hasHttpConnectionBug()) {
System.setProperty("http.keepAlive", "false");
}
}
/**
* Get the size in bytes of a bitmap.
* @param bitmap
* @return size in bytes
*/
@SuppressLint("NewApi")
public static int getBitmapSize(Bitmap bitmap) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
return bitmap.getByteCount();
}
// Pre HC-MR1
return bitmap.getRowBytes() * bitmap.getHeight();
}
/**
* Check if external storage is built-in or removable.
*
* @return True if external storage is removable (like an SD card), false
* otherwise.
*/
@SuppressLint("NewApi")
public static boolean isExternalStorageRemovable() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
return Environment.isExternalStorageRemovable();
}
return true;
}
/**
* Get the external app cache directory.
*
* @param context The context to use
* @return The external cache dir
*/
@SuppressLint("NewApi")
public static File getExternalCacheDir(Context context) {
if (hasExternalCacheDir()) {
return context.getExternalCacheDir();
}
// Before Froyo we need to construct the external cache dir ourselves
final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/";
return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir);
}
/**
* Check how much usable space is available at a given path.
*
* @param path The path to check
* @return The space available in bytes
*/
@SuppressLint("NewApi")
public static long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
return path.getUsableSpace();
}
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}
/**
* Get the memory class of this device (approx. per-app memory limit)
*
* @param context
* @return
*/
public static int getMemoryClass(Context context) {
return ((ActivityManager) context.getSystemService(
Context.ACTIVITY_SERVICE)).getMemoryClass();
}
/**
* Check if OS version has a http URLConnection bug. See here for more information:
* http://android-developers.blogspot.com/2011/09/androids-http-clients.html
*
* @return
*/
public static boolean hasHttpConnectionBug() {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO;
}
/**
* Check if OS version has built-in external cache dir method.
*
* @return
*/
public static boolean hasExternalCacheDir() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO;
}
/**
* Check if ActionBar is available.
*
* @return
*/
public static boolean hasActionBar() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
}
}