What you learn: You will learn how to scroll (pan) images larger than the display surface of your device using touch gestures. These touch gestures will be processed by low level touch event handlers, resulting in a light-weight smooth scrolling implementation.

Description:Imagine a rectangle as a window through which we can see a portion of an image currently loaded into memory. This window is the same size as our display. We use touch events to move the window over the surface of the image. What we need to do is:

load a large image into memory

set up a scroll rectangle the same size as our display

use touch events to move our scroll rectangle over our image

draw the portion of the image currently within the scroll rectangle to the display

What if I need an image larger than memory will hold?This tutorial describes how to work with images that fit into memory. For larger images, you will need a solution that loads portions of an image, either via streaming/caching (from an external source such as a web server) or compression/decompression (local file store). Many examples exist on the web. If you are working with map data, consider MapView.

What about GestureDetector and Gesture Builder?GestureDetector is a good choice for handling different kinds of gestures like fling, long-press or double-tap. You can certainly use it in place of the material presented here. But there may be times (for whatever reason) that you can't use GestureDetector, or you may not need all the functionality it offers. Gesture Builder is for handling of more complex gestures and managing groups of gestures.

Implementation:The full source is available at the end.

One caveat before we get started: for simplicity I haven't handled activity lifecycle or bitmap recycling. You will run out of memory fairly quickly if you force a lifecycle refresh by, for example, opening and closing the keyboard.

0.) In Eclipse, create a new Android Project, targeting Android 2.0 (older versions may work too, but the folders may be slightly different from those shown here). For consistency with this tutorial you may wish to name your main activity LargeImageScroller, and make your package name com.example.largeimagescroller.

1.) Obtain an image resource:

The image resource I used is 1440x1080 - tested with the Droid handset - but you can use a smaller one if you want; it just has to be larger than the display size so you can scroll it. Be careful if you go too much larger, as you may run out of memory. If you are using a different device your memory requirements may vary, and you may need to adjust the size of the image accordingly.

(For testing purposes I tested this with a huge image – 3200x2300 – one I was sure would take up a lot of memory, just to make sure the scrolling was smooth, but this isn't something you'd normally want to do.)

Add the image resource (I've named mine testlargeimg.png) to your /drawable-hdpi folder (may also be named /drawable depending on which Android version you are using).

2.) For convenience, we will run our simple application fullscreen and in landscape mode. To do this modify the project's manifest:

Edit AndroidManifest.xml and add the following to the application tag:

You might think that getDefaultDisplay() will always return the same values for a given hardware device. Actually, the values will change depending on, for example, screen orientation. On my Droid in landscape mode I see a width of 854 and a height of 480, but in portrait mode these values are reversed.

If you have an application that needs to know when the display settings change, you can hook the onSizeChanged() API (see the Android docs for more). For our application, we are always in landscape mode so these values wont change after we retrieve them.

That's it for the activity. Everything else happens in our custom view.

4.) Set up our SampleView:

Constructor:In our SampleView constructor we handle the bitmap loading, the scroll rectangle setup and the rectangle that defines how large an area we draw onto (our display rectangle) – in our case, the whole screen.

We have initialized our scroll rectangle to be exactly the same coordinates as the display rectangle. When we first run our application this means the upper-leftmost portion of our image will be visible.

The bitmap loader code is one of many standard ways to load Android bitmap resources.

Touch event handler:Our touch event handler is where we process our touch gestures. A gesture is broken into a series of actions, the most common of which are down, move and up, though there are others (see the Android docs for MotionEvent for a complete reference). Information about an event is contained in MotionEvent. For our application we only care about down and move, as you will see.

When you first touch the display a single ACTION_DOWN event is generated. Thereafter as you move your finger you will generate a chain of ACTION_MOVE events. The number of ACTION_MOVE events generated over a given time period is controlled by the Android OS. When either an ACTION_DOWN or an ACTION_MOVE event occurs, you can retrieve the coordinates of the event's location, using getRawX() and getRawY(). This gives us a way to determine how far our finger has traveled. We store the coordinates of the ACTION_DOWN event in startX and startY.

Side note: getRawX() and getRawY() always return absolute screen coordinates. Another way to retrieve coordinates is with getX() and getY() but beware: depending on the event you call getX() and getY() with, they may return either absolute (relative to device screen) or relative (relative to view) coordinates. For more see the Android docs. We use getRawX() and getRawY() for this application.

We build small moves from consecutive ACTION_MOVE events and then apply these small moves to our scroll rectangle. This will occur several-to-many times a second and so will give the appearance of smooth scrolling. scrollByX and scrollByY keep incremental totals of the amount we need to scroll by the next time our view is redrawn. startX and startY are updated also, so that we can keep tracking these movements as increments. Given that the ACTION_MOVE event may get generated many times, it is best to keep the code that executes from within the event handler to a minimum. This is true for any event handler.

Invalidate() indicates to the Android OS that we'd like our view to be redrawn. Our redraw happens in onDraw() (discussed below), where we update the coordinates of the scroll rectangle and repaint the enclosed bitmap portion.

The return true at the end indicates that we've processed the touch event to our liking and have no more use for it, so we tell the Android OS not to process it further.

Draw updater:Our draw handler, onDraw(), is responsible for calculating the updated scroll rectangle coordinates and drawing the area of the bitmap within this newly updated rectangle.

When you slide your finger to the left, you can think of this as 'pulling' the bitmap towards the left, under the scroll rectangle – this is exactly the same as sliding the scroll rectangle to the right. So in our ACTION_MOVE event handler, if we calculate a move update that indicates that we are moving to the left, we need to update the scroll rectangle to move to the right. This means we need to add the negative sense of the move update to our current scroll rectangle coordinates:

The checks against 0 are straightforward: since our left (or top) coordinate is 0 for both the scroll rectangle and the bitmap, this is simply a check to make sure our scroll rectangle x (or y) coordinate is not to the left (or top) of the left (or top) edge of our bitmap.

To understand the check against the right edge, it is helpful to look at the following diagram:

Since we perform our scroll rectangle x coordinate check using the left x coordinate, then in order to perform a check that uses the right edge of the scroll rectangle, we have to take the scroll rectangle's width into account. This will allow us to check the right edge of the scroll rectangle against the right edge of the bitmap. In the example above, we have a bitmap width of 8 and a scroll rectangle width of 3, so we would have a left x coordinate of 5 for our scroll rectangle (= 8-3). y behaves similarly. In our case the scroll rectangle width is also our display width so this is how we end up with (bitmap width – display width) in the code fragment above. The same applies to the height variables.

The hard part is done. Set the newly calculated coordinates into our scroll rectangle, and draw it:

The last detail in our draw handler is to update the original scroll coordinates with the new ones, so we can start over with the same process as we continue to update our drawing in response to user move gestures:

5.) To build and run, you will need to add the variable declarations; see the full source at the end. When you run the example you should be able to smoothly scroll around your image.

One final note:We never create a new bitmap, once the original is loaded into memory. Continuously creating bitmaps on the fly will kill your performance. In particular, avoid creating bitmaps in response to ACTION_MOVE events. We simply redraw the correct portion of the already loaded bitmap, which is defined by the scroll rectangle's coordinates.

Takeaways:

The implementation described here is for simple scrolling needs, and for use with images that will fit into memory.

ACTION_DOWN and ACTION_MOVE events can be used to calculate scroll move updates; you will get several-to-many ACTION_MOVE events for one move gesture.

To avoid poor performance, try to keep the code that executes from within the event handlers to a minimum.

To avoid poor performance, don't create bitmaps on the fly (in this tutorial we only create one bitmap on startup).

If you need to handle different kinds of gestures (fling, long-press, double-tap, etc.) consider an alternative such as GestureDetector.

The Full Source:

You'll need a single large image as described in step 1, recommended size is 1440x1080, though if your device is other than a Droid, your mileage may vary.

You will also need to edit your AndroidManifest.xml as described in step 2.

In order to get an inertia effect, you will need velocity similar to what you get in the onFling() callback for GestureDetector, and so it may be easier for you to just use GestureDetector directly and do what you want to do in onFling(), or adapt the source code for your needs. Once you've added either double tap logic or fling logic to the code I've done in this tutorial, you are using most of what the Android OS implements anyway, and Android's version is much better tested than mine (probably more performant too), so I suggest starting there. I've attached the version of GestureDetector.java from Android 2.1, but you can get the entire source code blob from:

and the author has kindly packaged up all the relevant material into a single zip. Scroll about a third of the way down, and you will see it.

Having said that; if you would like to implement your own, it sounds like you are on the right track: calculate the velocity (see the example in the attached file in the MotionEvent.ACTION_UP handler), and implement a scale factor on scrollByX, scrollByY so that the higher the velocity the larger your scroll amounts are. (Actually, to be realistic you probably want a single scalar value and apply that to a vector created from the start point to the end point; this way you get normalized scrolling speeds. In other words if you gesture to the northeast you should not scroll any faster than if you gesture to the north, or to the east, but I wouldn't worry about that right at first.) To get the inertia effect you are looking for, you can decrease the scale factor a bit (you'll have to experiment) over successive onDraw() calls. This will give you the effect of scrolling a lot immediately after the fling gesture is over, and scrolling less and less over time, until you are not scrolling at all, which is the effect I think you are looking for. Good Google terms would be 'fling', 'onFling' (or Apple's version is 'swipe', but the math will be the same ), 'successive frames' and 'velocity'.

Incidentally, this is very similar to the effect we do in 3D games where we have (for example) a camera (think first person shooter here) that we don't want to come to a sudden stop, but rather gradually slow down over several frames. So you will find a lot of material searching for 'camera physics' for games.

next steps are to implement zoom in/out over the image (by ussing matrix object), i hope it shouldn't be a problem for my limited skills on android (lot of years on web java, 15 days on android ).

only 1 detail, the tutorial calculates the display height without considere the statusbar nor title bar heights, so the image is cropped on dragging down... i'm tryed to get these bar heights to calculate the real available area but don't know how...

In order to get an inertia effect, you will need velocity similar to what you get in the onFling() callback for GestureDetector, and so it may be easier for you to just use GestureDetector directly and do what you want to do in onFling(), or adapt the source code for your needs.

in AndroidManifest.xml, with the thinking that an app should be able to use all available screen real estate if it so desires.

However, if you'd like to have the statusbar/titlebar, and incidentally handle screen orientation changes, you can override onSizeChanged() in SampleView to get the actual view dimensions. I've attached a .java file to show you how to do this (compare with the source code from this tutorial). The meat is:

I've changed displayWidth and displayHeight to viewWidth and viewHeight to convey that we are sizing these now based on view. Once we have this, onCreate() and the SampleView constructor are much simplified:

Other than some name changes, that's all you need. Don't forget to update your AndroidManifest.xml to either show the titlebar/statusbar or not, and also you can remove the screenOrientation attribute now.

The attached .java file is this tutorial, with the onSizeChanged() changes I just described, and should work for you now with or without status/titlebar (however you set it in your AndroidManifest.xml) and for landscape/portrait mode switches.

Yup! That's a pretty large image. As I mentioned in my tutorial I am not handling activity lifecycle refreshes. Every time you change orientation you are actually forcing a lifecycle refresh. There's plenty of documentation at the main Android portals. But to get you past your current issue you can do the following quick and dirty thing:

wherever you are loading your bitmap (in the SampleView constructor if I recall).

This will force a reload of the bitmap resource every time a lifecycle refresh is triggered, which may or may not be acceptable in the long run, but it's better than OOM

You should definitely check out the docs on activity lifecycle so you'll know more about how to clean up/persist/restore resources properly and allow your application to gracefully handle being interrupted (incoming phone calls, screen orientation switch, power notifications, user switching apps, etc.).

M4RC0 wrote:about the fling, I spent all evening but i think my android UI knowledge is still insufficient, i dont know how to use GestureDetector on the sample and over de bitmap :Pi will need more doc reading...

Coming from you that is high praise indeed. For those who don't know, PSkink developed a Sliding Draw/Panel widget well before it was available in the public SDK, and his STILL has more features. Oh and he has about 800 posts scattered between here and Google dev... ^^.

My feelings regarding the tutorials is that once you start needing swipe and double tap, the solution that you implemented with your Scroll class is preferable to what I've shown here. For those who haven't looked at his code (attached in an earlier post), his Scroll class extends View and implements OnGestureListener, and OnDoubleTapListener. The tutorial is mainly for people who, for whatever reason, find that the callbacks, etc., are a performance bottleneck. From what I've seen on the web, people do need occasionally to resort to this lower level handling. (Hopefully, they've actually used a profiler to determine this, but...)

Once you start fiddling with the more complicated gestures, callback performance drops into the noise. Double taps are probably the worst since the underlying code has to wait long enough to determine that the gesture is indeed a double tap and not a single tap. This is why I suggest to people to try to get something like GestureDetector, or another preexisting class or component, to work first. (GestureDetector is nice in this regard because it ONLY does the doubletap check if you've explicity set it to). What you have done in your Scroll.java file is cleaner and more maintainable, and based on classes and callback interfaces that have been quite well tested, so I would recommend that people start with something that.

I guess that is a long-winded way of answering your question of why I didn't add fling or double tap to the tutorials . As for zoom, yes I should probably add that somewhere .