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

Custom fonts in TextView and FontCache

Lot of times we want to  use a custom font in a TextView. One way to achieve this is to create our custom class that extends TextView.

First we place our fonts in the Asset folder. In this case we are going to use the SEGOE font, and put the font under the fonts folder in the assets.

Then we create our custom class. I have place it under es.slothdevelopers.views. And I have call it SegoeTextView.

public class SegoeTextView extends TextView {

    public SegoeTextView(Context context) {
        super(context);
    }

    public SegoeTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SegoeTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public void setTypeface(Typeface tf, int style) {
        if (!this.isInEditMode()) {
            if (style == Typeface.NORMAL) {
                super.setTypeface(FontCache.getFont(getContext(), "fonts/SEGOEUI.TTF"));
            } else if (style == Typeface.ITALIC) {
                super.setTypeface(FontCache.getFont(getContext(), "fonts/SEGOEUII.TTF"));
            } else if (style == Typeface.BOLD) {
                super.setTypeface(FontCache.getFont(getContext(), "fonts/SEGOEUIB.TTF"));
            }
        }
    }
}

We have override the setTypeface method to be able to change the font between bold, italic and normal in the layout without having to create another custom class.

We also used a FontCache to load the typeface:

FontCache.getFont(getContext(), "fonts/SEGOEUI.TTF")

Instead of calling:

Typeface.createFromAsset(getContext().getAssets(), "fonts/SEGOEUI.TTF");

It’s a good practice to use a font cache, the performance is much better. This can be quite important in elements that are created lot of times, such a ListView or a GridView.

Here we have the code of our FontCache:

public class FontCache {
    private static Map<String, Typeface> fontMap = new HashMap<String, Typeface>();

    public static Typeface getFont(Context context, String fontName){
        if (fontMap.containsKey(fontName)){
            return fontMap.get(fontName);
        }
        else {
            Typeface tf = Typeface.createFromAsset(context.getAssets(), fontName);
            fontMap.put(fontName, tf);
            return tf;
        }
    }
}

Now to use this custom TextView we just have to change our layout. For example:

<es.slothdevelopers.views.SegoeTextView
                        android:id="@+id/date"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="Mar 18, 2014 a las 18:01"
                        android:textColor="@color/text_color"
                        android:textSize="12sp" />

In the layout we can change the textStyle and it will change in our app.
In this view I used the same view for the name that for the date. I only had to add: android:textStyle=”bold”.

Custom TextViews