Large Image Scrolling Using Low Level Touch Events
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).
Difficulty: 1.5 of 5
What it looks like:

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
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:
Using xml Syntax Highlighting
- android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
Parsed in 0.000 seconds, using GeSHi 1.0.8.4
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):
Using xml Syntax Highlighting
- android:debuggable="true"
Parsed in 0.000 seconds, using GeSHi 1.0.8.4
In the activity tag, set the screen orientation to landscape:
Using xml Syntax Highlighting
- android:screenOrientation="landscape"
Parsed in 0.000 seconds, using GeSHi 1.0.8.4
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:
Using java Syntax Highlighting
- public class LargeImageScroller extends Activity {
- /** Called when the activity is first created. */
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(new SampleView(this));
- }
- private static class SampleView extends View {
- public SampleView(Context context) {
- super(context);
- }
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- }
- @Override
- protected void onDraw(Canvas canvas) {
- }
- }
- }
Parsed in 0.031 seconds, using GeSHi 1.0.8.4
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:
Using java Syntax Highlighting
- Display display = ((WindowManager)
- getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
- displayWidth = display.getWidth();
- displayHeight = display.getHeight();
Parsed in 0.030 seconds, using GeSHi 1.0.8.4
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.
Using java Syntax Highlighting
- displayRect = new Rect(0, 0, displayWidth, displayHeight);
- scrollRect = new Rect(0, 0, displayWidth, displayHeight);
- bmLargeImage = BitmapFactory.decodeResource(getResources(),
- R.drawable.testlargeimg);
Parsed in 0.033 seconds, using GeSHi 1.0.8.4
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.
Using java Syntax Highlighting
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- startX = event.getRawX();
- startY = event.getRawY();
- break;
- case MotionEvent.ACTION_MOVE:
- float x = event.getRawX();
- float y = event.getRawY();
- scrollByX = x - startX;
- scrollByY = y - startY;
- startX = x;
- startY = y;
- invalidate();
- break;
- }
- return true;
- }
- }
Parsed in 0.036 seconds, using GeSHi 1.0.8.4
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:
Using java Syntax Highlighting
- int newScrollRectX = scrollRectX - (int)scrollByX;
- int newScrollRectY = scrollRectY - (int)scrollByY;
Parsed in 0.035 seconds, using GeSHi 1.0.8.4
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:
Using java Syntax Highlighting
- // Don't scroll off the left or right edges of the bitmap.
- if (newScrollRectX < 0)
- newScrollRectX = 0;
- else if (newScrollRectX > (bmLargeImage.getWidth() - displayWidth))
- newScrollRectX = (bmLargeImage.getWidth() - displayWidth);
- // Don't scroll off the top or bottom edges of the bitmap.
- if (newScrollRectY < 0)
- newScrollRectY = 0;
- else if (newScrollRectY > (bmLargeImage.getHeight() - displayHeight))
- newScrollRectY = (bmLargeImage.getHeight() - displayHeight);
Parsed in 0.037 seconds, using GeSHi 1.0.8.4
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:
Using java Syntax Highlighting
- scrollRect.set(newScrollRectX, newScrollRectY, newScrollRectX + displayWidth,
- newScrollRectY + displayHeight);
- Paint paint = new Paint();
- canvas.drawBitmap(bmLargeImage, scrollRect, displayRect, paint);
Parsed in 0.035 seconds, using GeSHi 1.0.8.4
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:
Using java Syntax Highlighting
- scrollRectX = newScrollRectX;
- scrollRectY = newScrollRectY;
Parsed in 0.035 seconds, using GeSHi 1.0.8.4
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.
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.
"/src/your_package_structure/LargeImageScroller.java"
Using java Syntax Highlighting
- package com.example.largeimagescroller;
- import android.app.Activity;
- import android.os.Bundle;
- import android.content.Context;
- import android.graphics.Bitmap;
- import android.graphics.BitmapFactory;
- import android.graphics.Canvas;
- import android.graphics.Paint;
- import android.graphics.Rect;
- import android.view.Display;
- import android.view.MotionEvent;
- import android.view.View;
- import android.view.WindowManager;
- public class LargeImageScroller extends Activity {
- // Physical display width and height.
- private static int displayWidth = 0;
- private static int displayHeight = 0;
- /** Called when the activity is first created. */
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- // displayWidth and displayHeight will change depending on screen
- // orientation. To get these dynamically, we should hook onSizeChanged().
- // This simple example uses only landscape mode, so it's ok to get them
- // once on startup and use those values throughout.
- Display display = ((WindowManager)
- getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
- displayWidth = display.getWidth();
- displayHeight = display.getHeight();
- // SampleView constructor must be constructed last as it needs the
- // displayWidth and displayHeight we just got.
- setContentView(new SampleView(this));
- }
- private static class SampleView extends View {
- private static Bitmap bmLargeImage; //bitmap large enough to be scrolled
- private static Rect displayRect = null; //rect we display to
- private Rect scrollRect = null; //rect we scroll over our bitmap with
- private int scrollRectX = 0; //current left location of scroll rect
- private int scrollRectY = 0; //current top location of scroll rect
- private float scrollByX = 0; //x amount to scroll by
- private float scrollByY = 0; //y amount to scroll by
- private float startX = 0; //track x from one ACTION_MOVE to the next
- private float startY = 0; //track y from one ACTION_MOVE to the next
- public SampleView(Context context) {
- super(context);
- // Destination rect for our main canvas draw. It never changes.
- displayRect = new Rect(0, 0, displayWidth, displayHeight);
- // Scroll rect: this will be used to 'scroll around' over the
- // bitmap in memory. Initialize as above.
- scrollRect = new Rect(0, 0, displayWidth, displayHeight);
- // Load a large bitmap into an offscreen area of memory.
- bmLargeImage = BitmapFactory.decodeResource(getResources(),
- R.drawable.testlargeimg);
- }
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- // Remember our initial down event location.
- startX = event.getRawX();
- startY = event.getRawY();
- break;
- case MotionEvent.ACTION_MOVE:
- float x = event.getRawX();
- float y = event.getRawY();
- // Calculate move update. This will happen many times
- // during the course of a single movement gesture.
- scrollByX = x - startX; //move update x increment
- scrollByY = y - startY; //move update y increment
- startX = x; //reset initial values to latest
- startY = y;
- invalidate(); //force a redraw
- break;
- }
- return true; //done with this event so consume it
- }
- @Override
- protected void onDraw(Canvas canvas) {
- // Our move updates are calculated in ACTION_MOVE in the opposite direction
- // from how we want to move the scroll rect. Think of this as dragging to
- // the left being the same as sliding the scroll rect to the right.
- int newScrollRectX = scrollRectX - (int)scrollByX;
- int newScrollRectY = scrollRectY - (int)scrollByY;
- // Don't scroll off the left or right edges of the bitmap.
- if (newScrollRectX < 0)
- newScrollRectX = 0;
- else if (newScrollRectX > (bmLargeImage.getWidth() - displayWidth))
- newScrollRectX = (bmLargeImage.getWidth() - displayWidth);
- // Don't scroll off the top or bottom edges of the bitmap.
- if (newScrollRectY < 0)
- newScrollRectY = 0;
- else if (newScrollRectY > (bmLargeImage.getHeight() - displayHeight))
- newScrollRectY = (bmLargeImage.getHeight() - displayHeight);
- // We have our updated scroll rect coordinates, set them and draw.
- scrollRect.set(newScrollRectX, newScrollRectY,
- newScrollRectX + displayWidth, newScrollRectY + displayHeight);
- Paint paint = new Paint();
- canvas.drawBitmap(bmLargeImage, scrollRect, displayRect, paint);
- // Reset current scroll coordinates to reflect the latest updates,
- // so we can repeat this update process.
- scrollRectX = newScrollRectX;
- scrollRectY = newScrollRectY;
- }
- }
- }
Parsed in 0.053 seconds, using GeSHi 1.0.8.4
Hope this helps!
XCaf

)
), 'successive frames' and 'velocity'. 
