Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I move and scale an image like the Facebook profile image selection tool does?

I want crop image like Facebook profile image selection on Android, where the user can move and scale an image, causing it to be resized and/or cropped:

screenshot of the tool in question

How might I accomplish this?

like image 655
Chirag Avatar asked Mar 27 '14 09:03

Chirag


3 Answers

I had the same requirement. I solved it combining PhotoView and Cropper by replacing the ImageView with PhotoView in cropper lib.

I had to modify the CropWindow class in order to avoid touch events not being correctly handled:

   public void setImageView(PhotoView pv){
        mPhotoView = pv;
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        // If this View is not enabled, don't allow for touch interactions.
        if (!isEnabled()) {
            return false;
        }

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                  boolean dispatch = onActionDown(event.getX(), event.getY());
                  if(!dispatch)
                    mPhotoView.dispatchTouchEvent(event);

                return dispatch;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                getParent().requestDisallowInterceptTouchEvent(false);
                onActionUp();
                return true;

            case MotionEvent.ACTION_MOVE:
                onActionMove(event.getX(), event.getY());
                getParent().requestDisallowInterceptTouchEvent(true);
                return true;

            default:
                return false;
        }
    }

In CropImageView class changed few things as well:

private void init(Context context) {

    final LayoutInflater inflater = LayoutInflater.from(context);
    final View v = inflater.inflate(R.layout.crop_image_view, this, true);

    mImageView = (PhotoView) v.findViewById(R.id.ImageView_image2);

    setImageResource(mImageResource);
    mCropOverlayView = (CropOverlayView) v.findViewById(R.id.CropOverlayView);
    mCropOverlayView.setInitialAttributeValues(mGuidelines, mFixAspectRatio, mAspectRatioX, mAspectRatioY);
    mCropOverlayView.setImageView(mImageView);
}

You can notice that I have replaced ImageView with PhotoView inside R.layout.crop_image_view in Cropper library.

Cropper library supports fixed size and PhotoView allows you to move and scale the photo, giving you the best from both worlds. :)

Hope it helps.

Edit, for those that asked how to get the image that is only inside the crop area:

private Bitmap getCurrentDisplayedImage(){
        Bitmap result = Bitmap.createBitmap(mImageView.getWidth(), mImageView.getHeight(), Bitmap.Config.RGB_565);
        Canvas c = new Canvas(result);
        mImageView.draw(c);
        return result;
    }
    public Bitmap getCroppedImage() {

        Bitmap mCurrentDisplayedBitmap = getCurrentDisplayedImage();
        final Rect displayedImageRect = ImageViewUtil2.getBitmapRectCenterInside(mCurrentDisplayedBitmap, mImageView);

        // Get the scale factor between the actual Bitmap dimensions and the
        // displayed dimensions for width.
        final float actualImageWidth =mCurrentDisplayedBitmap.getWidth();
        final float displayedImageWidth = displayedImageRect.width();
        final float scaleFactorWidth = actualImageWidth / displayedImageWidth;

        // Get the scale factor between the actual Bitmap dimensions and the
        // displayed dimensions for height.
        final float actualImageHeight = mCurrentDisplayedBitmap.getHeight();
        final float displayedImageHeight = displayedImageRect.height();
        final float scaleFactorHeight = actualImageHeight / displayedImageHeight;

        // Get crop window position relative to the displayed image.
        final float cropWindowX = Edge.LEFT.getCoordinate() - displayedImageRect.left;
        final float cropWindowY = Edge.TOP.getCoordinate() - displayedImageRect.top;
        final float cropWindowWidth = Edge.getWidth();
        final float cropWindowHeight = Edge.getHeight();

        // Scale the crop window position to the actual size of the Bitmap.
        final float actualCropX = cropWindowX * scaleFactorWidth;
        final float actualCropY = cropWindowY * scaleFactorHeight;
        final float actualCropWidth = cropWindowWidth * scaleFactorWidth;
        final float actualCropHeight = cropWindowHeight * scaleFactorHeight;

        // Crop the subset from the original Bitmap.
        final Bitmap croppedBitmap = Bitmap.createBitmap(mCurrentDisplayedBitmap,
                                                         (int) actualCropX,
                                                         (int) actualCropY,
                                                         (int) actualCropWidth,
                                                         (int) actualCropHeight);

        return croppedBitmap;
    }

    public RectF getActualCropRect() {

        final Rect displayedImageRect = ImageViewUtil.getBitmapRectCenterInside(mBitmap, mImageView);

        final float actualImageWidth = mBitmap.getWidth();
        final float displayedImageWidth = displayedImageRect.width();
        final float scaleFactorWidth = actualImageWidth / displayedImageWidth;

        // Get the scale factor between the actual Bitmap dimensions and the displayed
        // dimensions for height.
        final float actualImageHeight = mBitmap.getHeight();
        final float displayedImageHeight = displayedImageRect.height();
        final float scaleFactorHeight = actualImageHeight / displayedImageHeight;

        // Get crop window position relative to the displayed image.
        final float displayedCropLeft = Edge.LEFT.getCoordinate() - displayedImageRect.left;
        final float displayedCropTop = Edge.TOP.getCoordinate() - displayedImageRect.top;
        final float displayedCropWidth = Edge.getWidth();
        final float displayedCropHeight = Edge.getHeight();

        // Scale the crop window position to the actual size of the Bitmap.
        float actualCropLeft = displayedCropLeft * scaleFactorWidth;
        float actualCropTop = displayedCropTop * scaleFactorHeight;
        float actualCropRight = actualCropLeft + displayedCropWidth * scaleFactorWidth;
        float actualCropBottom = actualCropTop + displayedCropHeight * scaleFactorHeight;

        // Correct for floating point errors. Crop rect boundaries should not exceed the
        // source Bitmap bounds.
        actualCropLeft = Math.max(0f, actualCropLeft);
        actualCropTop = Math.max(0f, actualCropTop);
        actualCropRight = Math.min(mBitmap.getWidth(), actualCropRight);
        actualCropBottom = Math.min(mBitmap.getHeight(), actualCropBottom);

        final RectF actualCropRect = new RectF(actualCropLeft,
                                               actualCropTop,
                                               actualCropRight,
                                               actualCropBottom);

        return actualCropRect;
    }




private boolean onActionDown(float x, float y) {    
        final float left = Edge.LEFT.getCoordinate();
        final float top = Edge.TOP.getCoordinate();
        final float right = Edge.RIGHT.getCoordinate();
        final float bottom = Edge.BOTTOM.getCoordinate();    
        mPressedHandle = HandleUtil.getPressedHandle(x, y, left, top, right, bottom, mHandleRadius);    
        if (mPressedHandle == null)
            return false;
        mTouchOffset = HandleUtil2.getOffset(mPressedHandle, x, y, left, top, right, bottom);

        invalidate();
        return true;
    }
like image 186
Nikola Despotoski Avatar answered Nov 04 '22 16:11

Nikola Despotoski


I have some additions to @Nikola Despotoski answer. Firstly, you don't have to change ImageView in R.layout.crop_image_view to PhotoView, because PhotoView logic can be simply attached in code as new PhotoViewAttacher(mImageView).

Also in default logic, a CropView's overlay size calculates only on its initialization according to imageView bitmap size. So it is not appropriate logic for us, becouse we change bitmap size by touches according to the requirement. So, we should change stored bitmap sizes in CropOverlayView and invalidate it each time when we change the main image.

And the last is that a range, where user can make cropping normally based on the image size, but if we made image bigger, it can go beyond the border of screen, so it will be possible to user to move a cropping view beyond the border, which is incorrect. So we also should handle this situation and provide limitation.

And the corresponding part of code for this three issues: In CropImageView:

 private void init(Context context) {

    final LayoutInflater inflater = LayoutInflater.from(context);
    final View v = inflater.inflate(R.layout.crop_image_view, this, true);

    mImageView = (ImageView) v.findViewById(R.id.ImageView_image);

    setImageResource(mImageResource);
    mCropOverlayView = (CropOverlayView) v.findViewById(R.id.CropOverlayView);
    mCropOverlayView.setInitialAttributeValues(mGuidelines, mFixAspectRatio, mAspectRatioX, mAspectRatioY);
    mCropOverlayView.setOutlineTouchEventReceiver(mImageView);

    final PhotoViewAttacher photoAttacher = new PhotoViewAttacher(mImageView);
    photoAttacher.setOnMatrixChangeListener(new PhotoViewAttacher.OnMatrixChangedListener() {
        @Override
        public void onMatrixChanged(RectF imageRect) {
        final Rect visibleRect = ImageViewUtil.getBitmapRectCenterInside(photoAttacher.getVisibleRectangleBitmap(), photoAttacher.getImageView());

        imageRect.top = Math.max(imageRect.top, visibleRect.top);
        imageRect.left = Math.max(imageRect.left, visibleRect.left);
        imageRect.right = Math.min(imageRect.right, visibleRect.right);
        imageRect.bottom = Math.min(imageRect.bottom, visibleRect.bottom);

        Rect bitmapRect = new Rect();
        imageRect.round(bitmapRect);

        mCropOverlayView.changeBitmapRectInvalidate(bitmapRect);
        }
    });
}

In CropOverlayView:

@Override
public boolean onTouchEvent(MotionEvent event) {

    // If this View is not enabled, don't allow for touch interactions.
    if (!isEnabled()) {
        return false;
    }

    switch (event.getAction()) {

        case MotionEvent.ACTION_DOWN:
            return onActionDown(event.getX(), event.getY());

        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            getParent().requestDisallowInterceptTouchEvent(false);
            return onActionUp();

        case MotionEvent.ACTION_MOVE:
            boolean result = onActionMove(event.getX(), event.getY());
            getParent().requestDisallowInterceptTouchEvent(true);
            return result;

        default:
            return false;
    }
}

public void changeBitmapRectInvalidate(Rect bitmapRect) {
    mBitmapRect = bitmapRect;
    invalidate();
}

private boolean onActionDown(float x, float y) {

    final float left = Edge.LEFT.getCoordinate();
    final float top = Edge.TOP.getCoordinate();
    final float right = Edge.RIGHT.getCoordinate();
    final float bottom = Edge.BOTTOM.getCoordinate();

    mPressedHandle = HandleUtil.getPressedHandle(x, y, left, top, right, bottom, mHandleRadius);

    if (mPressedHandle == null){
        return false;
    }

    // Calculate the offset of the touch point from the precise location
    // of the handle. Save these values in a member variable since we want
    // to maintain this offset as we drag the handle.
    mTouchOffset = HandleUtil.getOffset(mPressedHandle, x, y, left, top, right, bottom);

    invalidate();
    return true;
}

/**
 * Handles a {@link MotionEvent#ACTION_UP} or
 * {@link MotionEvent#ACTION_CANCEL} event.
 * @return true if event vas handled, else - false
 */
private boolean onActionUp() {

    if (mPressedHandle == null)
        return false;

    mPressedHandle = null;

    invalidate();
    return true;
}

/**
 * Handles a {@link MotionEvent#ACTION_MOVE} event.
 * 
 * @param x the x-coordinate of the move event
 * @param y the y-coordinate of the move event
 */
private boolean onActionMove(float x, float y) {

    if (mPressedHandle == null)
        return false;

    // Adjust the coordinates for the finger position's offset (i.e. the
    // distance from the initial touch to the precise handle location).
    // We want to maintain the initial touch's distance to the pressed
    // handle so that the crop window size does not "jump".
    x += mTouchOffset.first;
    y += mTouchOffset.second;

    // Calculate the new crop window size/position.
    if (mFixAspectRatio) {
        mPressedHandle.updateCropWindow(x, y, mTargetAspectRatio, mBitmapRect, mSnapRadius);
    } else {
        mPressedHandle.updateCropWindow(x, y, mBitmapRect, mSnapRadius);
    }
    invalidate();
    return true;
}

For properly getting cropped image you should use the second part of @Nikola Despotoski answer

like image 41
Beloo Avatar answered Nov 04 '22 15:11

Beloo


what you want can be exactly achieved by this lib simple-crop-image-lib

like image 35
Vibhor Bhardwaj Avatar answered Nov 04 '22 14:11

Vibhor Bhardwaj