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.
Tested with Android 2.0 and 2.0.1 platforms on Droid handset (firmware: Android 2.0.1).
Problems/Questions: Just ask...
Difficulty: 1.5 of 5
What it looks like:
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
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.
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:
While we are here you can also add the following if you wish to debug on an actual device (this also goes inside the application tag):
In the activity tag, set the screen orientation to landscape:
3.) Now we need an activity and a custom view:
We will create a custom view and add it to our activity so that we can handle our own drawing and touch events. The standard way of doing this is:
There are numerous examples of this type of custom view creation in the Android SDK if you'd like more information.
In onCreate() the only additional code we need is for getting our display width and height. We can use the system service getDefaultDisplay() API to do this:
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:
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.
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:
We need to do one more thing before we are ready to draw. We must check our coordinates to make sure no part of our scroll rectangle will be off the bitmap:
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.
- 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.
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.
Hope this helps!