AsyncTask / Bitmap / ImageView / Palette

Palette – Part 1

One of the many additions being introduced in Android-L is a new library for extracting key colours from bitmaps. In this short series we’ll take a look at the new Palette library.

palettePalette is incredibly easy to use (which is understandable considering it was written by Chris Banes who is superb at API design) and we’ll write a simple app which will extract key colours from images and display them over the selected image.

Before we begin, one word of warning: Palette is packaged within a support library which will provide backwards compatibility to API 7. However, at the time of working, out is still a Release Candidate build within the Android-L developer preview, and requires minSdkVersion 'L' in order to run. Once it is fully released I expect this restriction to be removed.

Warning out of the way, let’s move on to the code. First the layout we’ll use which consists of an ImageView and then a table of View objects which will display the various colours:

Within the Java code we’ll use a typical onCreate() method which inflates the layout and obtains references to the Views, and we also create and handle an ActionBar menu containing a single action to open an image.

public class PaletteActivity extends Activity {
    private static final int SELECT_IMAGE = 1;
    public static final String IMAGE_MIME_TYPE = "image/*";

    private ImageView imageView;
    private View paletteOverlay;
    private View normalVibrant;
    private View lightVibrant;
    private View darkVibrant;
    private View normalMuted;
    private View lightMuted;
    private View darkMuted;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_palette);
        imageView = (ImageView) findViewById(R.id.image);
        paletteOverlay = findViewById(R.id.palette_overlay);
        normalVibrant = findViewById(R.id.normal_vibrant);
        lightVibrant = findViewById(R.id.light_vibrant);
        darkVibrant = findViewById(R.id.dark_vibrant);
        normalMuted = findViewById(R.id.normal_muted);
        lightMuted = findViewById(R.id.light_muted);
        darkMuted = findViewById(R.id.dark_muted);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.palette, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == R.id.action_open) {
            Intent intent = new Intent();
            intent.setType(IMAGE_MIME_TYPE);
            intent.setAction(Intent.ACTION_GET_CONTENT);
            startActivityForResult(Intent.createChooser(intent,
                    getString(R.string.select_image)), SELECT_IMAGE);
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == RESULT_OK && requestCode == SELECT_IMAGE) {
            Uri selectedImageUri = data.getData();
            loadBitmap(selectedImageUri);
        }
    }
    .
    .
    .
}

The first point worthy of explanation is the image selection which is initiated in response to the use clicking the Open button in the ActionBar. The Android framework contains a file selection interface and we’ll use this. The reasoning for this is not developer laziness (although there’s no point in reinventing the wheel particularly when the Android file selector is pretty feature rich!), but to provide consistency to the user. If all apps use a standard mechanism for selecting files then it makes things much easier for users.

To do this we use startActivityForResult to kick of the file selection and we get a callback once the file selection is complete:

public class PaletteActivity extends Activity {
    private static final int SELECT_IMAGE = 1;
    public static final String IMAGE_MIME_TYPE = "image/*";
    .
    .
    .
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == R.id.action_open) {
            Intent intent = new Intent();
            intent.setType(IMAGE_MIME_TYPE);
            intent.setAction(Intent.ACTION_GET_CONTENT);
            startActivityForResult(Intent.createChooser(intent,
                    getString(R.string.select_image)), SELECT_IMAGE);
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == RESULT_OK && requestCode == SELECT_IMAGE) {
            Uri selectedImageUri = data.getData();
            loadBitmap(selectedImageUri);
        }
    }

    private void loadBitmap(Uri selectedImageUri) {
        ImageLoadTask imageLoadTask = ImageLoadTask.newInstance(this);
        imageLoadTask.execute(selectedImageUri);
    }

    public void setBitmap(Bitmap bitmap) {
        imageView.setImageBitmap(bitmap);
    }

    public void showError(int errorId) {
        String error = getString(errorId);
        Toast.makeText(this, error, Toast.LENGTH_LONG).show();
    }
}

Next we need to load the BitmapBitmap in a simple AsyncTask:

class ImageLoadTask extends AsyncTask {
    private final WeakReference activityWeakReference;

    public static ImageLoadTask newInstance(PaletteActivity activity) {
        WeakReference activityWeakReference = new WeakReference(activity);
        return new ImageLoadTask(activityWeakReference);
    }

    ImageLoadTask(WeakReference activityWeakReference) {
        this.activityWeakReference = activityWeakReference;
    }

    @Override
    protected Bitmap doInBackground(Uri... uris) {
        PaletteActivity activity = activityWeakReference.get();
        Bitmap bitmap = null;
        if (activity != null && uris.length > 0) {
            try {
                bitmap = MediaStore.Images.Media.getBitmap(activity.getContentResolver(), uris[0]);
            } catch (Exception e) {
                return null;
            }
        }
        return bitmap;
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        super.onPostExecute(bitmap);
        PaletteActivity activity = activityWeakReference.get();
        if (activity != null) {
            if (bitmap != null) {
                activity.setBitmap(bitmap);
            } else {
                activity.showError(R.string.load_error);
            }
        }
    }
}

We hold a WeakReference to PaletteActivity to ensure that we cannot leak a Context by holding a reference to it beyond its natural life-cycle (i.e. if the user exits the Activity). The Palette loading will be fairly short lived so really this shouldn’t be a problem, but it’s always worth protecting against Context-leaks, nonetheless.

Once the Bitmap load is complete, we can extract the Palette information which we’ll look at in the next article in this series.

The source code for this article is available here.

© 2014, Mark Allison. All rights reserved.

Copyright © 2014 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

5 Comments

  1. Thanks for an article! I have a question. What if a user will change device orientation while ImageLoadTask is executing? PaletteActivity will be recreated but a new activity instance will not be updated with a loaded bitmap. I would suggest to use an event bus to notify an activity when AsyncTask is completed.

Leave a Reply to Alex Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.