Welcome to this first Android tutorial on how to make your own one finger zoom control like the one found in the Camera and Album applications in the Sony Ericsson X10 Mini. The Tutorial is divided into four parts, each part adding new features. Below is a link to download the source code for part 1 of the tutorial, prepared for you to set up your own project in e.g. Eclipse.

[Download] One Finger Zoom sample project (211kb)

Don’t miss to download the Sony Ericsson Tutorials app on Android Market where all applications in this and other Sony Ericsson tutorials are available. With the SonyEricsson tutorials app you can easily try out the different parts of the tutorial and see what the end result will be.

Below is a video showing what you will be able to do after seeing all steps of the tutorial. There following parts of the turoital will be added over the next few weeks.

 

There will be some math in this tutorial, but there is no need to get discouraged if it is too much for you. Please feel free to skip the math part and accept that they work.

In this first part of the tutorial we are going to make a basic, interactive, zoom control that allows us to zoom into an image. This will be achieved by implementing three classes representing the following: 1) our own View class for drawing images with zoom applied to it, 2) a class holding the state of the zoom, and 3) a class implementing the OnTouchListener interface allowing the user to interact with the zoom. As we progress in the tutorial we will see the benefits of dividing our application into different classes this way, as it makes it easy to change specific parts of the behavior.

Basic zoom listener

Defining a zoom state

The first thing we need to do is to define how the zoom state should work. The goal for our definition should be to make it easy to understand, simple to interact with and do math on. There is probably other decent ways to do this, but I’ll show you one that worked for me.

We’ll define the zoom state using three parameters.

  • The amount of zoom, that is how much the content has been scaled. We let a zoom value of 1 define the state where the content fits the view perfectly in at least one dimension.
  • The amount of pan in dimension-x (pan-x), that is how much the content is moved in respect to the width of the screen. We let the pan value define where the center position of the zoom-view (the view defining which part of the content we see) is in relation to the content. For example, a pan-x value of 0.5 would mean the center of the content would be visible in the center of the view, while a value of 0 would mean the left hand side of the content would be visible in the center of the screen.
  • The amount of pan in dimension-y (pan-y), like the pan-x except how the content is moved in relation to the height of the screen.

Images illustrating how the zoom state works, the dashed gray area represents what is shown in the view and the patterned area represents the content. On the left: Zoom is 1, pan-x and pan-y are both 0.5, in this state the image fits the screen perfectly. In the middle: Zoom is 2, pan-x and pan-y are still both 0.5, less content is now shown on the screen but will be scaled up. To the right: Zoom is 3, pan-x is 0.7 and pan-y is 0.833, we now see less of the image, only the top right corner, scaled up.

Implementation

Now that we’ve got the basics laid down, let’s move on to coding and let’s start by implementing the state. Since we know the view will want to determine when the state has been modified we can implement the state as an instance of Observable, allowing us to easily notify other objects of state changes.
[java]
public class ZoomState extends Observable {

private float mZoom;
private float mPanX;
private float mPanY;

public float getPanX() {
return mPanX;
}

public float getPanY() {
return mPanY;
}

public float getZoom() {
return mZoom;
}

public void setPanX(float panX) {
if (panX != mPanX) {
mPanX = panX;
setChanged();
}
}

public void setPanY(float panY) {
if (panY != mPanY) {
mPanY = panY;
setChanged();
}
}

public void setZoom(float zoom) {
if (zoom != mZoom) {
mZoom = zoom;
setChanged();
}
}
}
[/java]

As simple as that, get/set methods for the available parameters and a call to setChanged() every time a parameter is changed. The client changing the values is responsible for calling notifyObservers() after modifying the state. Now that we have a state class, let’s move on to the view, we will extend the basic View class and implement the Observer interface in order to be able to observe and get callbacks when the state changes.

[java]
public class ImageZoomView extends View implements Observer {

private Bitmap mBitmap;
private ZoomState mState;

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

public void setImage(Bitmap bitmap) {
mBitmap = bitmap;

invalidate();
}

public void setZoomState(ZoomState state) {
if (mState != null) {
mState.deleteObserver(this);
}

mState = state;
mState.addObserver(this);

invalidate();
}

@Override
protected void onDraw(Canvas canvas) {
}

public void update(Observable observable, Object data) {
invalidate();
}

}
[/java]
Now we have created the basic structure of the view, and as this will be a zoom view for images we will be using a Bitmap object as the zoom content. We can set a bitmap to zoom in, we can set a zoom state to observe and we call invalidate when the content or the state changes. Next step we want to draw the bitmap, for this we need a Paint object and two Rect objects representing rectangles; one for the part of the content we wish to draw and one for the area of the view we wish to draw on.

[java]
private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
private final Rect mRectSrc = new Rect();
private final Rect mRectDst = new Rect();

@Override
protected void onDraw(Canvas canvas) {
if (mBitmap != null && mState != null) {
canvas.drawBitmap(mBitmap, mRectSrc, mRectDst, mPaint);
}
}
[/java]
As you can see there is a drawBitmap method on the Canvas object that takes two rectangle objects as input parameters just the way we need it to, and since we will be scaling the bitmap we should enable the bitmap filtering on the paint object. Finally we need to set up the rectangles correctly, and then we’re done.

To set up the rectangles so that the visible part of the source image is cropped correctly and drawn at the correct location on the view we need to introduce a new variable. The reason for this is that if the content does not have the same aspect ratio (width / height) as the view, then a zoom value of 1 will mean that only one of the dimensions fit the view. In the other dimension the content will only partially cover the view, which means it will correspond to a zoom value lower than 1 in that particular dimension. The solution for this is to introduce a variable we will call the aspect quotient, that is the quotient between the aspect ratio of the content and the aspect ratio of the view. By using this value we can separate the zoom value on the x-axis and on the y-axis, and to do so we will add help methods to the ZoomState class to calculate them.

[java]
public float getZoomX(float aspectQuotient) {
return Math.min(mZoom, mZoom * aspectQuotient);
}

public float getZoomY(float aspectQuotient) {
return Math.min(mZoom, mZoom / aspectQuotient);
}
[/java]
As the aspect quotient is defined by the view characteristics and content we will store this value in the ZoomView, and update its value whenever changes are made to the view or content (the content in our case being a bitmap). Such changes are for example if the view changes in size due to the user changing the phone orientation, or if the content changes because of the user switching between zoomable images in an album application.

[java]
private float mAspectQuotient;

private void calculateAspectQuotient() {
if (mBitmap != null) {
mAspectQuotient =
(((float)mBitmap.getWidth()) / mBitmap.getHeight()) /
(((float)getWidth()) / getHeight());
}
}

public void setImage(Bitmap bitmap) {
mBitmap = bitmap;

calculateAspectQuotient();

invalidate();
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);

calculateAspectQuotient();
}
[/java]
With the help of these methods we can now implement the onDraw method correctly. We’ll do this by first calculating how much scaling to apply in relation to the full size of the content (zoomX, zoomY). Then we calculate the source rectangle to crop the portion of the content that should be visible, allowing the calculations to crop outside of the content boundaries. Finally we need to recalculate the source rectangle as the drawBitmap function does not support cropping outside of our bitmap content, and then we recalculate the destination rectangle to compensate for this.

Images showing how the zooming is done by setting up source and destination rectangles. The leftmost image shows the content with the crop area shown with a dashed square, this is how the source rectangle is initially calculated. To its right is the result shown in the zoom view, the dashed square here represents the destination rectangle that now covers the entire zoom view.


The next step is to recalculate the rectangles so that the source rectangle fits inside of the content without changing the result shown on the screen. The left most image now shows the content with the zoom window completely inside the content. To its right is the result shown in the zoom view, the dashed square represents the destination rectangle that has now been recalculated so that it doesn't cover the parts of the view where nothing should be drawn.

[java]
@Override
protected void onDraw(Canvas canvas) {
if (mBitmap != null && mState != null) {
final int viewWidth = getWidth();
final int viewHeight = getHeight();
final int bitmapWidth = mBitmap.getWidth();
final int bitmapHeight = mBitmap.getHeight();

final float panX = mState.getPanX();
final float panY = mState.getPanY();
final float zoomX = mState.getZoomX(mAspectQuotient) * viewWidth / bitmapWidth;
final float zoomY = mState.getZoomY(mAspectQuotient) * viewHeight / bitmapHeight;

// Setup source and destination rectangles
mRectSrc.left = (int)(panX * bitmapWidth – viewWidth / (zoomX * 2));
mRectSrc.top = (int)(panY * bitmapHeight – viewHeight / (zoomY * 2));
mRectSrc.right = (int)(mRectSrc.left + viewWidth / zoomX);
mRectSrc.bottom = (int)(mRectSrc.top + viewHeight / zoomY);
mRectDst.left = getLeft();
mRectDst.top = getTop();
mRectDst.right = getRight();
mRectDst.bottom = getBottom();

// Adjust source rectangle so that it fits within the source image.
if (mRectSrc.left < 0) {
mRectDst.left += -mRectSrc.left * zoomX;
mRectSrc.left = 0;
}
if (mRectSrc.right > bitmapWidth) {
mRectDst.right -= (mRectSrc.right – bitmapWidth) * zoomX;
mRectSrc.right = bitmapWidth;
}
if (mRectSrc.top < 0) {
mRectDst.top += -mRectSrc.top * zoomY;
mRectSrc.top = 0;
}
if (mRectSrc.bottom > bitmapHeight) {
mRectDst.bottom -= (mRectSrc.bottom – bitmapHeight) * zoomY;
mRectSrc.bottom = bitmapHeight;
}

canvas.drawBitmap(mBitmap, mRectSrc, mRectDst, mPaint);
}
}
[/java]
So great, we have our view and state classes working, now let’s make a simple OnTouchListener so we can get to play around with the zoom as soon as possible! The listener will change the zoom or pan values of the state when the user touches the screen. For simplicity we’ll either zoom or pan when touch starts, which to do will be determined by an attribute we’ll call control type. Other than that we need to store the coordinates of the previous touch event so we know how much the touch has moved between each move event we receive.

For the zoom value we use the distance moved in the y-dimension, using this as the exponent when calculating the exponentiation of of a well chosen number, and then multiply the result with the current zoom level. Multiplying with an exponentiation is practical since if we where to for example move 10 pixels in the y-dimension, doing so gives the same result if it’s obtained by one or several move events. This is because, where we get the same result when moving a + b pixels as we get if we first move a pixels and then b pixels

For the pan we will simply add the distance moved in to their respective pan values, making the pan move relative to the touch movement. Finally we call notifyObservers() on the state in order to make the view redraw it’s content.

[java]
public class SimpleZoomListener implements View.OnTouchListener {

public enum ControlType {
PAN, ZOOM
}

private ControlType mControlType = ControlType.ZOOM;

private ZoomState mState;

private float mX;
private float mY;

public void setZoomState(ZoomState state) {
mState = state;
}

public void setControlType(ControlType controlType) {
mControlType = controlType;
}

public boolean onTouch(View v, MotionEvent event) {
final int action = event.getAction();
final float x = event.getX();
final float y = event.getY();

switch (action) {
case MotionEvent.ACTION_DOWN:
mX = x;
mY = y;
break;

case MotionEvent.ACTION_MOVE: {
final float dx = (x – mX) / v.getWidth();
final float dy = (y – mY) / v.getHeight();

if (mControlType == ControlType.ZOOM) {
mState.setZoom(mState.getZoom() * (float)Math.pow(20, -dy));
mState.notifyObservers();
} else {
mState.setPanX(mState.getPanX() – dx);
mState.setPanY(mState.getPanY() – dy);
mState.notifyObservers();
}

mX = x;
mY = y;
break;
}

}

return true;
}

}
[/java]
Alright, so we’re almost done. Now we just need an Activity and a layout to go with this and we’re done, the layout is quite straight forward, we simply add our new ZoomView and tell it to fill it’s parent.

[xml]
<?xml version=”1.0″ encoding=”utf-8″?>
<com.sonyericsson.zoom.ImageZoomView
xmlns:android=”http://schemas.android.com/apk/res/android”
android:id=”@+id/zoomview”
android:layout_width=”fill_parent”
android:layout_height=”fill_parent”
/>
[/xml]
In the activity we set up our zoom components in the onCreate() method, creating a new zoom state, decoding the bitmap we want to zoom in, creating the listener and connecting it to the state, and finally setting up the view. We also implement the onDestroy method and make sure to recycle bitmaps and remove listeners.

Finally we implement onCreateOptionsMenu() and onOptionsItemSelected() respectively enabling the user to change input method using the options menu and to reset the zoom to its original state.

[java]
public class TutorialZoomActivity1 extends Activity {

private static final int MENU_ID_ZOOM = 0;
private static final int MENU_ID_PAN = 1;
private static final int MENU_ID_RESET = 2;

private ImageZoomView mZoomView;
private ZoomState mZoomState;
private Bitmap mBitmap;
private SimpleZoomListener mZoomListener;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.main);

mZoomState = new ZoomState();

mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image800x600);

mZoomListener = new SimpleZoomListener();
mZoomListener.setZoomState(mZoomState);

mZoomView = (ImageZoomView)findViewById(R.id.zoomview);
mZoomView.setZoomState(mZoomState);
mZoomView.setImage(mBitmap);
mZoomView.setOnTouchListener(mZoomListener);

resetZoomState();
}

@Override
protected void onDestroy() {
super.onDestroy();

mBitmap.recycle();
mZoomView.setOnTouchListener(null);
mZoomState.deleteObservers();
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(Menu.NONE, MENU_ID_ZOOM, 0, R.string.menu_zoom);
menu.add(Menu.NONE, MENU_ID_PAN, 1, R.string.menu_pan);
menu.add(Menu.NONE, MENU_ID_RESET, 2, R.string.menu_reset);
return super.onCreateOptionsMenu(menu);
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_ID_ZOOM:
mZoomListener.setControlType(ControlType.ZOOM);
break;

case MENU_ID_PAN:
mZoomListener.setControlType(ControlType.PAN);
break;

case MENU_ID_RESET:
resetZoomState();
break;
}

return super.onOptionsItemSelected(item);
}

private void resetZoomState() {
mZoomState.setPanX(0.5f);
mZoomState.setPanY(0.5f);
mZoomState.setZoom(1f);
mZoomState.notifyObservers();
}
}
[/java]
And voila, we’re done! You can now start to play around with the zoom control on your own. It’s basic but works perfectly well for zooming around in images. A few things will probably seem to be lacking at the moment: first panning around doesn’t follow the finger correctly, and there is no limits or snap to functionality to keep us within reasonable bounds. These things, and others, will be the topic of the next parts of this zoom tutorial.

Class diagram showing how the zoom components interact

If you want to learn more about the zoom I suggest playing around with the code a bit, try changing how to zoom is controlled by the listener and try zooming into your own images.

Good luck!

[Download] One Finger Zoom sample project (211kb)

Sort by

  • By Brad Laue
    18th May 2010.
    17:08

    Are we working on Froyo for the Xperia phones yet

    Thumb up 1 Thumb down 0

  • By Manish
    18th May 2010.
    18:00

    Great, wasn’t i waiting for this! :D
    Thank you so much. Will let you know when i try it!
    And i guess, i’ll be loving your Tutorial App as well.
    P.S.
    Waiting for the Kinetic scrolling with Bounce effect Tutorial. :)

    Thumb up 2 Thumb down 0

  • By Manish
    18th May 2010.
    18:09

    Each one of them is a classic, i love them!
    At first, i was a bit confused on how would i handle the flicking of images if any event simply zooms in and out?
    But, then next samples were good enough to find me the answer.
    It’s so much better to zoom like that. The dynamic behavior is really cool. :)
    M going to try it now!
    Thank you so much so much!

    Thumb up 1 Thumb down 0

    • By Andreas Agvard
      18th May 2010.
      18:32

      Thanks, I’m glad you like them! The next part in this series as well as the first part of the list tutorial will be released soon.

      Thumb up 1 Thumb down 0

  • By anders
    18th May 2010.
    18:53

    Hi, what does this mean? will we be able to get one finger zoom on x10 also?

    http://blogs.sonyericsson.com/developerworld/2010/05/18/android-one-finger-zoom-tutorial-part-1/

    Thumb up 1 Thumb down 0

  • By Brad Laue
    18th May 2010.
    19:45

    Cause that’s much more important…

    Thumb up 1 Thumb down 1

  • By Manish
    18th May 2010.
    21:38

    @ Andreas Agvard
    Thanks for the reply.
    I’ve complied my views on the “One Finger Zooming”, and m pretty sure now, that it beats pinch zooming by quite a margin. :)
    Check out my Views on it and how does it compare to Pinch zooming as well.
    http://gadgets.apnafundaz.com/2010/05/one-finger-zoom-vs-multi-touch-zoom-in-xperia-x10-video-download-no-multitouch/

    Thumb up 2 Thumb down 1

  • By Gavriel
    19th May 2010.
    06:52

    Hey Im really new with android and the x10 being my first android phone I am excited to learn how to create my own apps/tweaks. The problem is that I am not a developer so I know very little in regards to what I am doing. Is it possible to post a little more detailed instruction? Or could you point me in the right direction for a tutorial that will help me out? I’m stuck right now on where to put my zoom, x and y values in the zoomstate code. And just out of curiosity once I have done this I will have one finger zooming enabled in my akbums for pics right? Lastly, thank you for this blog. It is very interesting and I really enjoy whats to come! :)

    Thumb up 1 Thumb down 1

  • By Revoklat
    19th May 2010.
    11:08

    @Manish:
    Yes indeed, the long hold will handle the start of zooming.
    But still, when will we be able to flick? I guess we should zoom out completely first, otherwise we are panning?

    This one finger zoom is obviously a better solution than pinch to zoom, just because you don’t need two fingers (hands) and you don’t need to hold your device in some strange position..

    Thumb up 1 Thumb down 1

  • By Andreas Agvard
    19th May 2010.
    13:38

    @Manish
    Nice and well written review! It’s fun to read your opinions, made me smile :)
    Thanks!

    Thumb up 1 Thumb down 1

  • By Andreas Agvard
    19th May 2010.
    15:41

    This tutorial is for developers that wish to make their own application with zooming capabilities, such as a gallery application but it could be any application really.

    It’s aimed at developers that know the basics of android and wish to learn more advanced things. This is intentional since there already exist lots of good resources on Android basics on the web. If you are new to Android and wish to learn more I suggest you head over to http://developer.android.com/resources and look into the Tutorials section there. It’s a great starting point for learning Android application development.

    It does not change the behavior of existing applications, such as getting one finger zoom in the album application on your phone. Sorry for the confusion, I hope this clears things up.

    Thumb up 1 Thumb down 1

1 2 3 4