开发者

Android: Bitmap not shown in ImageView

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)
     开发者_Go百科       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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜