Project: android-cropimage
/*
 * Copyright (C) 2007 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.android.camera; 
 
import java.io.File; 
import java.io.IOException; 
import java.io.OutputStream; 
import java.util.ArrayList; 
import java.util.concurrent.CountDownLatch; 
 
import android.app.WallpaperManager; 
import android.content.ContentResolver; 
import android.content.Context; 
import android.content.Intent; 
import android.graphics.Bitmap; 
import android.graphics.Canvas; 
import android.graphics.Matrix; 
import android.graphics.Path; 
import android.graphics.PointF; 
import android.graphics.PorterDuff; 
import android.graphics.Rect; 
import android.graphics.RectF; 
import android.graphics.Region; 
import android.media.FaceDetector; 
import android.net.Uri; 
import android.os.Bundle; 
import android.os.Handler; 
import android.provider.MediaStore; 
import android.util.AttributeSet; 
import android.util.Log; 
import android.view.MotionEvent; 
import android.view.View; 
import android.view.Window; 
import android.view.WindowManager; 
import android.widget.Toast; 
 
import com.android.camera.gallery.IImage; 
import com.android.camera.gallery.IImageList; 
import com.android.gallery.R; 
 
/**
 * The activity can crop specific region of interest from an image. 
 */
 
public class CropImage extends MonitoredActivity { 
    private static final String TAG = "CropImage"
 
    // These are various options can be specified in the intent. 
    private Bitmap.CompressFormat mOutputFormat = 
            Bitmap.CompressFormat.JPEG; // only used with mSaveUri 
    private Uri mSaveUri = null
    private boolean mSetWallpaper = false
    private int mAspectX, mAspectY; 
    private boolean mDoFaceDetection = true
    private boolean mCircleCrop = false
    private final Handler mHandler = new Handler(); 
 
    // These options specifiy the output image size and whether we should 
    // scale the output to fit it (or just crop it). 
    private int mOutputX, mOutputY; 
    private boolean mScale; 
    private boolean mScaleUp = true
 
    boolean mWaitingToPick; // Whether we are wait the user to pick a face. 
    boolean mSaving;  // Whether the "save" button is already clicked. 
 
    private CropImageView mImageView; 
    private ContentResolver mContentResolver; 
 
    private Bitmap mBitmap; 
    HighlightView mCrop; 
 
    private IImageList mAllImages; 
    private IImage mImage; 
 
    @Override 
    public void onCreate(Bundle icicle) { 
        super.onCreate(icicle); 
        mContentResolver = getContentResolver(); 
 
        requestWindowFeature(Window.FEATURE_NO_TITLE); 
        setContentView(R.layout.cropimage); 
 
        mImageView = (CropImageView) findViewById(R.id.image); 
 
        Intent intent = getIntent(); 
        Bundle extras = intent.getExtras(); 
 
        if (extras != null) { 
            if (extras.getString("circleCrop") != null) { 
                mCircleCrop = true
                mAspectX = 1
                mAspectY = 1
            } 
            mSaveUri = (Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT); 
            if (mSaveUri != null) { 
                String outputFormatString = extras.getString("outputFormat"); 
                if (outputFormatString != null) { 
                    mOutputFormat = Bitmap.CompressFormat.valueOf( 
                            outputFormatString); 
                } 
            } else { 
                mSetWallpaper = extras.getBoolean("setWallpaper"); 
            } 
            mBitmap = (Bitmap) extras.getParcelable("data"); 
            mAspectX = extras.getInt("aspectX"); 
            mAspectY = extras.getInt("aspectY"); 
            mOutputX = extras.getInt("outputX"); 
            mOutputY = extras.getInt("outputY"); 
            mScale = extras.getBoolean("scale"true); 
            mScaleUp = extras.getBoolean("scaleUpIfNeeded"true); 
            mDoFaceDetection = extras.containsKey("noFaceDetection"
                    ? !extras.getBoolean("noFaceDetection"
                    : true
        } 
 
        if (mBitmap == null) { 
            Uri target = intent.getData(); 
            mAllImages = ImageManager.makeImageList(mContentResolver, target, 
                    ImageManager.SORT_ASCENDING); 
            mImage = mAllImages.getImageForUri(target); 
            if (mImage != null) { 
                // Don't read in really large bitmaps. Use the (big) thumbnail 
                // instead. 
                // TODO when saving the resulting bitmap use the 
                // decode/crop/encode api so we don't lose any resolution. 
                mBitmap = mImage.thumbBitmap(IImage.ROTATE_AS_NEEDED); 
            } 
        } 
 
        if (mBitmap == null) { 
            finish(); 
            return
        } 
 
        // Make UI fullscreen. 
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); 
 
        findViewById(R.id.discard).setOnClickListener( 
                new View.OnClickListener() { 
                    public void onClick(View v) { 
                        setResult(RESULT_CANCELED); 
                        finish(); 
                    } 
                }); 
 
        findViewById(R.id.save).setOnClickListener( 
                new View.OnClickListener() { 
                    public void onClick(View v) { 
                        onSaveClicked(); 
                    } 
                }); 
 
        startFaceDetection(); 
    } 
 
    private void startFaceDetection() { 
        if (isFinishing()) { 
            return
        } 
 
        mImageView.setImageBitmapResetBase(mBitmap, true); 
 
        Util.startBackgroundJob(thisnull
                getResources().getString(R.string.runningFaceDetection), 
                new Runnable() { 
            public void run() { 
                final CountDownLatch latch = new CountDownLatch(1); 
                final Bitmap b = (mImage != null
                        ? mImage.fullSizeBitmap(IImage.UNCONSTRAINED, 
                        1024 * 1024
                        : mBitmap; 
                mHandler.post(new Runnable() { 
                    public void run() { 
                        if (b != mBitmap && b != null) { 
                            mImageView.setImageBitmapResetBase(b, true); 
                            mBitmap.recycle(); 
                            mBitmap = b; 
                        } 
                        if (mImageView.getScale() == 1F) { 
                            mImageView.center(truetrue); 
                        } 
                        latch.countDown(); 
                    } 
                }); 
                try { 
                    latch.await(); 
                } catch (InterruptedException e) { 
                    throw new RuntimeException(e); 
                } 
                mRunFaceDetection.run(); 
            } 
        }, mHandler); 
    } 
 
    private void onSaveClicked() { 
        // TODO this code needs to change to use the decode/crop/encode single 
        // step api so that we don't require that the whole (possibly large) 
        // bitmap doesn't have to be read into memory 
        if (mCrop == null) { 
            return
        } 
 
        if (mSaving) return
        mSaving = true
 
        Bitmap croppedImage; 
 
        // If the output is required to a specific size, create an new image 
        // with the cropped image in the center and the extra space filled. 
        if (mOutputX != 0 && mOutputY != 0 && !mScale) { 
            // Don't scale the image but instead fill it so it's the 
            // required dimension 
            croppedImage = Bitmap.createBitmap(mOutputX, mOutputY, 
                    Bitmap.Config.RGB_565); 
            Canvas canvas = new Canvas(croppedImage); 
 
            Rect srcRect = mCrop.getCropRect(); 
            Rect dstRect = new Rect(00, mOutputX, mOutputY); 
 
            int dx = (srcRect.width() - dstRect.width()) / 2
            int dy = (srcRect.height() - dstRect.height()) / 2
 
            // If the srcRect is too big, use the center part of it. 
            srcRect.inset(Math.max(0, dx), Math.max(0, dy)); 
 
            // If the dstRect is too big, use the center part of it. 
            dstRect.inset(Math.max(0, -dx), Math.max(0, -dy)); 
 
            // Draw the cropped bitmap in the center 
            canvas.drawBitmap(mBitmap, srcRect, dstRect, null); 
 
            // Release bitmap memory as soon as possible 
            mImageView.clear(); 
            mBitmap.recycle(); 
        } else { 
            Rect r = mCrop.getCropRect(); 
 
            int width = r.width(); 
            int height = r.height(); 
 
            // If we are circle cropping, we want alpha channel, which is the 
            // third param here. 
            croppedImage = Bitmap.createBitmap(width, height, 
                    mCircleCrop 
                    ? Bitmap.Config.ARGB_8888 
                    : Bitmap.Config.RGB_565); 
 
            Canvas canvas = new Canvas(croppedImage); 
            Rect dstRect = new Rect(00, width, height); 
            canvas.drawBitmap(mBitmap, r, dstRect, null); 
 
            // Release bitmap memory as soon as possible 
            mImageView.clear(); 
            mBitmap.recycle(); 
 
            if (mCircleCrop) { 
                // OK, so what's all this about? 
                // Bitmaps are inherently rectangular but we want to return 
                // something that's basically a circle.  So we fill in the 
                // area around the circle with alpha.  Note the all important 
                // PortDuff.Mode.CLEAR. 
                Canvas c = new Canvas(croppedImage); 
                Path p = new Path(); 
                p.addCircle(width / 2F, height / 2F, width / 2F
                        Path.Direction.CW); 
                c.clipPath(p, Region.Op.DIFFERENCE); 
                c.drawColor(0x00000000, PorterDuff.Mode.CLEAR); 
            } 
 
            // If the required dimension is specified, scale the image. 
            if (mOutputX != 0 && mOutputY != 0 && mScale) { 
                croppedImage = Util.transform(new Matrix(), croppedImage, 
                        mOutputX, mOutputY, mScaleUp, Util.RECYCLE_INPUT); 
            } 
        } 
 
        mImageView.setImageBitmapResetBase(croppedImage, true); 
        mImageView.center(truetrue); 
        mImageView.mHighlightViews.clear(); 
 
        // Return the cropped image directly or save it to the specified URI. 
        Bundle myExtras = getIntent().getExtras(); 
        if (myExtras != null && (myExtras.getParcelable("data") != null 
                || myExtras.getBoolean("return-data"))) { 
            Bundle extras = new Bundle(); 
            extras.putParcelable("data", croppedImage); 
            setResult(RESULT_OK, 
                    (new Intent()).setAction("inline-data").putExtras(extras)); 
            finish(); 
        } else { 
            final Bitmap b = croppedImage; 
            final int msdId = mSetWallpaper 
                    ? R.string.wallpaper 
                    : R.string.savingImage; 
            Util.startBackgroundJob(thisnull
                    getResources().getString(msdId), 
                    new Runnable() { 
                public void run() { 
                    saveOutput(b); 
                } 
            }, mHandler); 
        } 
    } 
 
    private void saveOutput(Bitmap croppedImage) { 
        if (mSaveUri != null) { 
            OutputStream outputStream = null
            try { 
                outputStream = mContentResolver.openOutputStream(mSaveUri); 
                if (outputStream != null) { 
                    croppedImage.compress(mOutputFormat, 75, outputStream); 
                } 
            } catch (IOException ex) { 
                // TODO: report error to caller 
                Log.e(TAG, "Cannot open file: " + mSaveUri, ex); 
            } finally { 
                Util.closeSilently(outputStream); 
            } 
            Bundle extras = new Bundle(); 
            setResult(RESULT_OK, new Intent(mSaveUri.toString()) 
                    .putExtras(extras)); 
        } else if (mSetWallpaper) { 
            try { 
                WallpaperManager.getInstance(this).setBitmap(croppedImage); 
                setResult(RESULT_OK); 
            } catch (IOException e) { 
                Log.e(TAG, "Failed to set wallpaper.", e); 
                setResult(RESULT_CANCELED); 
            } 
        } else { 
            Bundle extras = new Bundle(); 
            extras.putString("rect", mCrop.getCropRect().toString()); 
 
            File oldPath = new File(mImage.getDataPath()); 
            File directory = new File(oldPath.getParent()); 
 
            int x = 0
            String fileName = oldPath.getName(); 
            fileName = fileName.substring(0, fileName.lastIndexOf(".")); 
 
            // Try file-1.jpg, file-2.jpg, ... until we find a filename which 
            // does not exist yet. 
            while (true) { 
                x += 1
                String candidate = directory.toString() 
                        + "/" + fileName + "-" + x + ".jpg"
                boolean exists = (new File(candidate)).exists(); 
                if (!exists) { 
                    break
                } 
            } 
 
            try { 
                int[] degree = new int[1]; 
                Uri newUri = ImageManager.addImage( 
                        mContentResolver, 
                        mImage.getTitle(), 
                        mImage.getDateTaken(), 
                        null,    // TODO this null is going to cause us to lose 
                                 // the location (gps). 
                        directory.toString(), fileName + "-" + x + ".jpg"
                        croppedImage, null
                        degree); 
 
                setResult(RESULT_OK, new Intent() 
                        .setAction(newUri.toString()) 
                        .putExtras(extras)); 
            } catch (Exception ex) { 
                // basically ignore this or put up 
                // some ui saying we failed 
                Log.e(TAG, "store image fail, continue anyway", ex); 
            } 
        } 
 
        final Bitmap b = croppedImage; 
        mHandler.post(new Runnable() { 
            public void run() { 
                mImageView.clear(); 
                b.recycle(); 
            } 
        }); 
 
        finish(); 
    } 
 
    @Override 
    protected void onPause() { 
        super.onPause(); 
    } 
 
    @Override 
    protected void onDestroy() { 
        if (mAllImages != null) { 
            mAllImages.close(); 
        } 
        super.onDestroy(); 
    } 
 
    Runnable mRunFaceDetection = new Runnable() { 
        @SuppressWarnings("hiding"
        float mScale = 1F
        Matrix mImageMatrix; 
        FaceDetector.Face[] mFaces = new FaceDetector.Face[3]; 
        int mNumFaces; 
 
        // For each face, we create a HightlightView for it. 
        private void handleFace(FaceDetector.Face f) { 
            PointF midPoint = new PointF(); 
 
            int r = ((int) (f.eyesDistance() * mScale)) * 2
            f.getMidPoint(midPoint); 
            midPoint.x *= mScale; 
            midPoint.y *= mScale; 
 
            int midX = (int) midPoint.x; 
            int midY = (int) midPoint.y; 
 
            HighlightView hv = new HighlightView(mImageView); 
 
            int width = mBitmap.getWidth(); 
            int height = mBitmap.getHeight(); 
 
            Rect imageRect = new Rect(00, width, height); 
 
            RectF faceRect = new RectF(midX, midY, midX, midY); 
            faceRect.inset(-r, -r); 
            if (faceRect.left < 0) { 
                faceRect.inset(-faceRect.left, -faceRect.left); 
            } 
 
            if (faceRect.top < 0) { 
                faceRect.inset(-faceRect.top, -faceRect.top); 
            } 
 
            if (faceRect.right > imageRect.right) { 
                faceRect.inset(faceRect.right - imageRect.right, 
                               faceRect.right - imageRect.right); 
            } 
 
            if (faceRect.bottom > imageRect.bottom) { 
                faceRect.inset(faceRect.bottom - imageRect.bottom, 
                               faceRect.bottom - imageRect.bottom); 
            } 
 
            hv.setup(mImageMatrix, imageRect, faceRect, mCircleCrop, 
                     mAspectX != 0 && mAspectY != 0); 
 
            mImageView.add(hv); 
        } 
 
        // Create a default HightlightView if we found no face in the picture. 
        private void makeDefault() { 
            HighlightView hv = new HighlightView(mImageView); 
 
            int width = mBitmap.getWidth(); 
            int height = mBitmap.getHeight(); 
 
            Rect imageRect = new Rect(00, width, height); 
 
            // make the default size about 4/5 of the width or height 
            int cropWidth = Math.min(width, height) * 4 / 5
            int cropHeight = cropWidth; 
 
            if (mAspectX != 0 && mAspectY != 0) { 
                if (mAspectX > mAspectY) { 
                    cropHeight = cropWidth * mAspectY / mAspectX; 
                } else { 
                    cropWidth = cropHeight * mAspectX / mAspectY; 
                } 
            } 
 
            int x = (width - cropWidth) / 2
            int y = (height - cropHeight) / 2
 
            RectF cropRect = new RectF(x, y, x + cropWidth, y + cropHeight); 
            hv.setup(mImageMatrix, imageRect, cropRect, mCircleCrop, 
                     mAspectX != 0 && mAspectY != 0); 
            mImageView.add(hv); 
        } 
 
        // Scale the image down for faster face detection. 
        private Bitmap prepareBitmap() { 
            if (mBitmap == null) { 
                return null
            } 
 
            // 256 pixels wide is enough. 
            if (mBitmap.getWidth() > 256) { 
                mScale = 256.0F / mBitmap.getWidth(); 
            } 
            Matrix matrix = new Matrix(); 
            matrix.setScale(mScale, mScale); 
            Bitmap faceBitmap = Bitmap.createBitmap(mBitmap, 00, mBitmap 
                    .getWidth(), mBitmap.getHeight(), matrix, true); 
            return faceBitmap; 
        } 
 
        public void run() { 
            mImageMatrix = mImageView.getImageMatrix(); 
            Bitmap faceBitmap = prepareBitmap(); 
 
            mScale = 1.0F / mScale; 
            if (faceBitmap != null && mDoFaceDetection) { 
                FaceDetector detector = new FaceDetector(faceBitmap.getWidth(), 
                        faceBitmap.getHeight(), mFaces.length); 
                mNumFaces = detector.findFaces(faceBitmap, mFaces); 
            } 
 
            if (faceBitmap != null && faceBitmap != mBitmap) { 
                faceBitmap.recycle(); 
            } 
 
            mHandler.post(new Runnable() { 
                public void run() { 
                    mWaitingToPick = mNumFaces > 1
                    if (mNumFaces > 0) { 
                        for (int i = 0; i < mNumFaces; i++) { 
                            handleFace(mFaces[i]); 
                        } 
                    } else { 
                        makeDefault(); 
                    } 
                    mImageView.invalidate(); 
                    if (mImageView.mHighlightViews.size() == 1) { 
                        mCrop = mImageView.mHighlightViews.get(0); 
                        mCrop.setFocus(true); 
                    } 
 
                    if (mNumFaces > 1) { 
                        Toast t = Toast.makeText(CropImage.this
                                R.string.multiface_crop_help, 
                                Toast.LENGTH_SHORT); 
                        t.show(); 
                    } 
                } 
            }); 
        } 
    }; 
 
class CropImageView extends ImageViewTouchBase { 
    ArrayList<HighlightView> mHighlightViews = new ArrayList<HighlightView>(); 
    HighlightView mMotionHighlightView = null
    float mLastX, mLastY; 
    int mMotionEdge; 
 
    @Override 
    protected void onLayout(boolean changed, int left, int top, 
                            int right, int bottom) { 
        super.onLayout(changed, left, top, right, bottom); 
        if (mBitmapDisplayed.getBitmap() != null) { 
            for (HighlightView hv : mHighlightViews) { 
                hv.mMatrix.set(getImageMatrix()); 
                hv.invalidate(); 
                if (hv.mIsFocused) { 
                    centerBasedOnHighlightView(hv); 
                } 
            } 
        } 
    } 
 
    public CropImageView(Context context, AttributeSet attrs) { 
        super(context, attrs); 
    } 
 
    @Override 
    protected void zoomTo(float scale, float centerX, float centerY) { 
        super.zoomTo(scale, centerX, centerY); 
        for (HighlightView hv : mHighlightViews) { 
            hv.mMatrix.set(getImageMatrix()); 
            hv.invalidate(); 
        } 
    } 
 
    @Override 
    protected void zoomIn() { 
        super.zoomIn(); 
        for (HighlightView hv : mHighlightViews) { 
            hv.mMatrix.set(getImageMatrix()); 
            hv.invalidate(); 
        } 
    } 
 
    @Override 
    protected void zoomOut() { 
        super.zoomOut(); 
        for (HighlightView hv : mHighlightViews) { 
            hv.mMatrix.set(getImageMatrix()); 
            hv.invalidate(); 
        } 
    } 
 
    @Override 
    protected void postTranslate(float deltaX, float deltaY) { 
        super.postTranslate(deltaX, deltaY); 
        for (int i = 0; i < mHighlightViews.size(); i++) { 
            HighlightView hv = mHighlightViews.get(i); 
            hv.mMatrix.postTranslate(deltaX, deltaY); 
            hv.invalidate(); 
        } 
    } 
 
    // According to the event's position, change the focus to the first 
    // hitting cropping rectangle. 
    private void recomputeFocus(MotionEvent event) { 
        for (int i = 0; i < mHighlightViews.size(); i++) { 
            HighlightView hv = mHighlightViews.get(i); 
            hv.setFocus(false); 
            hv.invalidate(); 
        } 
 
        for (int i = 0; i < mHighlightViews.size(); i++) { 
            HighlightView hv = mHighlightViews.get(i); 
            int edge = hv.getHit(event.getX(), event.getY()); 
            if (edge != HighlightView.GROW_NONE) { 
                if (!hv.hasFocus()) { 
                    hv.setFocus(true); 
                    hv.invalidate(); 
                } 
                break
            } 
        } 
        invalidate(); 
    } 
 
    @Override 
    public boolean onTouchEvent(MotionEvent event) { 
        CropImage cropImage = (CropImage) getContext(); 
        if (cropImage.mSaving) { 
            return false
        } 
 
        switch (event.getAction()) { 
            case MotionEvent.ACTION_DOWN: 
                if (cropImage.mWaitingToPick) { 
                    recomputeFocus(event); 
                } else { 
                    for (int i = 0; i < mHighlightViews.size(); i++) { 
                        HighlightView hv = mHighlightViews.get(i); 
                        int edge = hv.getHit(event.getX(), event.getY()); 
                        if (edge != HighlightView.GROW_NONE) { 
                            mMotionEdge = edge; 
                            mMotionHighlightView = hv; 
                            mLastX = event.getX(); 
                            mLastY = event.getY(); 
                            mMotionHighlightView.setMode( 
                                    (edge == HighlightView.MOVE) 
                                    ? HighlightView.ModifyMode.Move 
                                    : HighlightView.ModifyMode.Grow); 
                            break
                        } 
                    } 
                } 
                break
            case MotionEvent.ACTION_UP: 
                if (cropImage.mWaitingToPick) { 
                    for (int i = 0; i < mHighlightViews.size(); i++) { 
                        HighlightView hv = mHighlightViews.get(i); 
                        if (hv.hasFocus()) { 
                            cropImage.mCrop = hv; 
                            for (int j = 0; j < mHighlightViews.size(); j++) { 
                                if (j == i) { 
                                    continue
                                } 
                                mHighlightViews.get(j).setHidden(true); 
                            } 
                            centerBasedOnHighlightView(hv); 
                            ((CropImage) getContext()).mWaitingToPick = false
                            return true
                        } 
                    } 
                } else if (mMotionHighlightView != null) { 
                    centerBasedOnHighlightView(mMotionHighlightView); 
                    mMotionHighlightView.setMode( 
                            HighlightView.ModifyMode.None); 
                } 
                mMotionHighlightView = null
                break
            case MotionEvent.ACTION_MOVE: 
                if (cropImage.mWaitingToPick) { 
                    recomputeFocus(event); 
                } else if (mMotionHighlightView != null) { 
                    mMotionHighlightView.handleMotion(mMotionEdge, 
                            event.getX() - mLastX, 
                            event.getY() - mLastY); 
                    mLastX = event.getX(); 
                    mLastY = event.getY(); 
 
                    if (true) { 
                        // This section of code is optional. It has some user 
                        // benefit in that moving the crop rectangle against 
                        // the edge of the screen causes scrolling but it means 
                        // that the crop rectangle is no longer fixed under 
                        // the user's finger. 
                        ensureVisible(mMotionHighlightView); 
                    } 
                } 
                break
        } 
 
        switch (event.getAction()) { 
            case MotionEvent.ACTION_UP: 
                center(truetrue); 
                break
            case MotionEvent.ACTION_MOVE: 
                // if we're not zoomed then there's no point in even allowing 
                // the user to move the image around.  This call to center puts 
                // it back to the normalized location (with false meaning don't 
                // animate). 
                if (getScale() == 1F) { 
                    center(truetrue); 
                } 
                break
        } 
 
        return true
    } 
 
    // Pan the displayed image to make sure the cropping rectangle is visible. 
    private void ensureVisible(HighlightView hv) { 
        Rect r = hv.mDrawRect; 
 
        int panDeltaX1 = Math.max(0, getLeft() - r.left); 
        int panDeltaX2 = Math.min(0, getRight() - r.right); 
 
        int panDeltaY1 = Math.max(0, getTop() - r.top); 
        int panDeltaY2 = Math.min(0, getBottom() - r.bottom); 
 
        int panDeltaX = panDeltaX1 != 0 ? panDeltaX1 : panDeltaX2; 
        int panDeltaY = panDeltaY1 != 0 ? panDeltaY1 : panDeltaY2; 
 
        if (panDeltaX != 0 || panDeltaY != 0) { 
            panBy(panDeltaX, panDeltaY); 
        } 
    } 
 
    // If the cropping rectangle's size changed significantly, change the 
    // view's center and scale according to the cropping rectangle. 
    private void centerBasedOnHighlightView(HighlightView hv) { 
        Rect drawRect = hv.mDrawRect; 
 
        float width = drawRect.width(); 
        float height = drawRect.height(); 
 
        float thisWidth = getWidth(); 
        float thisHeight = getHeight(); 
 
        float z1 = thisWidth / width * .6F
        float z2 = thisHeight / height * .6F
 
        float zoom = Math.min(z1, z2); 
        zoom = zoom * this.getScale(); 
        zoom = Math.max(1F, zoom); 
 
        if ((Math.abs(zoom - getScale()) / zoom) > .1) { 
            float [] coordinates = new float[] {hv.mCropRect.centerX(), 
                                                hv.mCropRect.centerY()}; 
            getImageMatrix().mapPoints(coordinates); 
            zoomTo(zoom, coordinates[0], coordinates[1], 300F); 
        } 
 
        ensureVisible(hv); 
    } 
 
    @Override 
    protected void onDraw(Canvas canvas) { 
        super.onDraw(canvas); 
        for (int i = 0; i < mHighlightViews.size(); i++) { 
            mHighlightViews.get(i).draw(canvas); 
        } 
    } 
 
    public void add(HighlightView hv) { 
        mHighlightViews.add(hv); 
        invalidate(); 
    } 
}