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.

Custom header with parallax effect in ListView

We will see how to set a custom header to our ListView and apply a parallax effect to the header image.

You can download the example code and the apk.

Parallax effect

Here is the custom header I use:

Custom header

You can check the layout files:

The main layout is a ListView, and we define the element layout in its own xml.

Each element of the ListView will represent a Model. For the example I use a basic model that has a name and a description.

public class Model {
    private String name;
    private String description;

    public Model(String name, String description) {
        this.name = name;
        this.description = description;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

We also have to define an adapter for that model.

public class ModelAdapter extends SlothArrayAdapter<Model> {

    @InjectView(R.id.name)          TextView name;
    @InjectView(R.id.description)   TextView description;

    public ModelAdapter(Context mContext, int layoutResourceId, List<Model> data) {
        super(mContext, layoutResourceId, data);
    }

    @Override
    protected void onCreateViewForPosition(View viewCreated, int position, Model data) {
        name.setText(data.getName());
        description.setText(data.getDescription());
    }
}

I’m using a custom array adapter from slothframework. That way I can use view injection and two custom methods onCreateViewForPosition and onCreatingLastView.
The view injection it’s not done unnecessarily, I’ve tried to implement some kind of ViewHolder pattern[performance tips for android’s ListViews].

Of course you can have your own adapter.

Now we will analyze the code of the MainActivity. The first thing we do is initialize the adapter and set it to the view.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    initAdapter();
    ...
}

private void initAdapter() {
    // instantiate the adapter and attach it to the listview
    adapter = new ModelAdapter(this, R.layout.element_list_view, modelList);
    listView.setAdapter(adapter);
}

We inflate and set the custom header:

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    inflateHeader();
    ...
}
...
private void inflateHeader() {
    // inflate custom header and attach it to the list
    LayoutInflater inflater = getLayoutInflater();
    ViewGroup header = (ViewGroup)inflater.inflate(R.layout.custom_header, listView, false);
    listView.addHeaderView(header, null, false);

    // we take the background image and button reference from the header
    backgroundImage = (ImageView) header.findViewById(R.id.customHeaderBackground);
    postButton  = (Button) header.findViewById(R.id.postsButton);
}

Parallax effect

There are other ways to get the parallax effect, but here we will see a very simple one. By now this parallax effect is for api level 11 and above (Android 3.0).

Our ListView must have its custom ScrollListener, so we add it in the onCreate method of our MainActivity. We only add it to devices with api level 11 and above for compatibility.

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    addScrollListenerForSDKsAbove11();
    ...
}

private void addScrollListenerForSDKsAbove11() {
    if (Integer.valueOf(Build.VERSION.SDK_INT)>11) {
        listView.setOnScrollListener(this);
    }
}

We must override the onScroll method and set the top of the image to its middle value:

// override the OnScrollListener methods: onScrollStateChanged & onScroll
@Override
public void onScrollStateChanged(AbsListView absListView, int i) {}

@Override
public void onScroll(AbsListView absListView, int i, int i2, int i3) {
    parallaxImage(backgroundImage);
}

private void parallaxImage(View view) {
    Rect rect = new Rect();
    view.getLocalVisibleRect(rect);
    if (lastTopValueAssigned != rect.top){
        lastTopValueAssigned = rect.top;
        view.setY((float) (rect.top/2.0));
    }
}

The effect is done in the parallaxImage method, when we call view.setY. Here we are displacing the view down half the size that the view goes up. For each 2 pixels that the view goes up the screen we displace the view 1 pixel down.

To avoid calling view.setY() unnecessarily we had defined a field to check if the value has changed (lastTopValueAssigned)

Here are the code references again:

Background image

Underlining views with custom shapes, overdraw performance

After the post of underlining views and custom drawables in android some people ask me about performance. So let’s talk about it.

So after a little research about performance I found this post http://www.curious-creature.org/2012/12/01/android-performance-case-study/ talking about performance and overdraw. I recommend read the post, but in resume we refer to overdraw as the number of times the GPU has to draw the screen for each component. You can active it in the Developer options as Show GPU overdraw.

It will color your screen this way:

Wordpress overdraw

  • No color means there is no overdraw. The pixel was painted only once. In this example, you can see that the background is intact.
  • Blue indicates an overdraw of 1x. The pixel was painted twice. Large blue areas are acceptable (if the entire window is blue, you can get rid of one layer.)
  • Green indicates an overdraw of 2x. The pixel was painted three times. Medium-sized green areas are acceptable but you should try to optimize them away.
  • Light red indicates an overdraw of 3x. The pixel was painted four times. Small light red areas are acceptable.
  • Dark red indicates an overdraw of 4x or more. The pixel was painted 5 times or more. This is wrong. Fix it.

In the other post we put this drawable:

Layers 2

<?xml version="1.0" encoding="utf-8"?>

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_selected="true">
        <layer-list>
            <item>
                <shape>
                    <solid android:color="#ffff7a00" />
                </shape>
            </item>
            <item android:bottom="3dp">
                <shape>
                    <solid android:color="#222222" />
                </shape>
            </item>
        </layer-list>
    </item>
</selector>

And we can see it has a 2x overdraw (green color) because of the 2 layers:

2 x overdraw

But we can fix that by changing the drawable for not to overlap between the layers.

layers_no_overlap_1

<?xml version="1.0" encoding="utf-8"?>

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_selected="true">
        <layer-list>
            <item android:bottom="3dp">
                <shape>
                    <solid android:color="#222222" />
                </shape>
            </item>
            <item android:top="29dp">
                <shape >
                    <solid android:color="#ffff7a00" />
                </shape>
            </item>
        </layer-list>
    </item>
</selector>

Now we have a 1x (blue) overdraw:

1 Overdraw

Because now the layers do not overlap it doesn’t matter which layer goes on top of another.

The main drawback of this method is that you only can use if you know beforehand the height of the drawable.

If you don’t know the height you will have to use a 9patch button or keep it like we put in the previous post paying the overdraw cost.

In our case we would use the drawable method in development to be able to change the drawables fast, and change to 9patch button if needed.

Example with 3 layer

This drawable is compound of 3 layers (see this post if you want to see the xml).

3 Layers example

The overdraw of this view is 3x:

3xoverdraw

Changing the drawable we can reduce it to a 1x factor:

Layers no overlap 2

<item android:state_selected="true">
    <layer-list>
        <item android:top="29dp">
            <shape>
                <solid android:color="#222222" />
            </shape>

        </item>
        <item android:top="26dp" android:bottom="3dp">
            <shape >

                <solid android:color="#ffff7a00" />
            </shape>
        </item>
        <item android:bottom="6dp">
            <shape>
                <solid android:color="#222222" />
            </shape>
        </item>
    </layer-list>
</item>

With the new xml the overdraw remains 1x (blue).

New overdraw for 3 layers

Underlining views, custom drawables in android

Edit:
In this post we talk about the performance of this method.

Sometimes we want to underline a view (a TextView or a Button), just to show the user that it’s active, or any other reason.

To achieve this we use custom drawables. In this case we use a layer-list.

The first element of the layer list is the base, and every item we put it’s a layer that goes on top, let’s try with an image, and it corresponding xml:

Layers

And here is the drawable. We can see that the first element is the base layer:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <layer-list>
            <item>
                <!-- layer 1-->
            </item>
            <item>
                <!-- layer 2-->
            </item>
            <item>
                <!-- layer 3-->
            </item>
            <item>
                <!-- layer 4-->
            </item>
        </layer-list>
    </item>
</selector>

And now to achieve the effect we must know the bottom, top, right and left properties of item. They are similar to the margin property, but the are relative to the view.

So if we set the bottom of the second layer to 3dp (for example) we will be able to see 3dp of the layer below in the bottom. Here is an example:

Layers 2

<?xml version="1.0" encoding="utf-8"?>

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_selected="true">
        <layer-list>
            <item>
                <shape>
                    <solid android:color="#ffff7a00" />
                </shape>
            </item>
            <item android:bottom="3dp">
                <shape>
                    <solid android:color="#222222" />
                </shape>
            </item>
        </layer-list>
    </item>
</selector>

Now we only need to assign the drawable to the background property of our view (ImageView, Button, etc).

Here is how it came out in my app:

Bar

Other effects

We can put lines in top and bottom at the same time:

Top and bottom

<?xml version="1.0" encoding="utf-8"?>

<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:state_selected="true">
        <layer-list>
            <item>
                <shape>
                    <solid android:color="#ffff7a00" />
                </shape>
            </item>
            <item android:top="3dp" android:bottom="3dp">
                <shape>
                    <solid android:color="#222222" />
                </shape>
            </item>
        </layer-list>
    </item>
</selector>

We can also put a line in one (or both) sides. Here we have a line in the left.

Left

<?xml version="1.0" encoding="utf-8"?>

<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:state_selected="true">
        <layer-list>
            <item>
                <shape >

                    <solid android:color="#ffff7a00" />
                </shape>
            </item>
            <item android:left="3dp">
                <shape>
                    <solid android:color="#222222" />
                </shape>
            </item>
        </layer-list>
    </item>
</selector>

We can use more that 2 layers to achieve new effects. Here we use 3 layers to get this effect.

Explaining 3 layers
3 Layers example

<?xml version="1.0" encoding="utf-8"?>

<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:state_selected="true">
        <layer-list>
            <item >
                <shape>
                    <solid android:color="#222222" />
                </shape>

            </item>
            <item android:bottom="3dp">
                <shape >

                    <solid android:color="#ffff7a00" />
                </shape>
            </item>
            <item android:bottom="6dp">
                <shape>
                    <solid android:color="#222222" />
                </shape>
            </item>
        </layer-list>
    </item>
</selector>

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