/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.android.imagepixelization; import android.animation.ObjectAnimator; import android.app.Activity; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.animation.LinearInterpolator; import android.widget.ImageView; import android.widget.SeekBar; import java.util.Arrays; /** * This application shows three different graphics/animation concepts. * * A pixelization effect is applied to an image with varying pixelization * factors to achieve an image that is pixelized to varying degrees. In * order to optimize the amount of image processing performed on the image * being pixelized, the pixelization effect only takes place if a predefined * amount of time has elapsed since the main image was last pixelized. The * effect is also applied when the user stops moving the seekbar. * * This application also shows how to use a ValueAnimator to achieve a * smooth self-animating seekbar. * * Lastly, this application shows a use case of AsyncTask where some * computation heavy processing can be moved onto a background thread, * so as to keep the UI completely responsive to user input. */ public class ImagePixelization extends Activity { final private static int SEEKBAR_ANIMATION_DURATION = 10000; final private static int TIME_BETWEEN_TASKS = 400; final private static int SEEKBAR_STOP_CHANGE_DELTA = 5; final private static float PROGRESS_TO_PIXELIZATION_FACTOR = 4000.0f; Bitmap mImageBitmap; ImageView mImageView; SeekBar mSeekBar; boolean mIsChecked = false; boolean mIsBuiltinPixelizationChecked = false; int mLastProgress = 0; long mLastTime = 0; Bitmap mPixelatedBitmap; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_image_pixelization); mImageView = (ImageView) findViewById(R.id.pixelView); mSeekBar = (SeekBar)findViewById(R.id.seekbar); mImageBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image); mImageView.setImageBitmap(mImageBitmap); mSeekBar.setOnSeekBarChangeListener(mOnSeekBarChangeListener); } private SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() { @Override public void onStopTrackingTouch(SeekBar seekBar) { if (Math.abs(mSeekBar.getProgress() - mLastProgress) > SEEKBAR_STOP_CHANGE_DELTA) { invokePixelization(); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { checkIfShouldPixelize(); } }; /** * Checks if enough time has elapsed since the last pixelization call was invoked. * This prevents too many pixelization processes from being invoked at the same time * while previous ones have not yet completed. */ public void checkIfShouldPixelize() { if ((System.currentTimeMillis() - mLastTime) > TIME_BETWEEN_TASKS) { invokePixelization(); } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.image_pixelization, menu); return true; } @Override public boolean onOptionsItemSelected (MenuItem item) { switch (item.getItemId()){ case R.id.animate: ObjectAnimator animator = ObjectAnimator.ofInt(mSeekBar, "progress", 0, mSeekBar.getMax()); animator.setInterpolator(new LinearInterpolator()); animator.setDuration(SEEKBAR_ANIMATION_DURATION); animator.start(); break; case R.id.checkbox: if (mIsChecked) { item.setChecked(false); mIsChecked = false; } else { item.setChecked(true); mIsChecked = true; } break; case R.id.builtin_pixelation_checkbox: mIsBuiltinPixelizationChecked = !mIsBuiltinPixelizationChecked; item.setChecked(mIsBuiltinPixelizationChecked); break; default: break; } return true; } /** * A simple pixelization algorithm. This uses a box blur algorithm where all the * pixels within some region are averaged, and that average pixel value is then * applied to all the pixels within that region. A higher pixelization factor * imposes a smaller number of regions of greater size. Similarly, a smaller * pixelization factor imposes a larger number of regions of smaller size. */ public BitmapDrawable customImagePixelization(float pixelizationFactor, Bitmap bitmap) { int width = bitmap.getWidth(); int height = bitmap.getHeight(); if (mPixelatedBitmap == null || !(width == mPixelatedBitmap.getWidth() && height == mPixelatedBitmap.getHeight())) { mPixelatedBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); } int xPixels = (int) (pixelizationFactor * ((float)width)); xPixels = xPixels > 0 ? xPixels : 1; int yPixels = (int) (pixelizationFactor * ((float)height)); yPixels = yPixels > 0 ? yPixels : 1; int pixel = 0, red = 0, green = 0, blue = 0, numPixels = 0; int[] bitmapPixels = new int[width * height]; bitmap.getPixels(bitmapPixels, 0, width, 0, 0, width, height); int[] pixels = new int[yPixels * xPixels]; int maxX, maxY; for (int y = 0; y < height; y+=yPixels) { for (int x = 0; x < width; x+=xPixels) { numPixels = red = green = blue = 0; maxX = Math.min(x + xPixels, width); maxY = Math.min(y + yPixels, height); for (int i = x; i < maxX; i++) { for (int j = y; j < maxY; j++) { pixel = bitmapPixels[j * width + i]; red += Color.red(pixel); green += Color.green(pixel); blue += Color.blue(pixel); numPixels ++; } } pixel = Color.rgb(red / numPixels, green / numPixels, blue / numPixels); Arrays.fill(pixels, pixel); int w = Math.min(xPixels, width - x); int h = Math.min(yPixels, height - y); mPixelatedBitmap.setPixels(pixels, 0 , w, x , y, w, h); } } return new BitmapDrawable(getResources(), mPixelatedBitmap); } /** * This method of image pixelization utilizes the bitmap scaling operations built * into the framework. By downscaling the bitmap and upscaling it back to its * original size (while setting the filter flag to false), the same effect can be * achieved with much better performance. */ public BitmapDrawable builtInPixelization(float pixelizationFactor, Bitmap bitmap) { int width = bitmap.getWidth(); int height = bitmap.getHeight(); int downScaleFactorWidth = (int)(pixelizationFactor * width); downScaleFactorWidth = downScaleFactorWidth > 0 ? downScaleFactorWidth : 1; int downScaleFactorHeight = (int)(pixelizationFactor * height); downScaleFactorHeight = downScaleFactorHeight > 0 ? downScaleFactorHeight : 1; int downScaledWidth = width / downScaleFactorWidth; int downScaledHeight = height / downScaleFactorHeight; Bitmap pixelatedBitmap = Bitmap.createScaledBitmap(bitmap, downScaledWidth, downScaledHeight, false); /* Bitmap's createScaledBitmap method has a filter parameter that can be set to either * true or false in order to specify either bilinear filtering or point sampling * respectively when the bitmap is scaled up or now. * * Similarly, a BitmapDrawable also has a flag to specify the same thing. When the * BitmapDrawable is applied to an ImageView that has some scaleType, the filtering * flag is taken into consideration. However, for optimization purposes, this flag was * ignored in BitmapDrawables before Jelly Bean MR1. * * Here, it is important to note that prior to JBMR1, two bitmap scaling operations * are required to achieve the pixelization effect. Otherwise, a BitmapDrawable * can be created corresponding to the downscaled bitmap such that when it is * upscaled to fit the ImageView, the upscaling operation is a lot faster since * it uses internal optimizations to fit the ImageView. * */ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { BitmapDrawable bitmapDrawable = new BitmapDrawable(getResources(), pixelatedBitmap); bitmapDrawable.setFilterBitmap(false); return bitmapDrawable; } else { Bitmap upscaled = Bitmap.createScaledBitmap(pixelatedBitmap, width, height, false); return new BitmapDrawable(getResources(), upscaled); } } /** * Invokes pixelization either on the main thread or on a background thread * depending on whether or not the checkbox was checked. */ public void invokePixelization () { mLastTime = System.currentTimeMillis(); mLastProgress = mSeekBar.getProgress(); if (mIsChecked) { PixelizeImageAsyncTask asyncPixelateTask = new PixelizeImageAsyncTask(); asyncPixelateTask.execute(mSeekBar.getProgress() / PROGRESS_TO_PIXELIZATION_FACTOR, mImageBitmap); } else { mImageView.setImageDrawable(pixelizeImage(mSeekBar.getProgress() / PROGRESS_TO_PIXELIZATION_FACTOR, mImageBitmap)); } } /** * Selects either the custom pixelization algorithm that sets and gets bitmap * pixels manually or the one that uses built-in bitmap operations. */ public BitmapDrawable pixelizeImage(float pixelizationFactor, Bitmap bitmap) { if (mIsBuiltinPixelizationChecked) { return builtInPixelization(pixelizationFactor, bitmap); } else { return customImagePixelization(pixelizationFactor, bitmap); } } /** * Implementation of the AsyncTask class showing how to run the * pixelization algorithm in the background, and retrieving the * pixelated image from the resulting operation. */ private class PixelizeImageAsyncTask extends AsyncTask { @Override protected BitmapDrawable doInBackground(Object... params) { float pixelizationFactor = (Float)params[0]; Bitmap originalBitmap = (Bitmap)params[1]; return pixelizeImage(pixelizationFactor, originalBitmap); } @Override protected void onPostExecute(BitmapDrawable result) { mImageView.setImageDrawable(result); } @Override protected void onPreExecute() { } @Override protected void onProgressUpdate(Void... values) { } } }