I'm developing an Activity which loads a list of URLs of images and displays them in a Gallery view. For better performance I decided to asynchronously load the images and cache them on the SD card.
I found this:
http://blog.jteam.nl/2009/09/17/exploring-the-world-of-android-part-2/
and adapted the code. As long as I didn't cache the files on the SD card everything worked fine, but now I see a strange behavior. I guess it's easier to describe the problem with code (see comments):
public class AsyncImageLoader {
private final static String TAG = "AsyncImageLoader";
private HashMap<String, SoftReference<Bitmap>> bitmapMap;
public AsyncImageLoader() {
this.bitmapMap = new HashMap<String, SoftReference<Bitmap>>();
}
// This method is called to load images asynchronously.
// If the Bitmap is cached in bitmapMap and the reference is
// still valid, the Bitmap is returned immediately.
// If the Bitmap is not in bitmapMap, another thread is started
// to load the image. Once it has been loaded, a callback method
// is called.
public Bitmap loadBitmap(final String imageUrl,
final IImageLoadListener imageCallback, final int minWidth,
final int minHeight) {
if (this.bitmapMap.containsKey(imageUrl)) {
SoftReference<Bitmap> softReference = this.bitmapMap.get(imageUrl);
Bitmap bitmap = softReference.get();
if (bitmap != null) {
Log.d(TAG, "Using a previously loaded Bitmap container");
return bitmap;
}
}
Log.d(TAG, "Need to load the Bitmap container");
final Handler handler = new Handler() {
#Override
public void handleMessage(Message message) {
imageCallback.imageLoaded((Bitmap) message.obj, imageUrl);
}
};
new Thread() {
#Override
public void run() {
Bitmap bitmap = loadImageFromUrl(imageUrl, minWidth, minHeight);
bitmapMap.put(imageUrl, new SoftReference<Bitmap>(bitmap));
Message message = handler.obtainMessage(0, bitmap);
handler.sendMessage(message);
}
}.start();
return null;
}
// Here is part of the magic behind the scenes:
// I get an InputStream (see below) and open it just
// to get the Bitmap's dimensions. Then I calculate the
// required width (best size for minWidth and minHeight)
// and then I create a downsampled Bitmap.
private synchronized static Bitmap loadImageFromUrl(String urlString,
int minWidth, int minHeight) {
Bitmap bitmap = null;
try {
InputStream is = getInputStream(urlString);
BitmapFactory.Options options1 = new BitmapFactory.Options();
options1.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null,
options1);
is.close();
int tempWidth = options1.outWidth;
int tempHeight = options1.outHeight;
int scale = 1;
while (true) {
if (tempWidth / 2 < minWidth || tempHeight / 2 < minHeight) {
break;
}
tempWidth /= 2;
tempHeight /= 2;
scale *= 2;
}
BitmapFactory.Options options2 = new BitmapFactory.Options();
options2.inSampleSize = scale;
bitmap = BitmapFactory.decodeStream(getInputStream(urlString),
null, options2);
// More magic: Once the Bitmap has been downloaded,
// I create a file on the SD card so that I don't
// need to download it again.
File cacheFile = getCacheFile(urlString);
if (cacheFile == null) {
cacheImage(urlString, bitmap);
}
} catch (IOException e) {
Log.e(TAG, e.getMessage(), e);
}
return bitmap;
}
// I check whether a cache file exists for the URL. If it exists
// I create the InputStream from this file and I create the InputStream
// from the URL otherwise.
private synchronized static InputStream getInputStream(String urlString)
throws IOException {
File cacheFile = getCacheFile(urlString);
if (cacheFile != null) {
// HERE seems to be something wrong. The file
// is valid and can be read, but if I return a
// FileInputStream (or InputStream using
// cacheFile.toURL().openStream()) the image is not
// shown.
Log.d(TAG, "Using " + cacheFile.getAbsolutePath());
return new FileInputStream(cacheFile);
} else {
// In this case, the image is shown everytime!
Log.d(TAG, "Downloading " + urlString);
URL url = new URL(urlString);
return url.openStream();
}
}
// This is just a helper method that returns the cache directory
// and if it doesn't exist it will be created first.
private synchronized static File getImageCacheDir() {
File imageCacheDir = new File(Environment.getExternalStorageDirectory()
.getAbsolutePath() + "/" + Commons.APP_DIRECTORY + "/cache/");
if (!imageCacheDir.exists()) {
imageCacheDir.mkdirs();
}
return imageCacheDir;
}
// This method returns a File object for the cache file if it exists
// and returns null otherwise.
private static File getCacheFile(String urlString) {
File file = new File(getImageCacheDir(), CryptoUtils.md5(urlString)
+ ".jpg");
if (!file.exists()) {
return null;
}
return file;
}
// Even more magic:
// At first I create a file with a .tmp extension just in case two
// threads try to read and write at the same time. The image is
// downloaded into the temporary file. If this succeeds the .tmp
// extension will be removed.
private synchronized static void cacheImage(String urlString, Bitmap bitmap) {
String filename = CryptoUtils.md5(urlString) + ".jpg";
File tmpFile = new File(getImageCacheDir(), filename + ".tmp");
if (tmpFile.exists()) {
Log.d(TAG, "Another process seems to create the cache file.");
return;
}
File file = new File(getImageCacheDir(), filename);
FileOutputStream os = null;
try {
os = new FileOutputStream(tmpFile);
boolean retval = bitmap
.compress(Bitmap.CompressFormat.JPEG, 90, os);
if (retval) {
tmpFile.renameTo(file);
Log.d(TAG, "Created cache image. Renaming temporary file.");
} else {
tmpFile.delete();
Log.d(TAG,
"Could not create cache image. Deleting temporary file.");
}
} catch (FileNotFoundException e) {
Log.e(TAG, e.getMessage(), e);
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
Log.e(TAG, e.getMessage(), e);
}
}
}
}
public interface IImageLoadListener {
public void imageLoaded(Bitmap imageBitmap, String imageUrl);
}
}
Here is another big chunk of code:
public class DisplayPhotoActivity extends Activity {
private final static int PROGRESS_DIALOG = 1;
private final static int PHOTO_PICKER = 2;
private final static int GALLERY_REQUEST_CODE = 1;
private final static int CAMERA_REQUEST_CODE = 2;
private AsyncImageLoader asyncImageLoader;
private Gallery gallery;
private List<Photo> images;
private ImageAdapter imageAdapter;
private int loadingCounter;
private Uri outputFileUri;
private Display display;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.display_photo);
this.asyncImageLoader = new AsyncImageLoader();
this.loadingCounter = 0;
this.images = new ArrayList<Photo>();
this.imageAdapter = new ImageAdapter(this, this.images);
final ImageView imageView = (ImageView) findViewById(R.id.large_image);
this.gallery = (Gallery) findViewById(R.id.gallery);
this.gallery.setAdapter(this.imageAdapter);
// The Gallery is on top of the Activity and there is a bigger
// ImageView below it. I want the image to be shown in the bigger
// ImageView when it has been clicked in the Gallery.
this.gallery.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View v,
int position, long id) {
// Here is the part where I load the Bitmap.
// This part works!
Bitmap bitmap = asyncImageLoader.loadBitmap(images
.get(position).getUrl(),
new AsyncImageLoader.IImageLoadListener() {
public void imageLoaded(Bitmap imageBitmap,
String imageUrl) {
if (imageBitmap != null) {
imageView.setImageBitmap(imageBitmap);
gallery.invalidate();
}
loadingCounter--;
if (loadingCounter == 0) {
getParent()
.setProgressBarIndeterminateVisibility(
false);
}
}
}, 300, 300);
if (bitmap != null) {
Log.d("DisplayPhotoActivity", "Image was cached. (I)");
imageView.setImageBitmap(bitmap);
gallery.invalidate();
loadingCounter--;
if (loadingCounter == 0) {
getParent()
.setProgressBarIndeterminateVisibility(false);
}
}
}
});
this.display = (Display) getIntent().getParcelableExtra("display");
populatePhotos(true);
}
// There is a known bug and this is the bug fix.
// See: http://code.google.com/p/android/issues/detail?id=8488
#Override
public void onPause() {
super.onPause();
System.gc();
}
#Override
public void onDestroy() {
super.onDestroy();
this.asyncImageLoader = null;
System.gc();
}
#Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case PROGRESS_DIALOG:
ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.setMessage("Loading...");
return progressDialog;
case PHOTO_PICKER:
final CharSequence[] items = {
getText(R.string.photo_picker_choose_from_gallery),
getText(R.string.photo_picker_take_a_photo) };
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getText(R.string.photo_picker_title));
builder.setItems(items, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int item) {
switch (item) {
case 0:
onChooseFromGalleryClick();
break;
case 1:
onTakeAPhoto();
}
}
});
return builder.create();
default:
return null;
}
}
private void onChooseFromGalleryClick() {
Intent intent = new Intent();
intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(Intent.createChooser(intent,
getText(R.string.photo_picker_choose_from_gallery)),
GALLERY_REQUEST_CODE);
}
private void onTakeAPhoto() {
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File appDirectory = new File(Environment.getExternalStorageDirectory(),
Commons.APP_DIRECTORY);
if (!appDirectory.exists()) {
appDirectory.mkdirs();
}
File photo = new File(appDirectory, "/pic.jpg");
this.outputFileUri = Uri.fromFile(photo);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, this.outputFileUri);
startActivityForResult(Intent.createChooser(cameraIntent,
getText(R.string.photo_picker_take_a_photo)),
CAMERA_REQUEST_CODE);
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuItem updateMenuItem = menu.add(this.getResources().getString(
R.string.update_menuitem_text));
updateMenuItem
.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
populatePhotos(true);
return false;
}
});
updateMenuItem.setIcon(R.drawable.ic_menu_refresh);
MenuItem uploadPhotoMenuItem = menu.add(this.getResources().getString(
R.string.photo_picker_title));
uploadPhotoMenuItem
.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
showDialog(PHOTO_PICKER);
return true;
}
});
uploadPhotoMenuItem.setIcon(getResources().getDrawable(
android.R.drawable.ic_menu_add));
return true;
}
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
Uri uri = null;
switch (requestCode) {
case GALLERY_REQUEST_CODE:
uri = data.getData();
break;
case CAMERA_REQUEST_CODE:
uri = this.outputFileUri;
break;
}
if (uri == null) {
return;
}
Display display = this.getIntent().getParcelableExtra("display");
this.startService(PhotoUploadService.getIntent(this, display, uri));
}
}
private void populatePhotos(boolean forceSync) {
new ImageListLoader().execute(this.display.getId(), forceSync ? 1 : 0);
}
private void updateInitialImage(Bitmap bitmap) {
ImageView imageView = (ImageView) findViewById(R.id.large_image);
if (imageView.getDrawable() == null) {
Log.d("DisplayPhotoActivity", "No initial image set.");
imageView.setImageBitmap(bitmap);
}
}
public class ImageListLoader extends AsyncTask<Integer, Void, Void> {
#Override
protected void onPreExecute() {
showDialog(PROGRESS_DIALOG);
}
#Override
protected Void doInBackground(Integer... params) {
if (params.length < 1) {
return null;
}
int displayId = params[0];
boolean forceSync = params.length == 2 && params[1] == 1;
List<Photo> photos = LocalDatabaseHelper.getInstance()
.getPhotosByDisplayId(displayId, forceSync);
images.clear();
for (Photo photo : photos) {
images.add(photo);
}
Log.d("FOO", "Size: " + images.size());
runOnUiThread(new Runnable() {
public void run() {
imageAdapter.notifyDataSetChanged();
}
});
return null;
}
#Override
protected void onPostExecute(Void result) {
dismissDialog(PROGRESS_DIALOG);
}
}
public class ImageAdapter extends BaseAdapter {
private Context context;
private List<Photo> images;
private int galleryItemBackground;
public ImageAdapter(Context context, List<Photo> images) {
this.context = context;
this.images = images;
TypedArray attr = this.context
.obtainStyledAttributes(R.styleable.DisplayPhotoActivity);
this.galleryItemBackground = attr
.getResourceId(
R.styleable.DisplayPhotoActivity_android_galleryItemBackground,
0);
attr.recycle();
}
public int getCount() {
return this.images.size();
}
public Object getItem(int position) {
return position;
}
public long getItemId(int position) {
return position;
}
public View getView(int position, View convertView, ViewGroup parent) {
getParent().setProgressBarIndeterminateVisibility(true);
loadingCounter++;
final ImageView imageView = new ImageView(this.context);
String imageUrl = this.images.get(position).getUrl();
Log.d("DisplayPhotoActivity", "getView(): " + imageUrl);
// Here is almost the same code as in the OnClickListener
// above, but this does not work as expected. If the image
// has never been cached (neither as a SoftReference nor as
// a file on the SD card) the image is downloaded and shown.
// No problem... But if the image is cached, it won't show up!
Bitmap bitmap = asyncImageLoader.loadBitmap(
imageUrl,
new AsyncImageLoader.IImageLoadListener() {
public void imageLoaded(Bitmap imageBitmap,
String imageUrl) {
Log.d("DisplayPhotoActivity", " - imageLoaded:");
if (imageBitmap != null) {
Log.d("DisplayPhotoActivity", " - imageBitmap != null");
updateInitialImage(imageBitmap);
Log.d("DisplayPhotoActivity", " - updateInitialImage(imageBitmap)");
imageView.setImageBitmap(imageBitmap);
Log.d("DisplayPhotoActivity", " - setImageBitmap(imageBitmap)");
int width = Math.round((150 * imageBitmap
.getWidth()) / imageBitmap.getHeight());
imageView
.setLayoutParams(new Gallery.LayoutParams(
width, 150));
//imageView.invalidate();
//gallery.invalidate();
}
loadingCounter--;
if (loadingCounter == 0) {
getParent()
.setProgressBarIndeterminateVisibility(
false);
}
}
}, 300, 300);
if (bitmap != null) {
// TODO: Something doesn't work here...
Log.d("DisplayPhotoActivity", "Image was cached");
Log.d("DisplayPhotoActivity",
bitmap.getWidth() + "x" + bitmap.getHeight());
updateInitialImage(bitmap);
imageView.setImageBitmap(bitmap);
int width = Math.round((150 * bitmap.getWidth())
/ bitmap.getHeight());
imageView.setLayoutParams(new Gallery.LayoutParams(width, 150));
imageView.invalidate();
gallery.invalidate();
loadingCounter--;
if (loadingCounter == 0) {
getParent().setProgressBarIndeterminateVisibility(false);
}
}
imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
imageView.setBackgroundResource(galleryItemBackground);
imageView.setImageDrawable(getResources().getDrawable(
R.drawable.loading));
imageView.setLayoutParams(new Gallery.LayoutParams(200, 150));
return imageView;
}
public float getScale(boolean focused, int offset) {
return Math.max(0, 1.0f / (float) Math.pow(2, Math.abs(offset)));
}
}
}
Here is the DDMS output:
07-25 02:34:58.496: DEBUG/DisplayPhotoActivity(23589): getView(): http://[...]/files/814efa535ed97cf44ea3dc3a1c15c3fb/1_1311540992715.jpg
07-25 02:34:58.496: DEBUG/AsyncImageLoader(23589): Need to load the Bitmap container
07-25 02:34:58.503: DEBUG/DisplayPhotoActivity(23589): getView(): http://[...]/files/814efa535ed97cf44ea3dc3a1c15c3fb/1_1311540992715.jpg
07-25 02:34:58.503: DEBUG/AsyncImageLoader(23589): Need to load the Bitmap container
07-25 02:35:01.031: DEBUG/AsyncImageLoader(23589): Created cache image. Renaming temporary file.
07-25 02:35:01.031: DEBUG/DisplayPhotoActivity(23589): - imageLoaded:
07-25 02:35:01.031: DEBUG/DisplayPhotoActivity(23589): - imageBitmap != null
07-25 02:35:01.031: DEBUG/DisplayPhotoActivity(23589): No initial image set.
07-25 02:35:01.031: DEBUG/DisplayPhotoActivity(23589): - updateInitialImage(imageBitmap)
07-25 02:35:01.031: DEBUG/DisplayPhotoActivity(23589): - setImageBitmap(imageBitmap)
07-25 02:35:01.597: DEBUG/AsyncImageLoader(23589): Using /mnt/sdcard/PDCollector/cache/4057d5bd1cd9ba7204371ec422ea884a.jpg
07-25 02:35:01.605: DEBUG/AsyncImageLoader(23589): Using /mnt/sdcard/PDCollector/cache/4057d5bd1cd9ba7204371ec422ea884a.jpg
07-25 02:35:01.800: DEBUG/AsyncImageLoader(23589): Using /mnt/sdcard/PDCollector/cache/4057d5bd1cd9ba7204371ec422ea884a.jpg
07-25 02:35:01.800: DEBUG/AsyncImageLoader(23589): Using /mnt/sdcard/PDCollector/cache/4057d5bd1cd9ba7204371ec422ea884a.jpg
07-25 02:35:01.804: DEBUG/DisplayPhotoActivity(23589): - imageLoaded:
07-25 02:35:01.804: DEBUG/DisplayPhotoActivity(23589): - imageBitmap != null
07-25 02:35:01.804: DEBUG/DisplayPhotoActivity(23589): No initial image set.
07-25 02:35:01.804: DEBUG/DisplayPhotoActivity(23589): - updateInitialImage(imageBitmap)
07-25 02:35:01.804: DEBUG/DisplayPhotoActivity(23589): - setImageBitmap(imageBitmap)
07-25 02:35:02.027: DEBUG/AsyncImageLoader(23589): Using /mnt/sdcard/PDCollector/cache/4057d5bd1cd9ba7204371ec422ea884a.jpg
07-25 02:35:02.027: DEBUG/AsyncImageLoader(23589): Using /mnt/sdcard/PDCollector/cache/4057d5bd1cd9ba7204371ec422ea884a.jpg
07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): - imageLoaded:
07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): - imageBitmap != null
07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): - updateInitialImage(imageBitmap)
07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): - setImageBitmap(imageBitmap)
07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): getView(): http://[...]/files/814efa535ed97cf44ea3dc3a1c15c3fb/1_1311540992715.jpg
07-25 02:35:02.082: DEBUG/AsyncImageLoader(23589): Using a previously loaded Bitmap container
07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): Image was cached
07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): 640x480
07-25 02:35:02.257: DEBUG/DisplayPhotoActivity(23589): - imageLoaded:
07-25 02:35:02.257: DEBUG/DisplayPhotoActivity(23589): - imageBitmap != null
07-25 02:35:02.257: DEBUG/DisplayPhotoActivity(23589): - updateInitialImage(imageBitmap)
07-25 02:35:02.257: DEBUG/DisplayPhotoActivity(23589): - setImageBitmap(imageBitmap)
I hope someone can explain this strange behavior. Thanks in advance.
Related
I have used the async task to download the data and then showing those images in the list view I used the cache to store the images as they are repeating but not in an order. But the images gets jumbled up and sometimes they don't download. I tried searching this, but couldn't find that.
This was one of mine dream company question and i didn't clear because of this.
Please help me around.
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private static final String BASE_URL = "https://ajax.googleapis.com/ajax/services/search/images?v=1.0&q=android&start=";
private static final String IMAGE_JSON_KEY = "unescapedUrl";
private static final String RESULTS_JSON_KEY = "results";
private static final String RESPONSE_DATA_JSON_KEY = "responseData";
private int mCurrentPage;
private ListView mListView;
private Context mContext;
ArrayList<String> mImageUrls;
private LruCache<String, Bitmap> mCache;
private CustomListAdapter mAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Get memory class of this device, exceeding this amount will throw an
// OutOfMemory exception.
final int memClass = ((ActivityManager) getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = 1024 * 1024 * memClass / 8;
// Initialize variables
mContext = this;
mCache = new LruCache<String, Bitmap>(cacheSize) {
#Override
protected int sizeOf(String key, Bitmap value) {
// The cache size will be measured in bytes rather than number of items.
return value.getByteCount();
}
};
mListView = (ListView) findViewById(R.id.list_view);
mAdapter = new CustomListAdapter();
mImageUrls = new ArrayList<>();
// If the urls are wrong then
if (mImageUrls.isEmpty() || checkDiff()) {
fetchNewImageUrls();
}
// Set the adapter to the list view
mListView.setAdapter(mAdapter);
}
class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private String mUrl;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView);
}
#Override
protected Bitmap doInBackground(String... params) {
String imageUrl = params[0];
mUrl = imageUrl;
Bitmap bitmap = getBitmapFromMemCache(imageUrl);
if (bitmap == null) {
try {
URL url = new URL(imageUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
connection.connect();
// Closing the stream after getting the sample size
InputStream inputStream = connection.getInputStream();
byte[] imageByteArray = convertToByteArray(inputStream);
bitmap = decodeSampledBitmap(imageByteArray, 200, 200);
inputStream.close();
if (bitmap != null) {
Log.d(TAG, "Image downloaded: " + imageUrl);
addBitmapToMemoryCache(imageUrl, bitmap);
} else {
Log.d(TAG, "Null Bitmap downloaded for: " + imageUrl);
}
connection.disconnect();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
} else {
Log.d(TAG, "Already present in the Cache");
}
return bitmap;
}
#Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = (ImageView) imageViewReference.get();
if (imageView != null && imageView.getTag().equals(mUrl)) {
imageView.setImageBitmap(bitmap);
}
}
}
}
public class CustomListAdapter extends BaseAdapter {
#Override
public int getCount() {
return mImageUrls.size();
}
#Override
public Object getItem(int position) {
return mImageUrls.get(position);
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
Log.d(TAG, "Get View is called for position: " + position);
View view = convertView;
Holder holder = null;
// Holder represents the elements of the view to use
// Here are initialized
if (view == null) {
view = LayoutInflater.from(mContext).inflate(R.layout.row_item, parent, false);
holder = new Holder();
holder.mRowImage = (ImageView) view.findViewById(R.id.image_view);
holder.mRowText = (TextView) view.findViewById(R.id.row_text);
view.setTag(holder);
} else {
holder = (Holder) view.getTag();
}
// Set default image background
holder.mRowImage.setImageResource(R.drawable.ic_launcher);
// here do operations in holder variable example
holder.mRowText.setText("Image Number: " + position);
// Set the tag for the imageview
holder.mRowImage.setTag(mImageUrls.get(position));
new BitmapWorkerTask(holder.mRowImage).execute(mImageUrls.get(position));
return view;
}
}
public static class Holder {
TextView mRowText;
ImageView mRowImage;
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return (Bitmap) mCache.get(key);
}
public Bitmap decodeSampledBitmap(byte[] imageByteArray, int reqWidth, int reqHeight) {
Bitmap bm = null;
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(imageByteArray, 0, imageByteArray.length, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
bm = BitmapFactory.decodeByteArray(imageByteArray, 0, imageByteArray.length, options);
return bm;
}
public 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);
}
}
Log.d(TAG, "Sample size is: " + inSampleSize);
return inSampleSize;
}
private void fetchNewImageUrls() {
new AsyncTask<String, Void, Boolean>() {
#Override
protected Boolean doInBackground(String... params) {
try {
StringBuilder response = new StringBuilder();
URL url = new URL(params[0]);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.connect();
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null)
response.append(inputLine);
in.close();
connection.disconnect();
String resp = response.toString();
Log.d(TAG, "Response is: " + response);
// Parsing the response
JSONObject jsonObject = new JSONObject(resp);
JSONObject jsonObject1 = jsonObject.getJSONObject(RESPONSE_DATA_JSON_KEY);
JSONArray jsonArray = jsonObject1.getJSONArray(RESULTS_JSON_KEY);
for (int i = 0; i < 4; i++) {
JSONObject dataObject = jsonArray.getJSONObject(i);
mImageUrls.add(dataObject.getString(IMAGE_JSON_KEY));
}
mCurrentPage++;
Log.d(TAG, "Number of image urls are: " + mImageUrls.size());
return true;
} catch (JSONException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
#Override
protected void onPostExecute(Boolean value) {
super.onPostExecute(value);
if (checkDiff() && value) {
Log.d(TAG, "Again fetching the Images");
fetchNewImageUrls();
}
if (!value) {
Log.d(TAG, "Error while getting the response");
}
mAdapter.notifyDataSetChanged();
}
}.execute(BASE_URL + mCurrentPage);
}
private boolean checkDiff() {
int diff = mImageUrls.size() - mCurrentPage * 4;
Log.d(TAG, "Diff is: " + diff);
return diff < 8;
}
public static byte[] convertToByteArray(InputStream input) {
byte[] buffer = new byte[8192];
int bytesRead;
ByteArrayOutputStream output = new ByteArrayOutputStream();
try {
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
}
return output.toByteArray();
}
}
Since you do not cancel the unfinished "out of view" downloads these may interfere with your gui.
Example
listview line1 shows item#1 with has an async task to download image "a" not completed yet
scroll down, line1 is now invisible;
listview line8 becomes visible recycling item#1 new async task to download image "x"
async task to download image "a" finishes displaying wrong image. Line8 shows image "a" instead of "x"
to solve this you have to cancel the pending unnecessary unfinished async task-s
public static class Holder {
ImageView mRowImage;
String mImageUrl;
// neccessary to cancel unfinished download
BitmapWorkerTask mDownloader;
}
static class BitmapWorkerTask extends AsyncTask<Holder, Void, Bitmap> {
Holder mHolder;
protected Bitmap doInBackground(Holder... holders) {
mHolder = holders[0];
...
}
protected void onPostExecute(...) {
mHolder.mDownloader = null;
if (!isCancelled()) {
this.mHolder.mRowImage.setImageBitmap(image);
}
this.mHolder = null;
}
}
public class CustomListAdapter extends BaseAdapter {
#Override
public View getView(int position, ...) {
...
if (view == null) {
holder = ...
...
} else {
holder = (Holder) view.getTag();
}
...
// cancel unfinished mDownloader
if (holder.mDownloader != null) {
holder.mDownloader.cancel(false);
holder.mDownloader = null;
}
holder.mImageUrl = mImageUrls.get(position);
holder.mDownloader = new BitmapWorkerTask()
holder.mDownloader.execute(holder);
}
}
Here is a working example for this combination of Adapter + Holder + AsyncTask
[Update]
Potential problems with this solution.
Most modern android versions execute only one async task at a time. If you are fetching the images via the web you cannot download multible images at the same time. See running-parallel-asynctask#stackoverflow for details.
There may be promlems with configuration change(like orientation). See #Selvin-s comment below. I have posted this question "what-happens-with-view-references-in-unfinished-asynctask-after-orientation-chan#stackoverflow" to find out more about it.
Hi in the below I am displaying images from sdcard.sdcard means storing into locally.but while privew the images it was showing some black color after image got displaying.
want to display smooth preview with black screen.i am unable to figure out the issue.can any one help me.
java
public class ImageGallery extends Activity {
Bundle bundle;
String catid, temp, responseJson;
JSONObject json;
ImageView imageViewPager;
// for parsing
JSONObject o1, o2;
JSONArray a1, a2;
int k;
Boolean toggleTopBar;
ArrayList<String> imageThumbnails;
ArrayList<String> imageFull;
public static int imagePosition=0;
SubsamplingScaleImageView imageView, imageViewPreview, fullImage ;
ImageView thumb1, back;
private LinearLayout thumb2;
RelativeLayout topLayout, stripeView;
RelativeLayout thumbnailButtons;
FrameLayout gridFrame;
//SharedPreferences data
SharedPreferences s1;
SharedPreferences.Editor editor;
int swipeCounter;
ParsingForFinalImages parsingObject;
int position_grid;
SharedPreferences p;
Bitmap bm;
int numOfImagesInsidee;
LinearLayout backLinLayout;
public static boolean isThumb2=false;
public static boolean isThumb1=false;
public static ViewPager viewPager;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_image_gallery);
//isThumb2=false;
toggleTopBar = false;
//position_grid=getIntent().getExtras().getInt("position");
thumbnailButtons = (RelativeLayout)findViewById(R.id.thumbnailButtons);
topLayout = (RelativeLayout)findViewById(R.id.topLayout);
//fullImage = (SubsamplingScaleImageView)findViewById(R.id.fullimage);
backLinLayout = (LinearLayout)findViewById(R.id.lin_back);
backLinLayout.setOnClickListener(
new View.OnClickListener() {
#Override
public void onClick(View view) {
Intent io = new Intent(getBaseContext(), MainActivity.class);
// clear the previous activity and start a new task
// System.gc();
// io.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(io);
finish();
}
});
ConnectionDetector cd = new ConnectionDetector(getBaseContext());
Boolean isInternetPresent = cd.isConnectingToInternet();
thumb1 = (ImageView)findViewById(R.id.thumb1);
thumb2 = (LinearLayout)findViewById(R.id.thumb2);
stripeView = (RelativeLayout)findViewById(R.id.stripeView) ;
gridFrame = (FrameLayout)findViewById(R.id.gridFrame);
thumb1.setOnClickListener(
new View.OnClickListener() {
#Override
public void onClick(View view) {
stripeView.setVisibility(View.GONE);
gridFrame.setVisibility(View.VISIBLE);
viewPager.setVisibility(View.GONE);
//fullImage.setVisibility(View.GONE);
thumb1.setClickable(false);
isThumb1=true;
isThumb2=false;
Log.i("Thumb Position 1",""+ImageGallery.imagePosition);
viewPager.removeAllViews();
Fragment newFragment = new GridFragment2();
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.gridFrame, newFragment).commit();
}
});
thumb2.setOnClickListener(
new View.OnClickListener() {
#Override
public void onClick(View view) {
// stripeView.setVisibility(View.VISIBLE);
stripeView.setVisibility(View.GONE);
gridFrame.setVisibility(View.VISIBLE);
viewPager.setVisibility(View.GONE);
// fullImage.setVisibility(View.GONE);
thumb1.setClickable(true);
isThumb2=true;
isThumb1=false;
Log.i("Thumb Position 2",""+ImageGallery.imagePosition);
viewPager.removeAllViews();
Fragment newFragment = new ImageStripeFragment();
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.gridFrame, newFragment).commit();
}
});
// allow networking on main thread
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
StrictMode.setThreadPolicy(policy);
/*bundle = getIntent().getExtras();
catid = bundle.getString("catid");*/
// Toast.makeText(getBaseContext(), catid, Toast.LENGTH_LONG).show();
// making json using the catalogue id we got
p = getSharedPreferences("gridData", Context.MODE_APPEND);
catid = p.getString("SelectedCatalogueIdFromGrid1", "");
int clickedListPos = p.getInt("clickedPosition", 0);
imageViewPreview = (SubsamplingScaleImageView)findViewById(R.id.preview);
imageThumbnails = new ArrayList<String>();
imageFull = new ArrayList<String>();
s1 = this.getSharedPreferences("data", Context.MODE_APPEND);
editor = s1.edit();
Log.d("catidfnl", catid);
numOfImagesInsidee = p.getInt("numberOfItemsSelectedFromGrid1", 0);
Log.d("blingbling2", String.valueOf(numOfImagesInsidee));
// adding downloaded images to arraylist
for(int m=0;m<numOfImagesInsidee;m++){
imageThumbnails.add(Environment.getExternalStorageDirectory()+"/"+"thumbImage" + catid + m+".png");
imageFull.add(Environment.getExternalStorageDirectory()+"/"+"fullImage" + catid + m+".png");
// imageFull.add("file://" + Environment.getExternalStorageDirectory() + "/" + "fullImage32.png");
}
viewPager = (ViewPager) findViewById(R.id.pager);
ImagePagerAdapter adapter = new ImagePagerAdapter();
viewPager.setAdapter(adapter);
// SubsamplingScaleImageView fullImage = new SubsamplingScaleImageView(ImageGallery.this);
// code to display image in a horizontal strip starts here
LinearLayout layout = (LinearLayout) findViewById(R.id.linear);
for (int i = 0; i < imageThumbnails.size(); i++) {
imageView = new SubsamplingScaleImageView(this);
imageView.setId(i);
imageView.setPadding(2, 2, 2, 2);
// Picasso.with(this).load("file://"+imageThumbnails.get(i)).into(imageView);
// imageView.setScaleType(ImageView.ScaleType.FIT_XY);
layout.addView(imageView);
ViewGroup.LayoutParams params = imageView.getLayoutParams();
params.width = 200;
params.height = 200;
imageView.setLayoutParams(params);
imageView.setZoomEnabled(false);
imageView.setDoubleTapZoomScale(0);
imageView.setImage(ImageSource.uri(imageThumbnails.get(0)));
imageView.setOnClickListener(
new View.OnClickListener() {
#Override
public void onClick(View view) {
imageView.setZoomEnabled(false);
imageViewPreview.setImage(ImageSource.uri(imageFull.get(view.getId())));
imageView.recycle();
imageViewPreview.recycle();
}
});
}
// code to display image in a horizontal strip ends here
imageViewPreview.setZoomEnabled(false);
/*imageViewPreview.setOnClickListener(
new View.OnClickListener() {
#Override
public void onClick(View view) {
imageViewPreview.setZoomEnabled(false);
stripeView.setVisibility(View.GONE);
gridFrame.setVisibility(View.GONE);
viewPager.setVisibility(View.VISIBLE);
}
});*/
imageViewPreview.setOnClickListener(new DoubleClickListener() {
#Override
public void onSingleClick(View v) {
Log.d("yo click", "single");
}
#Override
public void onDoubleClick(View v) {
Log.d("yo click", "double");
}
});
}
public abstract class DoubleClickListener implements View.OnClickListener {
private static final long DOUBLE_CLICK_TIME_DELTA = 300;//milliseconds
long lastClickTime = 0;
#Override
public void onClick(View v) {
long clickTime = System.currentTimeMillis();
if (clickTime - lastClickTime < DOUBLE_CLICK_TIME_DELTA){
onDoubleClick(v);
} else {
onSingleClick(v);
}
lastClickTime = clickTime;
}
public abstract void onSingleClick(View v);
public abstract void onDoubleClick(View v);
}
// #Override
// public void onBackPressed() {
// Intent io = new Intent(getBaseContext(), MainActivity.class);
// // clear the previous activity and start a new task
// super.onBackPressed();
// finish();
// // System.gc();
// // io.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
// startActivity(io);
// }
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_image_gallery, menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
private class ImagePagerAdapter extends PagerAdapter {
/* private int[] mImages = new int[] {
R.drawable.scroll3,
R.drawable.scroll1,
R.drawable.scroll2,
R.drawable.scroll4
};*/
/* private String[] description=new String[]
{
"One","two","three","four"
};
*/
#Override
public int getCount() {
Log.i("Image List Size", "" + imageFull.size());
return imageFull.size();
}
#Override
public boolean isViewFromObject(View view, Object object) {
return view == ((SubsamplingScaleImageView) object);
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
Context context = ImageGallery.this;
SubsamplingScaleImageView fullImage = new SubsamplingScaleImageView(ImageGallery.this);
// for placeholder
// fullImage.setImage(ImageSource.resource(R.drawable.tan2x));
if(!GridFragment2.isSelectedGrid2&&!ImageStripeFragment.isImageStripe) {
imagePosition = position;
fullImage.setImage(ImageSource.uri(imageFull.get(imagePosition)));
}
/* else if(!ImageStripeFragment.isImageStripe)
{
imagePosition = position;
fullImage.setImage(ImageSource.uri(imageFull.get(imagePosition)));
}
else if(ImageStripeFragment.isImageStripe)
{
position=imagePosition;
viewPager.setCurrentItem(imagePosition);
fullImage.setImage(ImageSource.uri(imageFull.get(position)));
}*/
else
{
position=imagePosition;
viewPager.setCurrentItem(imagePosition);
fullImage.setImage(ImageSource.uri(imageFull.get(position)));
//viewPager.removeAllViews();
}
// ImageView imageViewPager = new ImageView(context);
// ImageView imageViewPager = new ImageView(getApplicationContext());
// SubsamplingScaleImageView fullImage = new SubsamplingScaleImageView(ImageGallery.this);
GridFragment2.isSelectedGrid2=false;
ImageStripeFragment.isImageStripe=false;
// Log.i("Image Resource", "" + ImageSource.uri(imageFull.get(position)));
// imageViewPager.setImageBitmap(BitmapFactory.decodeFile(imageFull.get(position)));
// imageViewPager.setImageBitmap(myBitmap);
// fullImage.setImage(ImageSource.bitmap(bmImg));
//imageView.setImageResource(Integer.parseInt(imageFull.get(position)));
/*int padding = context.getResources().getDimensionPixelSize(
R.dimen.padding_medium);
imageView.setPadding(padding, padding, padding, padding);*/
/*imageView.setScaleType(ImageView.ScaleType.CENTER);
imageView.setImageResource(Integer.parseInt(imageFull.get(position)));
if(position==3)
{
}*/
// Log.i("Image Position",""+position);
/*text.setText(description[position]);
Log.i("Text Position",""+position);*/
/*switch(position)
{
case 0:
String pos=String.valueOf(position);
text.setText(pos);
break;
case 1:
String pos1=String.valueOf(position);
text.setText(pos1);
break;
case 2:
String pos2=String.valueOf(position);
text.setText(pos2);
break;
case 3:
String pos3=String.valueOf(position);
text.setText(pos3);
break;
}*/
fullImage.setOnClickListener(
new View.OnClickListener() {
#Override
public void onClick(View view) {
if (toggleTopBar == false) {
// thumbnailButtons.setVisibility(View.GONE);
thumbnailButtons.animate()
.translationY(-2000)
.setDuration(1000)
.start();
toggleTopBar = true;
} else if (toggleTopBar == true) {
// thumbnailButtons.setVisibility(View.VISIBLE);
thumbnailButtons.animate()
.translationY(0)
.setDuration(1000)
.start();
toggleTopBar = false;
}
}
});
((ViewPager) container).addView(fullImage, 0);
return fullImage;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
((ViewPager) container).removeView((SubsamplingScaleImageView) object);
}
/* #Override
public void destroyItem(View collection, int position, Object o) {
Log.d("DESTROY", "destroying view at position " + position);
View view = (View) o;
((ViewPager) collection).removeView(view);
view = null;
}*/
}
#Override
public void onBackPressed() {
// TODO Auto-generated method stub
super.onBackPressed();
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();
}
use this ImageLoader Class
public class ImageLoader {
MemoryCache memoryCache = new MemoryCache();
FileCache fileCache;
private Map<ImageView, String> imageViews = Collections
.synchronizedMap(new WeakHashMap<ImageView, String>());
ExecutorService executorService;
Handler handler = new Handler();// handler to display images in UI thread
int stub_id;
int widht;
public ImageLoader(Context context, int stub_idx) {
fileCache = new FileCache(context);
executorService = Executors.newFixedThreadPool(5);
stub_id = stub_idx;
}
public void DisplayImage(String url, ImageView imageView, int widht, ProgressBar bar) {
this.widht = widht;
imageViews.put(imageView, url);
Bitmap bitmap = memoryCache.get(url);
if (bitmap != null)
{
bar.setVisibility(View.GONE);
imageView.setImageBitmap(bitmap);
}
else {
queuePhoto(url, imageView, widht, bar);
imageView.setImageResource(stub_id);
}
}
private void queuePhoto(String url, ImageView imageView, int w, ProgressBar bar) {
PhotoToLoad p = new PhotoToLoad(url, imageView, bar);
executorService.submit(new PhotosLoader(p, w));
}
public Bitmap getBitmap(String url, int w) {
File f = fileCache.getFile(url);
// from SD cache
Bitmap b = decodeFile(f, w);
if (b != null)
return b;
// from web
try {
Bitmap bitmap = null;
URL imageUrl = new URL(url);
HttpURLConnection conn = (HttpURLConnection) imageUrl
.openConnection();
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(true);
InputStream is = conn.getInputStream();
OutputStream os = new FileOutputStream(f);
Utils.CopyStream(is, os);
os.close();
conn.disconnect();
bitmap = decodeFile(f, w);
return bitmap;
} catch (Throwable ex) {
ex.printStackTrace();
if (ex instanceof OutOfMemoryError)
memoryCache.clear();
return null;
}
}
// decodes image and scales it to reduce memory consumption
private Bitmap decodeFile(File f, int w) {
try {
// decode image size
BitmapFactory.Options o = new BitmapFactory.Options();
o.inJustDecodeBounds = true;
FileInputStream stream1 = new FileInputStream(f);
BitmapFactory.decodeStream(stream1, null, o);
stream1.close();
// Find the correct scale value. It should be the power of 2.
final int REQUIRED_SIZE = w;
System.out.println("screen wdth " + widht);
int width_tmp = o.outWidth, height_tmp = o.outHeight;
System.out.println("image with === " + width_tmp);
int scale = 1;
while (true) {
if (width_tmp / 2 < REQUIRED_SIZE
|| height_tmp / 2 < REQUIRED_SIZE)
break;
width_tmp /= 2;
height_tmp /= 2;
scale *= 2;
}
// decode with inSampleSize
BitmapFactory.Options o2 = new BitmapFactory.Options();
o2.inSampleSize = scale;
FileInputStream stream2 = new FileInputStream(f);
Bitmap bitmap = BitmapFactory.decodeStream(stream2, null, o2);
stream2.close();
return bitmap;
}
catch (FileNotFoundException e) {
Log.e(e.getMessage(), "");
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
// Task for the queue
private class PhotoToLoad {
public String url;
public ImageView imageView;
public ProgressBar bar;
public PhotoToLoad(String u, ImageView i, ProgressBar progressBar) {
url = u;
imageView = i;
bar = progressBar;
}
}
class PhotosLoader implements Runnable {
PhotoToLoad photoToLoad;
int w;
PhotosLoader(PhotoToLoad photoToLoad, int w) {
this.photoToLoad = photoToLoad;
this.w = w;
}
#Override
public void run() {
try {
if (imageViewReused(photoToLoad))
return;
Bitmap bmp = getBitmap(photoToLoad.url, w);
memoryCache.put(photoToLoad.url, bmp);
if (imageViewReused(photoToLoad))
return;
BitmapDisplayer bd = new BitmapDisplayer(bmp, photoToLoad);
handler.post(bd);
} catch (Throwable th) {
th.printStackTrace();
}
}
}
boolean imageViewReused(PhotoToLoad photoToLoad) {
String tag = imageViews.get(photoToLoad.imageView);
return tag == null || !tag.equals(photoToLoad.url);
}
// Used to display bitmap in the UI thread
class BitmapDisplayer implements Runnable {
Bitmap bitmap;
PhotoToLoad photoToLoad;
public BitmapDisplayer(Bitmap b, PhotoToLoad p) {
bitmap = b;
photoToLoad = p;
}
public void run() {
if (imageViewReused(photoToLoad))
return;
if (bitmap != null)
photoToLoad.imageView.setImageBitmap(bitmap);
else
photoToLoad.imageView.setImageResource(stub_id);
photoToLoad.bar.setVisibility(View.GONE);
}
}
public void clearCache() {
memoryCache.clear();
fileCache.clear();
}
Then make new class for File cache
public class FileCache {
private File cacheDir;
public FileCache(Context context) {
// Find the dir to save cached images
if (android.os.Environment.getExternalStorageState().equals(
android.os.Environment.MEDIA_MOUNTED))
cacheDir = new File(
android.os.Environment.getExternalStorageDirectory(),
"LazyList");
else
//cacheDir = url.getCacheDir();
if (!cacheDir.exists())
cacheDir.mkdirs();
}
public File getFile(String url) {
// I identify images by hashcode. Not a perfect solution, good for the
// demo.
String filename = String.valueOf(url.hashCode());
// Another possible solution (thanks to grantland)
// String filename = URLEncoder.encode(url);
File f = new File(cacheDir, filename);
return f;
}
public void clear() {
File[] files = cacheDir.listFiles();
if (files == null)
return;
for (File f : files)
f.delete();
}
}
Then last make Memory cache class , all three seperatly class
public class MemoryCache {
private static final String TAG = "MemoryCache";
private Map<String, Bitmap> cache=Collections.synchronizedMap(
new LinkedHashMap<String, Bitmap>(10,1.5f,true));//Last argument true for LRU ordering
private long size=0;//current allocated size
private long limit=1000000;//max memory in bytes
public MemoryCache(){
//use 25% of available heap size
setLimit(Runtime.getRuntime().maxMemory()/4);
}
public void setLimit(long new_limit){
limit=new_limit;
Log.i(TAG, "MemoryCache will use up to "+limit/1024./1024.+"MB");
}
public Bitmap get(String id){
try{
if(!cache.containsKey(id))
return null;
//NullPointerException sometimes happen here http://code.google.com/p/osmdroid/issues/detail?id=78
return cache.get(id);
}catch(NullPointerException ex){
ex.printStackTrace();
return null;
}
}
public void put(String id, Bitmap bitmap){
try{
if(cache.containsKey(id))
size-=getSizeInBytes(cache.get(id));
cache.put(id, bitmap);
size+=getSizeInBytes(bitmap);
checkSize();
}catch(Throwable th){
th.printStackTrace();
}
}
private void checkSize() {
Log.i(TAG, "cache size="+size+" length="+cache.size());
if(size>limit){
Iterator<Entry<String, Bitmap>> iter=cache.entrySet().iterator();//least recently accessed item will be the first one iterated
while(iter.hasNext()){
Entry<String, Bitmap> entry=iter.next();
size-=getSizeInBytes(entry.getValue());
iter.remove();
if(size<=limit)
break;
}
Log.i(TAG, "Clean cache. New size "+cache.size());
}
}
public void clear() {
try{
//NullPointerException sometimes happen here http://code.google.com/p/osmdroid/issues/detail?id=78
cache.clear();
size=0;
}catch(NullPointerException ex){
ex.printStackTrace();
}
}
long getSizeInBytes(Bitmap bitmap) {
if(bitmap==null)
return 0;
return bitmap.getRowBytes() * bitmap.getHeight();
}
Recently I tried to write an app which captures frame buffer from say USB camera (that need to be displayed as preview), and image processed output that need to be overlapped on the preview. Can anybody give me some pointers how to start? Moreover I need to draw some rectangle on the preview.
I was trying to use multiple SurfaceView to draw but not succeeded. Can anybody help me out?
What you need it the bitmap and image view reference counting. Android keeps image data in native array, which is not recycling automatically when the vm GC running. Vm part is garbage collected one way and the native part is another way and much later. You app may run out of memory pretty quickly.
Here is set of classes that can help. I think I've got them from android image tutorial and modified a bit for my own convenience.
package com.example.android.streaming.ui.cache;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.util.AttributeSet;
import android.widget.ImageView;
/**
* Sub-class of ImageView which automatically notifies the drawable when it is
* being displayed.
*/
public class RecyclingImageView extends ImageView {
public RecyclingImageView(Context context) {
super(context);
}
public RecyclingImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* #see android.widget.ImageView#onDetachedFromWindow()
*/
#Override
protected void onDetachedFromWindow() {
// This has been detached from Window, so clear the drawable
setImageDrawable(null);
super.onDetachedFromWindow();
}
/**
* #see android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)
*/
#Override
public void setImageDrawable(Drawable drawable) {
// Keep hold of previous Drawable
final Drawable previousDrawable = getDrawable();
// Call super to set new Drawable
super.setImageDrawable(drawable);
// Notify new Drawable that it is being displayed
notifyDrawable(drawable, true);
// Notify old Drawable so it is no longer being displayed
notifyDrawable(previousDrawable, false);
}
#Override
public void setImageResource(int resId) {
// Keep hold of previous Drawable
final Drawable previousDrawable = getDrawable();
super.setImageResource(resId);
// Notify new Drawable that it is being displayed
final Drawable newDrawable = getDrawable();
notifyDrawable(newDrawable, true);
// Notify old Drawable so it is no longer being displayed
notifyDrawable(previousDrawable, false);
}
/**
* Notifies the drawable that it's displayed state has changed.
*
* #param drawable
* #param isDisplayed
*/
private static void notifyDrawable(Drawable drawable, final boolean isDisplayed) {
if (drawable != null) {
if (drawable instanceof RecyclingBitmapDrawable) {
// The drawable is a CountingBitmapDrawable, so notify it
((RecyclingBitmapDrawable) drawable).setIsDisplayed(isDisplayed);
} else if (drawable instanceof LayerDrawable) {
// The drawable is a LayerDrawable, so recurse on each layer
LayerDrawable layerDrawable = (LayerDrawable) drawable;
for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) {
notifyDrawable(layerDrawable.getDrawable(i), isDisplayed);
}
}
}
}
}
And here is another one, a bitmap itself.
package com.example.android.streaming.ui.cache;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.util.Log;
import com.example.android.streaming.StreamingApp;
import com.vg.hangwith.BuildConfig;
/**
* A BitmapDrawable that keeps track of whether it is being displayed or cached.
* When the drawable is no longer being displayed or cached,
* {#link Bitmap#recycle() recycle()} will be called on this drawable's bitmap.
*/
public class RecyclingBitmapDrawable extends BitmapDrawable {
private int cacheRefCount = 0;
private int displayRefCount = 0;
private boolean hasBeenDisplayed;
public RecyclingBitmapDrawable(Resources res, Bitmap bitmap) {
super(res, bitmap);
}
/**
* Notify the drawable that the displayed state has changed. Internally a
* count is kept so that the drawable knows when it is no longer being
* displayed.
*
* #param isDisplayed
* - Whether the drawable is being displayed or not
*/
public void setIsDisplayed(boolean isDisplayed) {
synchronized (this) {
if (isDisplayed) {
displayRefCount++;
hasBeenDisplayed = true;
} else {
displayRefCount--;
}
}
// Check to see if recycle() can be called
checkState();
}
/**
* Notify the drawable that the cache state has changed. Internally a count
* is kept so that the drawable knows when it is no longer being cached.
*
* #param isCached
* - Whether the drawable is being cached or not
*/
public void setIsCached(boolean isCached) {
synchronized (this) {
if (isCached) {
cacheRefCount++;
} else {
cacheRefCount--;
}
}
// Check to see if recycle() can be called
checkState();
}
private synchronized void checkState() {
// If the drawable cache and display ref counts = 0, and this drawable
// has been displayed, then recycle
if (cacheRefCount <= 0 && displayRefCount <= 0 && hasBeenDisplayed && hasValidBitmap()) {
if (BuildConfig.DEBUG)
Log.d(StreamingApp.TAG, "No longer being used or cached so recycling. " + toString());
getBitmap().recycle();
}
}
private synchronized boolean hasValidBitmap() {
Bitmap bitmap = getBitmap();
return bitmap != null && !bitmap.isRecycled();
}
}
Now, iun your activity, whatever it does, if it needs to present recyclable image, you add this in xml res:
<com.example.android.streaming.ui.cache.RecyclingImageView
android:id="#+id/ad_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:background="#drawable/bkgd_whitegradient"
android:contentDescription="#string/dummy_desc"
android:padding="20dip"/>
This is just an example, id, background, can be whatever you need.
final RecyclingImageView adImage = (RecyclingImageView) findViewById(R.id.ad_image);
adImage.setImageDrawable(new RecyclingBitmapDrawable(getResources(), getBitmap(this)));
adImage.setVisibility(View.VISIBLE);
Note the getBitmap(), this is an example. It is you who should implement it in a way you need. It returns Bitmap instance. In your case, this Bitmap will be created out of array of bytes you've received from your camera. Let's try to do it here too.
Next, I have a class for managing avatars in my app (long list of users is a good example). It has number of useful static methods, so you may not need to create it.
package com.example.android.streaming.ui.cache;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.json.JSONObject;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.util.Log;
import android.util.LruCache;
import com.example.android.streaming.StreamingApp;
import com.example.android.streaming.datamodel.Broadcast;
import com.example.android.streaming.datamodel.Channel;
import com.facebook.model.GraphUser;
import com.parse.ParseFile;
import com.parse.ParseUser;
import com.vg.hangwith.BuildConfig;
import com.vg.hangwith.R;
public class AvatarCache {
private Map<String, LoadImageTask> tasks = new HashMap<String, AvatarCache.LoadImageTask>();
private LruCache<String, RecyclingBitmapDrawable> memoryCache;
public final static int AVATAR_BOUNDS = 100;
private String cacheDir;
private Context context;
public synchronized void addTask(String tag, LoadImageTask task) {
tasks.put(tag, task);
if (BuildConfig.DEBUG)
Log.d(StreamingApp.TAG, "Added avatar load task for tag " + tag);
}
public synchronized void removeTask(String tag) {
tasks.remove(tag);
if (BuildConfig.DEBUG)
Log.d(StreamingApp.TAG, "Removed avatar load task for tag " + tag);
}
public synchronized void cancelTasks(int keepLastItems) {
int count = 0;
Iterator<Map.Entry<String, LoadImageTask>> iter = tasks.entrySet().iterator();
while (iter.hasNext() && tasks.size() > keepLastItems) {
Map.Entry<String, LoadImageTask> entry = iter.next();
entry.getValue().cancel(true);
iter.remove();
count++;
}
if (BuildConfig.DEBUG)
Log.d(StreamingApp.TAG, "Canceled " + count + " avatar load tasks");
}
public void cancelTasks() {
cancelTasks(0);
}
public final static Bitmap downscaleAvatar(Bitmap bitmap) {
if (bitmap.getWidth() > AVATAR_BOUNDS && bitmap.getHeight() > AVATAR_BOUNDS) {
int height = (int) Math.floor(bitmap.getHeight() / ((1.0f * bitmap.getWidth()) / AVATAR_BOUNDS));
Bitmap scaled = Bitmap.createScaledBitmap(bitmap, AVATAR_BOUNDS, height, false);
bitmap.recycle();
bitmap = null;
return scaled;
} else {
return bitmap;
}
}
public final 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) {
// Calculate ratios of height and width to requested height and width
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
// Choose the smallest ratio as inSampleSize value, this will guarantee
// a final image with both dimensions larger than or equal to the
// requested height and width.
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
}
public class LoadImageTask extends AsyncTask<Void, Void, RecyclingBitmapDrawable> {
protected RecyclingImageView image;
protected String url, tag;
protected boolean avatar;
public LoadImageTask(String url, String tag, boolean avatar, RecyclingImageView image) {
super();
this.url = url;
this.tag = tag;
this.image = image;
this.avatar = avatar;
image.setTag(R.string.tag_key, tag);
addTask(tag, this);
}
#Override
protected RecyclingBitmapDrawable doInBackground(Void... dummy) {
if (isCancelled() || !isSameImage())
return null;
RecyclingBitmapDrawable drawable = getAvatarFromMemCache(tag);
if (drawable == null) {
drawable = getAvatarFromDiskCache(tag);
if (drawable == null) {
try {
if (BuildConfig.DEBUG)
Log.d(StreamingApp.TAG, "Loading avatar " + url);
/* First decode bounds to check the image size. */
BitmapFactory.Options options = new BitmapFactory.Options();
/* Calculate if the avatar should be down scaled. */
if (avatar) {
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(new URL(url).openConnection().getInputStream(), null, options);
options.inSampleSize = calculateInSampleSize(options, AVATAR_BOUNDS, AVATAR_BOUNDS);
}
options.inJustDecodeBounds = false;
/* Download down scaled avatar. */
Bitmap bitmap = BitmapFactory.decodeStream(new URL(url).openConnection().getInputStream(), null, options);
if (bitmap != null) {
drawable = new RecyclingBitmapDrawable(context.getResources(), bitmap);
if (drawable != null) {
addAvatarToDiskCache(tag, url, drawable);
addAvatarToMemoryCache(tag, drawable);
}
}
} catch (Exception e) {
Log.w(StreamingApp.TAG, "Failed to load and save avatar image. " + e.getMessage());
}
} else {
addAvatarToMemoryCache(tag, drawable);
}
}
return drawable;
}
private synchronized boolean isSameImage() {
// In case that the same image is reused for different avatar (during scroll), this
// function will return false.
Object imageTag = image.getTag(R.string.tag_key);
return imageTag != null && imageTag.equals(tag);
}
private void finishedWithResult(RecyclingBitmapDrawable result) {
if (result != null && isSameImage())
image.setImageDrawable(result);
removeTask(tag);
}
#Override
protected void onPostExecute(RecyclingBitmapDrawable result) {
finishedWithResult(result);
super.onPostExecute(result);
}
#Override
protected void onCancelled(RecyclingBitmapDrawable result) {
finishedWithResult(result);
super.onCancelled();
}
#Override
protected void onCancelled() {
finishedWithResult(null);
super.onCancelled();
}
}
public AvatarCache(Context context) {
super();
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/10th of the available memory for this memory cache. With small avatars like
// we have this is enough to keep ~100 avatars in cache.
final int cacheSize = maxMemory / 10;
if (BuildConfig.DEBUG)
Log.d(StreamingApp.TAG, "Init avatar cache, size: " + cacheSize + ", max mem size: " + maxMemory);
memoryCache = new LruCache<String, RecyclingBitmapDrawable>(cacheSize) {
#Override
protected int sizeOf(String key, RecyclingBitmapDrawable drawable) {
// The cache size will be measured in kilobytes rather than
// number of items.
Bitmap bitmap = drawable.getBitmap();
int bitmapSize = bitmap != null ? bitmap.getByteCount() / 1024 : 0;
return bitmapSize == 0 ? 1 : bitmapSize;
}
#Override
protected void entryRemoved(boolean evicted, String key, RecyclingBitmapDrawable oldValue,
RecyclingBitmapDrawable newValue) {
// The removed entry is a recycling drawable, so notify it.
// that it has been removed from the memory cache
oldValue.setIsCached(false);
}
};
this.cacheDir = context.getCacheDir().getAbsolutePath();
this.context = context;
}
public void flush() {
int oldSize = memoryCache.size();
memoryCache.evictAll();
if (BuildConfig.DEBUG)
Log.d(StreamingApp.TAG, "Flush avatar cache, flushed " + (oldSize - memoryCache.size()) + " new size "
+ memoryCache.size());
cancelTasks();
}
public void addAvatarToMemoryCache(String key, RecyclingBitmapDrawable drawable) {
if (getAvatarFromMemCache(key) == null) {
drawable.setIsCached(true);
memoryCache.put(key, drawable);
if (BuildConfig.DEBUG)
Log.d(StreamingApp.TAG, "Add to avatar cache, size: " + memoryCache.size());
}
}
public RecyclingBitmapDrawable getAvatarFromMemCache(String key) {
return memoryCache.get(key);
}
public void addAvatarToDiskCache(String name, String url, RecyclingBitmapDrawable drawable) throws IOException {
if (drawable == null)
return;
File dir = new File(cacheDir);
if (!dir.exists())
dir.mkdirs();
File file = new File(dir, name);
Bitmap bitmap = drawable.getBitmap();
if (!file.exists() && bitmap != null) {
OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
drawable.getBitmap().compress(Bitmap.CompressFormat.PNG, 85, out);
out.flush();
out.close();
}
}
/*
* Update avatar from the network if older than this.
*/
public static final int AVATAR_MAX_AGE_DAYS = 7;
public RecyclingBitmapDrawable getAvatarFromDiskCache(String name) {
File file = new File(cacheDir, name);
/* Check if cached bitmap is old. */
if ((System.currentTimeMillis() - file.lastModified()) > AVATAR_MAX_AGE_DAYS * 24 * 60 * 60 * 1000)
return null;
try {
Bitmap bitmap = BitmapFactory.decodeFile(file.getCanonicalPath());
if (bitmap != null) {
// Log.w(App.TAG, "Loaded " + (bitmap.getByteCount() / 1024.0f) + "K bitmap " + name + " w: "
// + bitmap.getWidth() + " h: " + bitmap.getHeight());
return new RecyclingBitmapDrawable(context.getResources(), bitmap);
}
} catch (Exception e) {
Log.w(StreamingApp.TAG, "Failed to decode avatar image " + name + ". " + e.getMessage());
}
return null;
}
public static boolean isValidURL(String url) {
try {
new URL(url);
return true;
} catch (Exception e) {
}
return false;
}
public void loadUrlAvatar(String url, String name, RecyclingImageView image, int placeholder, boolean checkDiskCache) {
RecyclingBitmapDrawable drawable = getAvatarFromMemCache(name);
if (drawable == null && checkDiskCache) {
drawable = getAvatarFromDiskCache(name);
if (drawable != null)
addAvatarToMemoryCache(name, drawable);
}
if (drawable == null) {
image.setImageResource(placeholder);
if (url != null && isValidURL(url))
new LoadImageTask(url, name, true, image).execute();
} else {
image.setImageDrawable(drawable);
}
}
public static String getUserAvatarURL(ParseUser user) {
if (user == null)
return null;
if (user.get("avatar") == null || user.get("avatar") == JSONObject.NULL)
return user.getString("avatar_url");
if (user.get("avatar") instanceof JSONObject)
Log.w(StreamingApp.TAG, "JSONObject found instead of ParseFile: " + ((JSONObject) user.get("avatar")).toString());
return ((ParseFile) user.get("avatar")).getUrl();
}
public static String getUserAvatarURL(GraphUser user) {
return "http://graph.facebook.com/" + user.getId() + "/picture";
}
public static String getBroadcastAvatarURL(Broadcast broadcast) {
if (broadcast.getThumbnail() == null)
return null;
return broadcast.getThumbnail().getUrl();
}
public void loadUserAvatar(ParseUser user, RecyclingImageView image, int placeholder, boolean checkDiskCache) {
if (user != null)
loadUrlAvatar(getUserAvatarURL(user), user.getUsername(), image, placeholder, checkDiskCache);
}
public void loadUserAvatar(GraphUser user, RecyclingImageView image, int placeholder, boolean checkDiskCache) {
if (user != null)
loadUrlAvatar(getUserAvatarURL(user), user.getId(), image, placeholder, checkDiskCache);
}
public void loadBroadcastAvatar(Broadcast broadcast, RecyclingImageView image, int placeholder,
boolean checkDiskCache) {
if (broadcast != null)
loadUrlAvatar(getBroadcastAvatarURL(broadcast), broadcast.getObjectId(), image, placeholder, checkDiskCache);
}
public void clearUserAvatar(ParseUser user) {
File file = new File(cacheDir, user.getUsername());
if (file.exists())
file.delete();
memoryCache.remove(user.getUsername());
if (BuildConfig.DEBUG)
Log.d(StreamingApp.TAG, "Remove avatar from cache, size: " + memoryCache.size());
}
public static String getChannelImageURL(Channel channel, boolean small, boolean ageRestricted) {
if (ageRestricted) {
if (small && channel.getSmallRestrictedState() != null)
return channel.getSmallRestrictedState().getUrl();
else if (!small && channel.getLargeRestrictedState() != null)
return channel.getLargeRestrictedState().getUrl();
} else {
if (small && channel.getSmallEmptyState() != null)
return channel.getSmallEmptyState().getUrl();
else if (!small && channel.getLargeEmptyState() != null)
return channel.getLargeEmptyState().getUrl();
}
return null;
}
public static final String channelImageCacheName(Channel channel, boolean small, boolean ageRestricted) {
return channel.getObjectId() + "-" + (ageRestricted ? "age" : "empty") + "-" + (small ? "small" : "large");
}
public boolean loadChannelImage(Channel channel, RecyclingImageView image, boolean checkDiskCache, boolean small,
boolean ageRestricted) {
boolean result = false;
if (channel == null)
return false;
String name = channelImageCacheName(channel, small, ageRestricted);
RecyclingBitmapDrawable drawable = getAvatarFromMemCache(name);
if (drawable == null && checkDiskCache) {
drawable = getAvatarFromDiskCache(name);
if (drawable != null)
addAvatarToMemoryCache(name, drawable);
}
if (drawable == null) {
String url = getChannelImageURL(channel, small, ageRestricted);
result = url != null && isValidURL(url);
if (result)
new LoadImageTask(url, name, false, image).execute();
} else {
image.setImageDrawable(drawable);
result = true;
}
return result;
}
public void loadUrlImage(String url, RecyclingImageView image, String name, boolean checkDiskCache) {
RecyclingBitmapDrawable drawable = getAvatarFromMemCache(name);
if (drawable == null && checkDiskCache) {
drawable = getAvatarFromDiskCache(name);
if (drawable != null)
addAvatarToMemoryCache(name, drawable);
}
if (drawable == null) {
if (url != null && isValidURL(url))
new LoadImageTask(url, name, false, image).execute();
} else {
image.setImageDrawable(drawable);
}
}
}
Note, it uses Parse framework at some places. Just ignore it.
In this example, AvatarCache is loading image by url in doInBackground() function. As you can see it gets an input stream of out url. You can modify it to feed it some different input stream that you use for loading your image. Then you also need to modify loadUrlImage(). In other words, just remove the url thing.
And this is how you can use it with Uri. Modify it for using input stream or array of bytes. Just use appropriate BitmapFactory.decodeSomething() method.
public Bitmap getBitmap(Uri uri) {
BitmapFactory.Options options = new BitmapFactory.Options();
AssetFileDescriptor fd = null;
Bitmap b = null;
try {
fd = getContentResolver().openAssetFileDescriptor(uri, "r");
if (fd != null) {
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, options);
options.inSampleSize = AvatarCache.calculateInSampleSize(options, AvatarCache.AVATAR_BOUNDS, AvatarCache.AVATAR_BOUNDS);
options.inJustDecodeBounds = false;
b = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, options);
try {
fd.close();
} catch (IOException e) {
}
}
} catch (Exception e) {
e.printStackTrace();
}
return b;
}
As you can see, AvatarCache is only used statically in this example. In case you need to manage a lot of images, like a photo lib preview.create AvatarCache in your app instance. Also add memory mgnt methods.
#Override
public void onCreate() {
super.onCreate();
avatarCache = new AvatarCache(this);
}
public void onTrimMemory(int level) {
if (level == TRIM_MEMORY_COMPLETE || level == TRIM_MEMORY_RUNNING_CRITICAL || level == TRIM_MEMORY_RUNNING_LOW) {
if (avatarCache != null)
avatarCache.flush();
}
super.onTrimMemory(level);
}
#Override
public void onLowMemory() {
Log.w(StreamingApp.TAG, "Low memory event received. Clear avatars cache.");
if (avatarCache != null)
avatarCache.flush();
super.onLowMemory();
}
And then you can use it a way like this:
avatarCache.loadUserAvatar(...);
It will automatically load the image and place it to the cache. When the app is short of memory, cache will be flushed.
Hope this helps. It is quite a lot of stuff here but when you go through this once, you will never have issues with images in your android app. Happy coding!
PS. Ask questions if you need. Just be specific with what you need asking and give also some context of your particular use case.
My ImageCache
public class ImageCache {
private static ImageCache instance;
private Context mContext;
private HashMap<Chat, Boolean> isContain = new HashMap<Chat, Boolean>();
private static Bitmap defaultBitmap;
private int loginId;
private WorkQueue workQueue;
private LruCache<Chat, Bitmap> cache = new LruCache<Chat, Bitmap>(
(int) getMaxSize()) {
#SuppressLint("NewApi")
protected int sizeOf(Chat key, Bitmap value) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
return value.getByteCount();
} else {
return value.getRowBytes() * value.getHeight();
}
}
protected Bitmap create(Chat ChatKey) {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.i("getView", "main thread");
return null;
}
Log.i("getView", "image cache");
// if(Thread.currentThread().getName().equals("main")){
// return null;
// }
Bitmap bitmap = null;
if (ChatKey.getMsgType() == Chat.TYPE_IMAGE) {
bitmap = getImageThumbnail(ChatKey);
synchronized (isContain) {
isContain.put(ChatKey, true);
}
return bitmap;
}
bitmap = getVideoThumbnail(ChatKey);
synchronized (isContain) {
isContain.put(ChatKey, true);
}
return bitmap;
}
#Override
protected void entryRemoved(boolean evicted, Chat key, Bitmap oldValue,
Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
synchronized (isContain) {
isContain.put(key, false);
}
oldValue.recycle();
if (evicted) {
}
}
};
private Bitmap getVideoThumbnail(Chat chat) {
if (chat.isPending()) {
return getVideoThumbnail(chat.getFilePath());
}
if (loginId == chat.getSenderID()) {
DataBaseHandler baseHandler = new DataBaseHandler(mContext);
String fIlePath = baseHandler.getSentMsgFIlePath(chat.getMsgID());
if (fIlePath != null) {
return getVideoThumbnail(fIlePath);
}
}
File file = new File(Util.videoDir, chat.getMsgID()
+ Util.getExtension(chat.getChatContent()));
if (!file.exists()) {
file = new File(Util.thumbnails, chat.getMsgID() + "");
if (!file.exists()) {
Util.downloadThumbnail(chat);
if (!file.exists()) {
return null;
}
}
return BitmapFactory.decodeFile(file.getAbsolutePath());
}
return getVideoThumbnail(file.getAbsolutePath());
}
private Bitmap getImageThumbnail(Chat chat) {
if (chat.isPending()) {
return getImageThumbnail(chat.getFilePath());
}
if (loginId == chat.getSenderID()) {
DataBaseHandler baseHandler = new DataBaseHandler(mContext);
String fIlePath = baseHandler.getSentMsgFIlePath(chat.getMsgID());
if (fIlePath != null) {
return getImageThumbnail(fIlePath);
}
}
File file = new File(Util.imageDir, chat.getMsgID() + "");
if (!file.exists()) {
file = new File(Util.thumbnails, chat.getMsgID() + "");
if (!file.exists()) {
Util.downloadThumbnail(chat);
if (!file.exists()) {
return null;
}
}
Bitmap image = BitmapFactory.decodeFile(file.getAbsolutePath());
return image;
}
return getImageThumbnail(file.getAbsolutePath());
}
private Bitmap getImageThumbnail(String imagePath) {
BitmapFactory.Options options = new Options();
options.inPreferredConfig = Config.RGB_565;
Bitmap image = null;
if (imagePath == null) {
return null;
}
Cursor cursor = mContext.getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[] { MediaStore.Video.Media._ID },
new String(MediaStore.Video.Media.DATA + "=?"),
new String[] { imagePath }, null);
if (cursor.getCount() > 0) {
cursor.moveToFirst();
image = MediaStore.Images.Thumbnails.getThumbnail(
mContext.getContentResolver(), cursor.getInt(0),
Thumbnails.MICRO_KIND, options);
cursor.close();
return image;
}
cursor.close();
return null;
}
private Bitmap getVideoThumbnail(String imagePath) {
Bitmap image = null;
BitmapFactory.Options options = new Options();
options.inPreferredConfig = Config.RGB_565;
Cursor cursor = mContext.getContentResolver().query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
new String[] { MediaStore.Video.Media._ID },
new String(MediaStore.Video.Media.DATA + "=?"),
new String[] { imagePath }, null);
if (cursor.getCount() > 0) {
cursor.moveToFirst();
image = MediaStore.Video.Thumbnails.getThumbnail(
mContext.getContentResolver(), cursor.getInt(0),
Thumbnails.MICRO_KIND, null);
cursor.close();
return image;
}
cursor.close();
return null;
}
private ImageCache(Context context) {
mContext = context.getApplicationContext();
loginId = Util.getSharedPref(mContext).getInt(C.LOGIN_USER_ID, -1);
defaultBitmap = BitmapFactory.decodeResource(mContext.getResources(),
R.drawable.image);
workQueue = WorkQueue.getInstance(context);
}
public static ImageCache getInstance(Context context) {
if (instance == null) {
instance = new ImageCache(context);
}
return instance;
}
private long getMaxSize() {
return Runtime.getRuntime().totalMemory() / 4;
}
public boolean isBitmapLoaded(Chat key) {
synchronized (isContain) {
if (isContain.get(key) == null) {
return false;
}
return isContain.get(key);
}
}
public void getImage(Chat key){
cache.get(key);
}
public Bitmap getBitmap(Chat key) {
if (isBitmapLoaded(key)) {
return cache.get(key);
}
workQueue.addRequest(key);
return defaultBitmap;
}
public void remove(Chat key) {
cache.remove(key);
isContain.put(key, false);
}
public void evictAll() {
cache.evictAll();
}
}
My workqueue
public class WorkQueue {
private final int MAX_THREAD = 1;
private WorkerThread[] threads;
private ArrayList<Chat> queue;
private static WorkQueue instance;
private ImageCache imageCache;
private ImageLoadListner imageLoadListner;
private WorkQueue(Context context) {
threads = new WorkerThread[MAX_THREAD];
for (int i = 0; i < MAX_THREAD; i++) {
threads[i] = new WorkerThread();
threads[i].start();
}
queue = new ArrayList<Chat>();
}
public void setImageCache(ImageCache imageCache){
this.imageCache=imageCache;
}
public static WorkQueue getInstance(Context context) {
if (instance == null) {
instance = new WorkQueue(context);
}
return instance;
}
public void addRequest(Chat key) {
synchronized (queue) {
if(queue.contains(key)){
return;
}
if(queue.size()==20){
queue.remove(0);
}
queue.add(key);
queue.notify();
}
}
public void setImageLoadListner(ImageLoadListner listner) {
this.imageLoadListner = listner;
}
private class WorkerThread extends Thread {
#Override
public void run() {
super.run();
setName("Background");
synchronized (queue) {
if (queue.size() == 0) {
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
while (queue.size() > 0) {
Chat chat = queue.get(0);
imageCache.getImage(chat);
imageLoadListner.onImageLoad(chat);
synchronized (queue) {
queue.remove(0);
if (queue.size() == 0) {
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
I am calling getBitmap(key) from getView() of adapter and notify the adapter when an image load by calling the onIMageLoad() of adapter from workqueqe. Calling imagecache create from backthread even then LOg in create() of imageCache is printing and listView scrolling very slow due to the bitmap creating on backThread.
Hanish U should use LinkedList WorkQueue forprivate ArrayList<Chat> queue; because in linked list addition or deletion is very fast, and save the image id in Chat object because you are doing double work for fetching the Thumbnil first get the Image id on path than get the thumb.
and for Video u can use directly without changing your code
ThumbnailUtils.public static Bitmap createVideoThumbnail (String filePath, int kind)
I am struggling to update a list view with data from a database, this works nicely by using a SimpleCursorAdapter. But the image view on the rows is not updated on the activity start, I have to scroll through the list a few times and only then the images are loaded in the image view.
This is the binder i am using for the SimpleCursorAdapter:
private class PromotionViewBinder implements SimpleCursorAdapter.ViewBinder {
private int done;
public boolean setViewValue(View view, Cursor cursor, int index) {
Log.e(""+cursor.getCount(),"");
View tmpview = view;
if (index == cursor.getColumnIndex(PromotionsTable.SEEN_COL)) {
boolean read = cursor.getInt(index) > 0 ? true : false;
TextView title = (TextView) tmpview;
if (!read) {
title.setTypeface(Typeface.DEFAULT_BOLD, 0);
} else {
title.setTypeface(Typeface.DEFAULT);
}
return true;
} else if (tmpview.getId() == R.id.promotions_list_row_image){
String imageURL = cursor.getString(index);
Log.e("",imageURL);
imageRetriever.displayImage(imageURL, (ImageView)tmpview);
return true;
} else {
return false;
}
}
}
The image retriever class is the LazyList example from here. As you will see this is using a runnable to retrieve the images and once the task is done is automatically updating the given imageView...Do you think that the reference to the imageView is lost somewhere on the way?
Thanx in advance,
Nick
package com.tipgain.promotions;
The image retriever class:
/**
* This class is used for retrieving images from a given web link. it uses local
* storage and memory to store the images. Once a image is downloaded
* successfully the UI gets updated automatically.
*
*
*/
public class ImageRetriever {
private final String TAG = ImageRetriever.class.getName();
private MemoryImageCache memoryImgCache = new MemoryImageCache();
private LocalStorageImageCache localFileCache;
private Map<ImageView, String> imageViewHolders = Collections
.synchronizedMap(new WeakHashMap<ImageView, String>());
private ExecutorService execService;
final int defaultImageID = R.drawable.photo_not_available;
public ImageRetriever(Context context) {
localFileCache = new LocalStorageImageCache(context);
execService = Executors.newFixedThreadPool(5);
}
public void displayImage(String url, ImageView imageView) {
imageViewHolders.put(imageView, url);
Bitmap bmp = memoryImgCache.retrieve(url);
if (bmp != null) {
Log.e("case 1", " " + (bmp != null));
imageView.setImageBitmap(bmp);
} else {
Log.e("case 2", " " + (bmp == null));
addImageToQueue(url, imageView);
imageView.setImageResource(defaultImageID);
}
}
private void addImageToQueue(String url, ImageView imageView) {
NextImageToLoad img = new NextImageToLoad(url, imageView);
execService.submit(new ImagesRetriever(img));
}
/**
* This method is used for retrieving the Bitmap Image.
*
* #param url
* String representing the url pointing to the image.
* #return Bitmap representing the image
*/
private Bitmap getBitmap(String url) {
File imageFile = localFileCache.getFile(url);
// trying to get the bitmap from the local storage first
Bitmap bmp = decodeImageFile(imageFile);
if (bmp != null)
return bmp;
// if the file was not found locally we retrieve it from the web
try {
URL imageUrl = new URL(url);
HttpURLConnection conn = (HttpURLConnection) imageUrl
.openConnection();
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(true);
InputStream is = conn.getInputStream();
OutputStream os = new FileOutputStream(imageFile);
Utils.CopyStream(is, os);
os.close();
bmp = decodeImageFile(imageFile);
return bmp;
} catch (MalformedURLException e) {
Log.e(TAG, e.getMessage());
} catch (FileNotFoundException e) {
Log.e(TAG, e.getMessage());
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
return null;
}
/**
* This method is used for decoding a given image file. Also, to reduce
* memory, the image is also scaled.
*
* #param imageFile
* #return
*/
private Bitmap decodeImageFile(File imageFile) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(new FileInputStream(imageFile), null,
options);
// Find the correct scale value. It should be the power of 2.
// Deciding the perfect scaling value. (^2).
final int REQUIRED_SIZE = 100;
int tmpWidth = options.outWidth, tmpHeight = options.outHeight;
int scale = 1;
while (true) {
if (tmpWidth / 2 < REQUIRED_SIZE
|| tmpHeight / 2 < REQUIRED_SIZE)
break;
tmpWidth /= 2;
tmpHeight /= 2;
scale *= 2;
}
// decoding using inSampleSize
BitmapFactory.Options option2 = new BitmapFactory.Options();
option2.inSampleSize = scale;
return BitmapFactory.decodeStream(new FileInputStream(imageFile),
null, option2);
} catch (FileNotFoundException e) {
Log.e(TAG, e.getLocalizedMessage());
}
return null;
}
private boolean reusedImage(NextImageToLoad image) {
Context c = image.imageView.getContext();
c.getContentResolver().notifyChange(PromotionsProvider.CONTENT_URI, null);
String tag = imageViewHolders.get(image.imageView);
if ((tag == null) || (!tag.equals(image.url)))
return true;
return false;
}
/**
* Clears the Memory and Local cache
*/
public void clearCache() {
memoryImgCache.clear();
localFileCache.clear();
}
/**
* This class implements a runnable that is used for updating the promotions
* images on the UI
*
*
*/
class UIupdater implements Runnable {
Bitmap bmp;
NextImageToLoad image;
public UIupdater(Bitmap bmp, NextImageToLoad image) {
this.bmp = bmp;
this.image = image;
Log.e("", "ui updater");
}
public void run() {
Log.e("ui updater", "ui updater");
if (reusedImage(image))
return;
Log.e("nick", "" + (bmp == null) + " chberugv");
if (bmp != null){
image.imageView.setImageBitmap(bmp);
Context c = image.imageView.getContext();
c.getContentResolver().notifyChange(PromotionsProvider.CONTENT_URI, null);
}else
image.imageView.setImageResource(defaultImageID);
}
}
private class ImagesRetriever implements Runnable {
NextImageToLoad image;
ImagesRetriever(NextImageToLoad image) {
this.image = image;
}
public void run() {
Log.e("images retirever", " images retriever");
if (reusedImage(image))
return;
Bitmap bmp = getBitmap(image.url);
memoryImgCache.insert(image.url, bmp);
if (reusedImage(image))
return;
UIupdater uiUpdater = new UIupdater(bmp, image);
Activity activity = (Activity) image.imageView.getContext();
activity.runOnUiThread(uiUpdater);
//Context c = image.imageView.getContext();
//c.getContentResolver().notifyChange(PromotionsProvider.CONTENT_URI, null);
}
}
/**
* This class encapsulates the image being downloaded.
*
* #author Nicolae Anca
*
*/
private class NextImageToLoad {
public String url;
public ImageView imageView;
public NextImageToLoad(String u, ImageView i) {
url = u;
imageView = i;
}
}
}
Modified Runnable:
class UIupdater implements Runnable {
Bitmap bmp;
NextImageToLoad image;
public UIupdater(Bitmap bmp, NextImageToLoad image) {
this.bmp = bmp;
this.image = image;
}
public void run() {
if (reusedImage(image))
return;
if (bmp != null){
image.imageView.setImageBitmap(bmp);
Context c = image.imageView.getContext();
c.getContentResolver().notifyChange(PromotionsProvider.CONTENT_URI, null);
}else
image.imageView.setImageResource(defaultImageID);
}
}
Thats an interesting way to do what you are doing. Have you tried extending the Simple Cursor Adapter?
What you do is implement a ViewHolder and put your imageview in it.
Then in your ImageRetriever, write a Listener which will be called once the image is ready and retrieved.
Implement this listener in the Viewholder.
You create the view in getView() and request for the image in BindView().
Once the image gets loaded, the list will be refreshed automatically.
one way to do it is by calling notifyDataSetChenged on listview, and another was is to have adapter as member variable and when something changes on listview you call a function that assigns new listadapter to member adapter. That way your list will be redraw on change.
I guess, you have to use some handler, calling after image load, which will call notifyDataSetChanged for list adapter