Custom ImageView with Zoom and Drag

I have made a custom ImageView that allows you to zoom in and out, and drag the window. The code is hosted in bitbucket: https://bitbucket.org/jewinaruto/zoomimage

Download  the apk corresponding to the video.

I tried to allow the user to customize the image as much as possible, so there are several custom parameters to configure in the xml. This are the things you can configure:

  • Adjust the image fit. If you disable this you can move or zoom the image as you want (Attr: adjustToBounds).
  • Block the image in the middle when it’s not occupying all the view (Google Pictures like). (Attr: blockImageInTheMidleWhileIsSmallerThanView)
  • Not allow the image never surpass the limits. If you let the image surpass the limits it will be like QuickPick (like the top video), if not it will be like Google Pictures or Google+ (like the video below). (Attr: allowExcedLimitsWhenMovingImage)
  • Double click to adjust image to center, or zoom in. You can enable it or no (Attr: doubleClickAdjust), set the maximum time to make a double click (Attr: doubleClickTimeInMillis), or set the zoom level double clicking by second time (Attr: doubleClickZoomLevel).
  • Configure the zoom levels. They are relative to the initial position of the view.(Attr: mininumZoomLevel & maximumZoomLevel). You can make this values relative to the size of the Image (Attr: isMinimumZoomLevelRelativeToView && isMaximumZoomLevelRelativeToView)

Download  the apk corresponding to the video.

Here is the code for the parameters:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ZoomImageView">
        <attr name="doubleClickTimeInMillis" format="integer"/>
        <attr name="mininumZoomLevel" format="float" />
        <attr name="maximumZoomLevel" format="float"/>
        <attr name="doubleClickZoomLevel" format="float" />
        <attr name="isMinimumZoomLevelRelativeToView" format="boolean" />
        <attr name="isMaximumZoomLevelRelativeToView" format="boolean"/>
        <attr name="adjustToBounds" format="boolean"/>
        <attr name="doubleClickAdjust" format="boolean"/>
        <attr name="allowExcedLimitsWhenMovingImage" format="boolean"/>
        <attr name="blockImageInTheMidleWhileIsSmallerThanView" format="boolean"/>
    </declare-styleable>
</resources>

Parsing the attributes:

private void parseAttributes(Context context, AttributeSet attrs) {
    TypedArray a = context.getTheme().obtainStyledAttributes(
            attrs, R.styleable.ZoomImageView, 0, 0);
    try {
        maxZoomLevel = a.getFloat(R.styleable.ZoomImageView_maximumZoomLevel, 3f);
        minZoomLevel = a.getFloat(R.styleable.ZoomImageView_mininumZoomLevel, 1f);
        doubleClickZoomLevel = a.getFloat(R.styleable.ZoomImageView_doubleClickZoomLevel, 2f);
        isMaxZoomLevelRelative = a.getBoolean(R.styleable.ZoomImageView_isMaximumZoomLevelRelativeToView, true);
        isMinZoomLevelRelative = a.getBoolean(R.styleable.ZoomImageView_isMinimumZoomLevelRelativeToView, true);
        adjustToBounds = a.getBoolean(R.styleable.ZoomImageView_adjustToBounds, true);
        doubleClickAdjust = a.getBoolean(R.styleable.ZoomImageView_doubleClickAdjust, true);
        allowExcedLimitsWhenMovingImage = a.getBoolean(R.styleable.ZoomImageView_allowExcedLimitsWhenMovingImage, false);
        blockImageInTheMiddle = a.getBoolean(R.styleable.ZoomImageView_blockImageInTheMidleWhileIsSmallerThanView, true);
        doubleClickTimeInMillis = a.getInteger(R.styleable.ZoomImageView_doubleClickTimeInMillis, 250);
    } finally {
        a.recycle();
    }
}

And here we have an example of how to configure the image view (this was the configuration for the second video).

<es.slothdevelopers.zoomimages.views.ZoomImageView
    android:id="@+id/image"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/picture_01"
    custom:allowExcedLimitsWhenMovingImage="false"
    custom:blockImageInTheMidleWhileIsSmallerThanView="true"
    custom:doubleClickTimeInMillis="275"
    custom:doubleClickZoomLevel="3"
    custom:maximumZoomLevel="3"
    />

The code is too extend to put it in the post, but bassically you have to extend an ImageView and set it an onTouch listener.

Here you have the code for the custom imageview. This fragment shows you the onTouch method.

@Override
public boolean onTouch(View view, MotionEvent event) {
    if (!areValuesInitialized){
        initializeValues();
    }
    if (mode == CENTERING_IMAGE){
        return true;
    }

    switch (event.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:
            savedMatrix.set(matrix);
            start.set(event.getX(), event.getY());
            mode = DRAG;
            break;
        case MotionEvent.ACTION_POINTER_DOWN:
            oldDistance = spacing(event);
            if (oldDistance > 10f) {
                savedMatrix.set(matrix);
                midPoint(mid, event);
                mode = ZOOM;
            }
            break;
        case MotionEvent.ACTION_UP:
            checkClick(event);
            if (mode != CENTERING_IMAGE){
                postAdjust();
            }
        case MotionEvent.ACTION_POINTER_UP:
            setModeToNoneIfNotAdjustingOrCenteringView();
            break;
        case MotionEvent.ACTION_MOVE:
            if (mode == DRAG) {
                moveImage(event);
            } else if (mode == ZOOM) {
                scaleImage(event);
            }
            preAdjust();
            break;
    }

    return true;
}

To move and scale the images we use matrix.

In this case we use a 3×3 matrix, for this example we can use this as a reference:

|f0 f1 f2|
|f3 f4 f5|
|f6 f7 f8|

This are the corresponding values:

  • f0 -> x scale value
  • f2 -> x position value
  • f4 -> y scale value
  • f5 -> y position value

For this example the x scale value will always be equal to y scale value.

To adjust the image with an animation I use a Runnable and an Interpolator, I took the idea from here.

private void interpolateMatrixToValue(final float destinyMatrixValues[]){
    final float originMatrixValues[] = new float[9];
    matrix.getValues(originMatrixValues);

    final Interpolator interpolator = new AccelerateDecelerateInterpolator();
    final long startTime = System.currentTimeMillis();
    final long duration = 400;
    post(new Runnable() {
        @Override
        public void run() {
            float tempMatrix[] = new float[9];

            float t = (float) (System.currentTimeMillis() - startTime) / duration;
            t = t > 1.0f ? 1.0f : t;
            float interpolatedRatio = interpolator.getInterpolation(t);

            for (int i = 0; i < 9; i++){
                tempMatrix[i] =
                        originMatrixValues[i] +
                                interpolatedRatio * (destinyMatrixValues[i] - originMatrixValues[i]);
            }

            matrix.setValues(tempMatrix);
            setImageMatrix(matrix);
            if ((t < 1f) && ((mode == ADJUSTING) || mode == CENTERING_IMAGE)) {
                post(this);
            } else {
                if ((mode == CENTERING_IMAGE) || (mode == ADJUSTING)) {
                    mode = NONE;
                }
            }
        }
    });
}

In the future I want to add kinetic scrolling and transform this code into a library.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s