Large Image Scrolling Using Low Level Touch Events

Basic Tutorials concerning: GUI, Views, Activites, XML, Layouts, Intents, ...

Large Image Scrolling Using Low Level Touch Events

Postby XCaffeinated » Mon Feb 15, 2010 1:20 am

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).

:?: Problems/Questions: Just ask...

Difficulty: 1.5 of 5 :)

What it looks like:
Image


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:
Syntax: [ Download ] [ Hide ]
Using xml Syntax Highlighting
  1. 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):
Syntax: [ Download ] [ Hide ]
Using xml Syntax Highlighting
  1. android:debuggable="true"
Parsed in 0.000 seconds, using GeSHi 1.0.8.4

In the activity tag, set the screen orientation to landscape:
Syntax: [ Download ] [ Hide ]
Using xml Syntax Highlighting
  1. 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:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. public class LargeImageScroller extends Activity {
  2.  
  3.         /** Called when the activity is first created. */
  4.         @Override
  5.         public void onCreate(Bundle savedInstanceState) {
  6.                 super.onCreate(savedInstanceState);
  7.  
  8.                 setContentView(new SampleView(this));
  9.         }
  10.        
  11.         private static class SampleView extends View {
  12.  
  13.                 public SampleView(Context context) {
  14.                         super(context);
  15.                 }
  16.                
  17.                 @Override
  18.                 public boolean onTouchEvent(MotionEvent event) {
  19.                 }
  20.  
  21.                 @Override
  22.                 protected void onDraw(Canvas canvas) {
  23.                 }
  24.         }
  25. }
Parsed in 0.032 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:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. Display display = ((WindowManager)
  2.                         getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
  3.  
  4. displayWidth = display.getWidth();             
  5. displayHeight = display.getHeight();
Parsed in 0.031 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.
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. displayRect = new Rect(0, 0, displayWidth, displayHeight);
  2. scrollRect = new Rect(0, 0, displayWidth, displayHeight);
  3.  
  4. bmLargeImage = BitmapFactory.decodeResource(getResources(),
  5.                                 R.drawable.testlargeimg);
Parsed in 0.034 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.
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. @Override
  2. public boolean onTouchEvent(MotionEvent event) {
  3.  
  4.         switch (event.getAction()) {
  5.                 case MotionEvent.ACTION_DOWN:
  6.                         startX = event.getRawX();
  7.                         startY = event.getRawY();
  8.                         break;
  9.  
  10.                 case MotionEvent.ACTION_MOVE:
  11.                         float x = event.getRawX();
  12.                         float y = event.getRawY();
  13.                         scrollByX = x - startX;
  14.                         scrollByY = y - startY;
  15.                         startX = x;
  16.                         startY = y;
  17.                         invalidate();
  18.                         break;
  19.                 }
  20.                 return true;
  21.         }
  22. }
Parsed in 0.037 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:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. int newScrollRectX = scrollRectX - (int)scrollByX;
  2. int newScrollRectY = scrollRectY - (int)scrollByY;
Parsed in 0.036 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:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. // Don't scroll off the left or right edges of the bitmap.
  2. if (newScrollRectX < 0)
  3.         newScrollRectX = 0;
  4. else if (newScrollRectX > (bmLargeImage.getWidth() - displayWidth))
  5.         newScrollRectX = (bmLargeImage.getWidth() - displayWidth);
  6.  
  7. // Don't scroll off the top or bottom edges of the bitmap.
  8. if (newScrollRectY < 0)
  9.         newScrollRectY = 0;
  10. else if (newScrollRectY > (bmLargeImage.getHeight() - displayHeight))
  11.         newScrollRectY = (bmLargeImage.getHeight() - displayHeight);
Parsed in 0.038 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:
Image

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:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. scrollRect.set(newScrollRectX, newScrollRectY, newScrollRectX + displayWidth,
  2.                                         newScrollRectY + displayHeight);
  3. Paint paint = new Paint();
  4. canvas.drawBitmap(bmLargeImage, scrollRect, displayRect, paint);
Parsed in 0.039 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:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. scrollRectX = newScrollRectX;
  2. scrollRectY = newScrollRectY;
Parsed in 0.036 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.
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.

"/src/your_package_structure/LargeImageScroller.java"
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. package com.example.largeimagescroller;
  2.  
  3. import android.app.Activity;
  4. import android.os.Bundle;
  5. import android.content.Context;
  6. import android.graphics.Bitmap;
  7. import android.graphics.BitmapFactory;
  8. import android.graphics.Canvas;
  9. import android.graphics.Paint;
  10. import android.graphics.Rect;
  11. import android.view.Display;
  12. import android.view.MotionEvent;
  13. import android.view.View;
  14. import android.view.WindowManager;
  15.  
  16. public class LargeImageScroller extends Activity {
  17.  
  18.         // Physical display width and height.
  19.         private static int displayWidth = 0;
  20.         private static int displayHeight = 0;
  21.  
  22.         /** Called when the activity is first created. */
  23.         @Override
  24.         public void onCreate(Bundle savedInstanceState) {
  25.                 super.onCreate(savedInstanceState);
  26.  
  27.                 // displayWidth and displayHeight will change depending on screen
  28.                 // orientation. To get these dynamically, we should hook onSizeChanged().
  29.                 // This simple example uses only landscape mode, so it's ok to get them
  30.                 // once on startup and use those values throughout.
  31.                 Display display = ((WindowManager)
  32.                         getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
  33.                 displayWidth = display.getWidth();             
  34.                 displayHeight = display.getHeight();    
  35.  
  36.                 // SampleView constructor must be constructed last as it needs the
  37.                 // displayWidth and displayHeight we just got.
  38.                 setContentView(new SampleView(this));
  39.         }
  40.        
  41.         private static class SampleView extends View {
  42.                 private static Bitmap bmLargeImage; //bitmap large enough to be scrolled
  43.                 private static Rect displayRect = null; //rect we display to
  44.                 private Rect scrollRect = null; //rect we scroll over our bitmap with
  45.                 private int scrollRectX = 0; //current left location of scroll rect
  46.                 private int scrollRectY = 0; //current top location of scroll rect
  47.                 private float scrollByX = 0; //x amount to scroll by
  48.                 private float scrollByY = 0; //y amount to scroll by
  49.                 private float startX = 0; //track x from one ACTION_MOVE to the next
  50.                 private float startY = 0; //track y from one ACTION_MOVE to the next
  51.  
  52.                 public SampleView(Context context) {
  53.                         super(context);
  54.  
  55.                         // Destination rect for our main canvas draw. It never changes.
  56.                         displayRect = new Rect(0, 0, displayWidth, displayHeight);
  57.                         // Scroll rect: this will be used to 'scroll around' over the
  58.                         // bitmap in memory. Initialize as above.
  59.                         scrollRect = new Rect(0, 0, displayWidth, displayHeight);
  60.  
  61.                         // Load a large bitmap into an offscreen area of memory.
  62.                         bmLargeImage = BitmapFactory.decodeResource(getResources(),
  63.                                 R.drawable.testlargeimg);
  64.                 }
  65.                
  66.                 @Override
  67.                 public boolean onTouchEvent(MotionEvent event) {
  68.  
  69.                         switch (event.getAction()) {
  70.                                 case MotionEvent.ACTION_DOWN:
  71.                                         // Remember our initial down event location.
  72.                                         startX = event.getRawX();
  73.                                         startY = event.getRawY();
  74.                                         break;
  75.  
  76.                                 case MotionEvent.ACTION_MOVE:
  77.                                         float x = event.getRawX();
  78.                                         float y = event.getRawY();
  79.                                         // Calculate move update. This will happen many times
  80.                                         // during the course of a single movement gesture.
  81.                                         scrollByX = x - startX; //move update x increment
  82.                                         scrollByY = y - startY; //move update y increment
  83.                                         startX = x; //reset initial values to latest
  84.                                         startY = y;
  85.                                         invalidate(); //force a redraw
  86.                                         break;
  87.                         }
  88.                         return true; //done with this event so consume it
  89.                 }
  90.  
  91.                 @Override
  92.                 protected void onDraw(Canvas canvas) {
  93.  
  94.                         // Our move updates are calculated in ACTION_MOVE in the opposite direction
  95.                         // from how we want to move the scroll rect. Think of this as dragging to
  96.                         // the left being the same as sliding the scroll rect to the right.
  97.                         int newScrollRectX = scrollRectX - (int)scrollByX;
  98.                         int newScrollRectY = scrollRectY - (int)scrollByY;
  99.  
  100.                         // Don't scroll off the left or right edges of the bitmap.
  101.                         if (newScrollRectX < 0)
  102.                                 newScrollRectX = 0;
  103.                         else if (newScrollRectX > (bmLargeImage.getWidth() - displayWidth))
  104.                                 newScrollRectX = (bmLargeImage.getWidth() - displayWidth);
  105.  
  106.                         // Don't scroll off the top or bottom edges of the bitmap.
  107.                         if (newScrollRectY < 0)
  108.                                 newScrollRectY = 0;
  109.                         else if (newScrollRectY > (bmLargeImage.getHeight() - displayHeight))
  110.                                 newScrollRectY = (bmLargeImage.getHeight() - displayHeight);
  111.  
  112.                         // We have our updated scroll rect coordinates, set them and draw.
  113.                         scrollRect.set(newScrollRectX, newScrollRectY,
  114.                                 newScrollRectX + displayWidth, newScrollRectY + displayHeight);
  115.                         Paint paint = new Paint();
  116.                         canvas.drawBitmap(bmLargeImage, scrollRect, displayRect, paint);
  117.  
  118.                         // Reset current scroll coordinates to reflect the latest updates,
  119.                         // so we can repeat this update process.
  120.                         scrollRectX = newScrollRectX;
  121.                         scrollRectY = newScrollRectY;
  122.                 }
  123.         }
  124. }
Parsed in 0.053 seconds, using GeSHi 1.0.8.4

Hope this helps!
XCaf
Last edited by XCaffeinated on Fri Feb 19, 2010 9:59 am, edited 1 time in total.
XCaffeinated
Developer
Developer
 
Posts: 25
Joined: Sun Nov 29, 2009 10:16 pm

Top

Postby cousinHub » Fri Feb 19, 2010 8:17 am

thanks, very nice explanation.

H.
User avatar
cousinHub
Junior Developer
Junior Developer
 
Posts: 10
Joined: Sun Oct 04, 2009 7:26 am

Postby XCaffeinated » Fri Feb 19, 2010 9:46 am

Glad you found it useful CousinHub! :)

-x
XCaffeinated
Developer
Developer
 
Posts: 25
Joined: Sun Nov 29, 2009 10:16 pm

inertia effect

Postby M4RC0 » Wed Mar 10, 2010 9:57 pm

great tutorial (as usual XCaffeinated)

now im trying to get some inertia effect on drag stop (MotionEvent.ACTION_UP), like google maps one, you know, the classic drag effect...

the approach i planned is to calculate the diference between startDragPoint and the end one (ACTION_UP event point), so higher values should fire more inertia.

i'd also been looking for some implementation of drag inertia but didnt found (i dont know the usual expresion for inertia in english to google it :P)

is it hard to you to implement this inertia behaviour?

thanks a lot
M4RC0
Junior Developer
Junior Developer
 
Posts: 12
Joined: Sun Feb 28, 2010 10:53 pm

Postby XCaffeinated » Wed Mar 10, 2010 11:28 pm

Hi M4RCO,

Thanks for the kind words.

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:

http://android.opensourceror.org/2010/01/18/android-source/

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.

-XCaffeinated
Attachments
GestureDetector.java
(21.12 KiB) Downloaded 735 times
XCaffeinated
Developer
Developer
 
Posts: 25
Joined: Sun Nov 29, 2009 10:16 pm

Postby M4RC0 » Thu Mar 11, 2010 9:25 am

thanks for a fast and perfect answer to a non so perfect question :P

i'm going to try the simpliest implementation and, if u want, i'll send you tha patch for your tutorial.

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 :P).

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...

one more time, thanks a lot XCaffeinated
M4RC0
Junior Developer
Junior Developer
 
Posts: 12
Joined: Sun Feb 28, 2010 10:53 pm

Top

Postby pskink » Thu Mar 11, 2010 10:49 am

XCaffeinated wrote:Hi M4RCO,

Thanks for the kind words.

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.


android already has class that implements inertia effect - it's Scroller.

in your onFling() method just call Scroller's method fling() and in your onDraw you can check Scroller's x & y values to update your fling
pskink
pskink
Master Developer
Master Developer
 
Posts: 719
Joined: Mon Nov 24, 2008 3:49 pm

Postby XCaffeinated » Thu Mar 11, 2010 7:47 pm

@pskink:
Absolutely correct! Thanks for pointing it out. I should've mentioned it.

@M4RCO:
Regarding the statusbar/titlebar: for this tutorial I've specifically removed it by using:
Syntax: [ Download ] [ Hide ]
Using xml Syntax Highlighting
  1. android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
Parsed in 0.000 seconds, using GeSHi 1.0.8.4

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:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. // Our view dimensions change depending on, among other things, screen
  2. // orientation. onSizeChanged() is a notification that such a change has
  3. // occurred. For our purposes, we can use the newly changed values to set up
  4. // our scroll and display rectangles. This is how we handle a user switch
  5. // between portrait and landscape modes, or any other type of 'view has
  6. // changed' operation.
  7. @Override
  8. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  9.                        
  10.         // Cache our new dimensions; we'll need them for drawing.
  11.         viewWidth = w;
  12.         viewHeight = h;
  13.                        
  14.         // Destination rect for our main canvas draw.
  15.         displayRect = new Rect(0, 0, viewWidth, viewHeight);
  16.         // Scroll rect: this will be used to 'scroll around' over the
  17.         // bitmap in memory. Initialize as above.
  18.         scrollRect = new Rect(0, 0, viewWidth, viewHeight);
  19.  
  20.         super.onSizeChanged(w, h, oldw, oldh);
  21. }
Parsed in 0.038 seconds, using GeSHi 1.0.8.4


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:

Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. /** Called when the activity is first created. */
  2. @Override
  3. public void onCreate(Bundle savedInstanceState) {
  4.         super.onCreate(savedInstanceState);
  5.  
  6.         setContentView(new SampleView(this));
  7. }
Parsed in 0.037 seconds, using GeSHi 1.0.8.4

and:

Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. public SampleView(Context context) {
  2.         super(context);
  3.  
  4.         // Load a large bitmap into an offscreen area of memory.
  5.         bmLargeImage = BitmapFactory.decodeResource(getResources(),
  6.                             R.drawable.testlargeimg);
  7. }
Parsed in 0.037 seconds, using GeSHi 1.0.8.4


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.

If you'd like the Eclipse project, just PM me.

Hope this helps!
XCaffeinated
Attachments
ViewChangedTest.java
(4.53 KiB) Downloaded 456 times
XCaffeinated
Developer
Developer
 
Posts: 25
Joined: Sun Nov 29, 2009 10:16 pm

Postby M4RC0 » Fri Mar 12, 2010 12:12 am

thanks a lot by the explanation

the tittlebar fix works fine for me, but when changing orientation, phone shows a OutOfMemory error caused by bitmap size (O_o)... (2000x2500, but on portrait works fine, OOM on landscape)

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 :P
i will need more doc reading...

anyway, i'll be trying it tomorrow... thanks you all
M4RC0
Junior Developer
Junior Developer
 
Posts: 12
Joined: Sun Feb 28, 2010 10:53 pm

Postby XCaffeinated » Fri Mar 12, 2010 1:10 am

Hi M4RCO,

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:

Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. protected void onPause() {
  2.          super.onPause();
  3.  
  4.         if (bmLargeImage != null)
  5.         {
  6.                 bmLargeImage.recycle();
  7.                 bmLargeImage = null;
  8.         }
  9. }
Parsed in 0.037 seconds, using GeSHi 1.0.8.4


Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. protected void onResume(){
  2.          super.onResume();
  3.  
  4.          if (bmLargeImage == null)
  5.                  bmLargeImage = BitmapFactory.decodeResource(getResources(),
  6.                                  R.drawable.balboapark);
  7. }
Parsed in 0.037 seconds, using GeSHi 1.0.8.4


Add the above to your activity. Then change:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. // Load a large bitmap into an offscreen area of memory.
  2. bmLargeImage = BitmapFactory.decodeResource(getResources(), R.drawable.balboapark);
Parsed in 0.037 seconds, using GeSHi 1.0.8.4
to:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. // Load a large bitmap into an offscreen area of memory.
  2. if (bmLargeImage == null)
  3.                bmLargeImage = BitmapFactory.decodeResource(getResources(), R.drawable.balboapark);
Parsed in 0.038 seconds, using GeSHi 1.0.8.4

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 :wink:

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.).

XCaffeinated
XCaffeinated
Developer
Developer
 
Posts: 25
Joined: Sun Nov 29, 2009 10:16 pm

Postby pskink » Fri Mar 12, 2010 9:20 am

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 :P
i will need more doc reading...

anyway, i'll be trying it tomorrow... thanks you all


using Scroller in order to use inertia fling is quite easy

assume you draw your bitmap by calling:

Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. canvas.drawBitmap(mBitmap, mBitmapX, mBitmapY, null);
  2.  
Parsed in 0.037 seconds, using GeSHi 1.0.8.4


you must limit your mBitmapX/mBitmapY to:
minX <= mBitmapX <= maxX
minY <= mBitmapY <= maxY

where:
minX = view.getWidth() - mBitmap.getWidth()
minY = view.getHeight() - mBitmap.getHeight()
maxX = 0
maxY = 0

and now:

1) in your onFling (OnGestureListener) you start scrolling by:

Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
  2.  
Parsed in 0.037 seconds, using GeSHi 1.0.8.4


where:
startX is mBitmapX
startY is mBitmapY
velocityX is taken from onFling velocityX param (not sure if negated)
velocityY is taken from onFling velocityY param (not sure if negated)
minX, maxX, minY, maxY are computed above

(i'm not sure here if velocityX/velocityY should be negated - you have to check it out)

2) in your onDraw method:

Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. if (mScroller.computeScrollOffset()) {
  2.         mBitmapX = mScroller.getCurrX();
  3.         mBitmapY = mScroller.getCurrY();
  4. }
  5.  
  6. // draw bitmap here
  7. canvas.drawBitmap(mBitmap, mBitmapX, mBitmapY, null);
  8.  
Parsed in 0.037 seconds, using GeSHi 1.0.8.4



that's it!
pskink
pskink
Master Developer
Master Developer
 
Posts: 719
Joined: Mon Nov 24, 2008 3:49 pm

Postby M4RC0 » Fri Mar 12, 2010 9:49 am

thaks... i wll try the reload of the bitmap on lifecycle refresh and test the performance,

about the scroller, it's possible me to have a mistake about the app sequence, I assumed that i will need something like this (pseudocode):

Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1.  
  2. velocity = mouserUpEvent - mouseDouwEvent;
  3.  
  4. while(velocity>0){
  5.  
  6.     bitmap.setX(velocityExpressionForX);
  7.  
  8.     bitmap.setY(velocityExpressionForY);
  9.  
  10.     decreaseVelocity;
  11.  
  12. }
  13.  
  14.  
Parsed in 0.037 seconds, using GeSHi 1.0.8.4


but i didn't see any loop, should i assume that onDraw method is called constantlly? definitly i should read a bit more about android lifecycle...

sorry about that, really i have not experience developing graphics/phisics behaviours, my skills are related on conectivity, databases and those so interesting things :P

thanks in advance
M4RC0
Junior Developer
Junior Developer
 
Posts: 12
Joined: Sun Feb 28, 2010 10:53 pm

Postby pskink » Fri Mar 12, 2010 11:00 am

M4RC0 wrote:but i didn't see any loop, should i assume that onDraw method is called constantlly? definitly i should read a bit more about android lifecycle...

sorry about that, really i have not experience developing graphics/phisics behaviours, my skills are related on conectivity, databases and those so interesting things :P

thanks in advance


you are absolutely right, i forgot about invaldation...

so no loop, rather onDraw should re-invaldate

see attached Scroll.java - you can scroll (down & move) and fling (down & while moving up) as well - it uses gradiend instead of bitmap but its trivial to change to bitmap
Attachments
Scroll.java
(2.98 KiB) Downloaded 421 times
pskink
pskink
Master Developer
Master Developer
 
Posts: 719
Joined: Mon Nov 24, 2008 3:49 pm

Postby pskink » Fri Mar 12, 2010 7:55 pm

pskink wrote:see attached Scroll.java - you can scroll (down & move) and fling (down & while moving up) as well - it uses gradiend instead of bitmap but its trivial to change to bitmap


well, just another couple of dozens of lines of java and here comes the zooming ! (drag, swipe & double tup)

XCoffeinated, you are damn good in tutorials, dont want to write about this?
Attachments
Scroll.java
(5.3 KiB) Downloaded 674 times
pskink
pskink
Master Developer
Master Developer
 
Posts: 719
Joined: Mon Nov 24, 2008 3:49 pm

Postby XCaffeinated » Sat Mar 13, 2010 9:17 am

Hi PSkink,

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 :).

Regards,
Sharon
XCaffeinated
Developer
Developer
 
Posts: 25
Joined: Sun Nov 29, 2009 10:16 pm

Top
Next

Return to Novice Tutorials

Who is online

Users browsing this forum: No registered users and 9 guests