Android zoomable/scrollable ImageView with overlays - android

I would like to display an image (including scroll and zoom functionality).
Additionally, I need to add some overlays to the image dynamically. The overlays are in a relationship to the content within the image.
If the user scrolls, the overlays should move too
If the user zooms,
the overlays should not be zoomed but hold their relative position to
the image
In other words: I'd like to implement a Map with some markers, but I provide my own map material.
My main question is: How could I do this?
What I have tried so far
I tried to implement it within a WebView as it provided the basic zooming and scaling functionality. However, I did not succeed in adding overlays without using Javascript and HTML (what I thought was not a very convenient way of solving the problem)
There are projects (i.e. TouchImageView) which are implementing the scroll/zoom functionality but they make it even harder to add own overlay functionality from my point of view.
Questions
Is there a simple solution or approach to my problem?
What is the right way of adding and moving views at runtime in Android?
(Usually, I would use a basic layout and play with the margin - would that be best practice?)
Could somebody point me to the right direction, please.
Min. API Level: 10

I would suggest extending the View class and overriding onDraw method.
Take a look at the Canvas API.
You'll probably have an onDraw method that looks like :
#Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
canvas.translate(mPosX, mPosY); //position of your actual data
canvas.scale(mScaleFactor, mScaleFactor); // zoom factor
map.draw(canvas); //you draw your map here
canvas.restore(); //go back to saved state
//for each marker you have, do the same "save - translate - scale - draw" loop
}
Since you don't need your markers to be zoomable, you'll probably wont need scale step but you need to calculate their position properly.
Depending on your # of objects etc, you may need to have a more optimized way of drawing your markers (e.g. re-drawing only changed rectangular areas). see View.invalidate(Rect)

I Think this one can help you
Refer this project Link
it Has a Automatic Scrolling of Image and Zoom a Image When you click On it.

Have you tried subclassing View and handling yourself user gestures? It is my first choice for complex behaviours as such. It should look something like this:
In your class extending View you should instantiate the required image and overlays at creation time as Bitmap objects
OnScroll event you will calculate how the image and overlays chould be after such scroll, refresh the Bitmaps and invalidate the current view
Using a GestureDetector, you can handle pinch events and treat zoom in/zoom out events just as scroll events
Remember to always call invalidate after changing your Bitmaps
Draw the bitmaps during the onDraw method
Plenty of material about this individual tasks on StackOverflow, let me know if you need adittional help in any of these.

Checkout this example project https://github.com/jzafrilla/AndroidImageHostpot

After a long time, I found the exact answer to my solution: TileView
Setting up the map view:
#Override
protected void onCreate( Bundle savedInstanceState ) {
super.onCreate( savedInstanceState );
TileView tileView = new TileView( this );
tileView.setSize( 2000, 3000 ); // the original size of the untiled image
tileView.addDetailLevel( 1f, "tile-%d-%d.png");
setContentView( tileView );
}
Adding the marker:
tileView.addMarker( someView, 250, 500, -0.5f, -1.0f );
Code from the documentation.

Related

How to draw over small portions of custom view?

I'm using a custom View to draw rectangles(which will have text inside them at a certain point) and I want to highlight each rectangle when selected, which will happen every few seconds when user selects a rectangle. Should I implement highlighted rectangle in onDraw or is there a way just to redraw each rectangle without redrawing the whole View? I was thinking of using "invalidate(rect)" but it's been deprecated.
I'm trying to be considerate of the cost of invalidating the whole View compared to just redrawing a rectangle.
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for(CellCoordinates cellCoordinate : mCoordinateCells) {
canvas.drawText(" ", cellCoordinate.getRect().exactCenterX(), cellCoordinate.getRect().exactCenterY(), cellPaint);
}
}
Using invalidate() and onDraw() is fine. Dirty rect is not really have effect on API21+
... In API 21 the given rectangle is ignored entirely in favor of an internally-calculated area instead. ...
Dirty rect is deprecated because of different drawing model in hardware accelerated views. Checkout this link for more information
Also, it seems your rectangles can be implemented as custom Drawables with states (selected and normal). It will not give you extra performance, but might help to divide and structure code for drawing. This might help

Simultaneously move six points in circular paths around their own orbital centers

Suppose I have six points drawn in a Canvas along the circumference of a circle, in the pattern of a hexagon.
I want to simultaneously move and re-draw each of these six points in a small circular path - for example, each point moves along the circumference of a circle with 1/10th the radius of the larger circle.
I'm a little lost on how to do this with Canvas and onDraw and I don't know if better solutions exist. How would you do this?
Some answers have at least pointed me toward this which shows how a point might move along a circular path, but I don't know how to implement it for this situation:
for (double t = 0; t < 2*Pi; t += 0.01)
{
x = R*cos(t) + x_0;
y = R*sin(t) + y_0;
}
Thank you!
Here's one approach:
Start with a custom view that extends View and overrides onDraw. You are on the right track with using the drawing functions of Canvas.
Give your custom view a field to hold the current angle:
private float mTheta;
Then add a method like:
public void setTheta(float radians) {
mTheta = radians;
invalidate();
}
Then in onDraw, use the current value of mTheta to calculate the position of your points.
Of course, with the custom view you might need to handle sizing with an onMeasure override and possibly some layout with an onLayout override. If you set the dimensions of the view to an absolute dp value, the default behavior should work for you.
Doing it this way will set you up to override onTouch and allow user interaction to move the graphics, or use a ValueAnimator to cycle from 0 to 2π calling setTheta from within your AnimatorUpdateListener.
Put some code together and when you get stuck, post another question. If you add a comment to this answer with a link to the new question, I'll take a look at it.

Am I invalidating the entire screen on every call?

I am new to Android Development and reading the book Hello Android. It uses a Sudoku example, and the code that I am referring to is here.
In this , onTouchScreen, it calls select method, that calls invalidate twice. The question is that, on invalidating is the onDraw method called right after that? So will in this case, inside my select method, it will do
invalidate
call onDraw
Do some stuff
invalidate
call onDraw
Is this how it will happen, also, will the entire screen be regenerated? All the numbers and hints etc., because from the book the author says
In an earlier version of this example, I invalidated the entire
screen whenever the cursor was moved. Thus, on every key
press, the whole puzzle had to be redrawn. This caused it to lag
noticeably. Switching the code to invalidate only the smallest
rectangles that changed made it run much faster.
What exactly is he trying to say here?
Added Info
I added some logs in the onDraw method, some at the starting, some in the for loop. Whenever I touched a new rectangle, all the logs were called. Doesnt that mean that the entire screen is geting repopulated, since all the code in onDraw is reexecuted?
Kraken
Q: But what about the logs, surely if my loops are getting executed it means that all the canvas.draw will be getting executed too?
A: Yes, the whole drawing will be executed in your sample code. You have to optimize the rendering process by yourself, in onDraw method.
Q: How does the system know, what piece of code will "only" redraw the dirty area?
A: Canvas::getClipBounds will give you a dirty rect, which you should draw something on.
Inside your for loop in onDraw, compare the dirty rect with the rect which you want to draw. Then do continue if they do not intersect.
But remember, if you have several area set to dirty, the returned rect will be a union of all dirty areas.
Please see the following two questions below:
Getting the dirty region inside draw()
Android: invalidate(dirty)
Hope this will help you.
==========================
The author is right. But this still can be optimized.
Calling invalidate(Rect) will automatically set a clip area for the canvas. (That's why canvas.getClipBounds() can return that area).
Then, during onDraw(), anything drawing out of the clip area, will be ignored. They do not appear on the screen, so it REALLY reduce the time of drawing.
But ignoring them still costs overhead. Therefore, for graphical intensive app, onDraw() could be better optimized if you exclude them in advance.
You can find a great example for optimizing onDraw() in android's KeyboardView, which provide the view of your android's input method.
http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/1.5_r4/android/inputmethodservice/KeyboardView.java
This is directly from View documentation:
Drawing is handled by walking the tree and rendering each view that intersects the invalid region. Because the tree is traversed in-order, this means that parents will draw before (i.e., behind) their children, with siblings drawn in the order they appear in the tree. If you set a background drawable for a View, then the View will draw it for you before calling back to its onDraw() method.
Note that the framework will not draw views that are not in the invalid region.`
From what I understand, once your view is drawn for the first time, a tree if formed with parent and child objects along with their positions on screen. When you pass a designated area to invalidate, this tree is checked for effected nodes in that area and only those nodes will be called for draw.
Now what I also don't understand is that in this example, the only View is the PuzzleView. I'm not sure how a drawing a single view can be optimized. Check if it is discussed further in the text.
If it is not, then my theory would be that the canvas objects(rectangles) are also part of the above said tree and only those parts, i.e. the rectangles in the specified area are drawn.
More importantly, do you see any improvement after using area invalidate vs full invalidate?
Even if you call invalidate multiple times the onDraw method will only be called once. Basically the onDraw gets called inside the RunLoop method when a view has been invalidated. That means that if you invalidate the view multiple times before giving back the control to the runloop the view will be redrawn only once.
Notice that if you invalidate two different rects of the view the system will try to make an union of those rects before redrawing your view.
In the code, the invalidate that you are talking about is this:
invalidate(selRect);
?
If it is he only calls the onDraw of this selected rectangle selRect.
Only the invalidate(); redraws the hole screen.
Hope it helps.
On this example, you should notice that invalidate() calls have a Rect as parameter. This mean that only this zone of the view is getting dirty and is going to be redrawn by the system.
Calling invalidate() will not trigger the onDraw() method right after. The system only decide whenever he wants to redraw the view.
From Android documentation :
If the view is visible, onDraw(android.graphics.Canvas) will be called
at some point in the future.
Knowing that, inside the select method, this will probably happen :
1. Invalidate a small portion of the View
2. Do some stuff
3. Invalidate another small portion of the View
4. Theses 2 portions of the View are getting redrawn
Hope that helped.
As #jjxtra mentioned above
Invalidate with rect does not alter canvas clip bounds with hardware acceleration turned on. The entire view is always redrawn regardless of the rect passed to Invalidate.
In API 21 the given rectangle is ignored entirely in favor of an internally-calculated area instead. public void invalidate(int l, int t, int r, int b); and public void invalidate(Rect dirty); already marked as deprecated!
I solved the problem by specifing the subset of the bitmap to draw.
class MyView extends View {
private Bitmap mBitmap;
private Rect mBound = new Rect(0, 0, 300, 300); // 300x300 by default, invoke updateBound if in needed
...
private void updateBound(PointF pointF) {
if (mBound.left > (int)pointF.x) {
mBound.left = (int)pointF.x;
}
if (mBound.bottom < (int)pointF.y) {
mBound.bottom = (int)pointF.y;
}
if (mBound.top > (int)pointF.y) {
mBound.top = (int)pointF.y;
}
if (mBound.right < (int)pointF.x) {
mBound.right = (int)pointF.x;
}
}
#Override
public boolean onTouchEvent(MotionEvent event) {
...
invalidate();
return true;
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mBitmap, mBound, mBound, null);
}
}
By default I only draw in the area of the (0, 0, 300, 300). But you can update the bound if in needed, just invoke updateBound.

Large image with clickable sections

I've an large image in my android app, which represents a real pictured device.
Now I would like to know, whats the best way to do the following:
Mark some specific image sectors as clickable
Visual preview the click event (like a normal android button)
I've thought about something like the "imagemap" similar solution of
http://catchthecows.com/?p=113.
But it doesn't highlight the clicked section (neccessary for me).
It's not possible, or lets say very complicated, to create "n" different pictures which handle the highlighting effect (for example: section 1 clicked - so highlighting it, section 2 clicked - so higlighting it ....).
Any suggestions would be great.
Thanks a lot.
Btw:
My image looks like the following (the red sections should be clickable).
I had quick look at the ImageMap source you provided.
Here's the onDraw() method for ImageMap:
/**
* Paint the view
* image first, location decorations next, bubbles on top
*/
#Override
protected void onDraw(Canvas canvas) {
drawMap(canvas);
drawLocations(canvas);
drawBubbles(canvas);
}
So, as the comment states, the full image is drawn first, followed by the 'location decorations' (which would seem to be an area of particular interest), and finally the area 'bubbles', which basically seem to be tooltips for the selectable areas and might not be applicable to you.
Taking a look at the drawing method for the 'location decorations':
protected void drawLocations(Canvas canvas) {
for (Area a : mAreaList) {
a.onDraw(canvas);
}
}
And finally the draw method for the Area objects (which are the definable, selectable image areas):
// an onDraw is set up to provide an extensible way to
// decorate an area. When drawing remember to take the
// scaling and translation into account
public void onDraw(Canvas canvas) {
if (_decoration != null) {
float x = (getOriginX() * mResizeFactorX) + mScrollLeft - 17;
float y = (getOriginY() * mResizeFactorY) + mScrollTop - 17;
canvas.drawBitmap(_decoration, x, y, null);
}
}
I would say your best bet would be to use the ImageMap class to take care of most of the required functionality. You can use the supplied Area.onDraw() method to take care of the area 'highlighting'. Instead of drawing a 'decoration bitmap', use the shape information defined in the Area class or subclasses and use Canvas methods to draw a transparent highlight mask over the selected areas.
EDIT: You would need a seperate onDraw() implementation for each of the Area subclasses, as the area bounds information is stored in the subclasses.

Dynamically create / draw images to put in android view

I'm not sure I'm doing this the "right" way, so I'm open to other options as well. Here's what I'm trying to accomplish:
I want a view which contains a graph. The graph should be dynamically created by the app itself. The graph should be zoom-able, and will probably start out larger than the screen (800x600 or so)
I'm planning on starting out simple, just a scatter plot. Eventually, I want a scatter plot with a fit line and error bars with axis that stay on the screen while the graph is zoomed ... so that probably means three images overlaid with zoom functions tied together.
I've already built a view that can take a drawable, can use focused pinch-zoom and drag, can auto-scale images, can switch images dynamically, and takes images larger than the screen. Tying the images together shouldn't be an issue.
I can't, however, figure out how to dynamically draw simple images.
For instance: Do I get a BitMap object and draw on it pixel by pixel? I wanted to work with some of the ShapeDrawables, but it seems they can only draw a shape onto a canvas ... how then do I get a bitmap of all those shapes into my view? Or alternately, do I have to dynamically redraw /all/ of the image I want to portray in the "onDraw" routine of my view every time it moves or zooms?
I think the "perfect" solution would be to use the ShapeDrawable (or something like it to draw lines and label them) to draw the axis with the onDraw method of the view ... keep them current and at the right level ... then overlay a pre-produced image of the data points / fit curve / etc that can be zoomed and moved. That should be possible with white set to an alpha on the graph image.
PS: The graph image shouldn't actually /change/ while on the view. It's just zooming and being dragged. The axis will probably actually change with movement. So pre-producing the graph before (or immediately upon) entering the view would be optimal. But I've also noticed that scaling works really well with vector images ... which also sounds appropriate (rather than a bitmap?).
So I'm looking for some general guidance. Tried reading up on the BitMap, ShapeDrawable, Drawable, etc classes and just can't seem to find the right fit. That makes me think I'm barking up the wrong tree and someone with some more experience can point me in the right direction. Hopefully I didn't waste my time building the zoom-able view I put together yesterday :).
First off, it is never a waste of time writing code if you learned something from it. :-)
There is unfortunately still no support for drawing vector images in Android. So bitmap is what you get.
I think the bit you are missing is that you can create a Canvas any time you want to draw on a bitmap. You don't have to wait for onDraw to give you one.
So at some point (from onCreate, when data changes etc), create your own Bitmap of whatever size you want.
Here is some psuedo code (not tested)
Bitmap mGraph;
void init() {
// look at Bitmap.Config to determine config type
mGraph = new Bitmap(width, height, config);
Canvas c = new Canvas(mybits);
// use Canvas draw routines to draw your graph
}
// Then in onDraw you can draw to the on screen Canvas from your bitmap.
protected void onDraw(Canvas canvas) {
Rect dstRect = new Rect(0,0,viewWidth, viewHeight);
Rect sourceRect = new Rect();
// do something creative here to pick the source rect from your graph bitmap
// based on zoom and pan
sourceRect.set(10,10,100,100);
// draw to the screen
canvas.drawBitmap(mGraph, sourceRect, dstRect, graphPaint);
}
Hope that helps a bit.

Categories

Resources