I wrote an Activity which shows only one custom view.
The view is simple, draw a random color and invalidate a smaller region, and draw a random color, and invalidate a even smaller region, and so on...
The expected result should be like this. It works well by using software render, and getClipBounds() returns the region I just passed into invalidate. But when hardware acceleration is enabled, the entire view is always redrawn by a new color, and getClipBounds() return the entire view's region.
I know there are some posts like this and this. Answers said that getClipBounds() returns the entire view's region with hardware acceleration but only the ones intersecting the dirty region will be redrawn.
Is there anything wrong or my misunderstanding?
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
// random color
int color = Color.rgb((int) (Math.random() * 255),
(int) (Math.random() * 255), (int) (Math.random() * 255));
canvas.drawColor(color);
canvas.getClipBounds(rect);
// smaller dirty region
invalidate(0, 0, rect.width() - 1, rect.height() - 1);
}
Unfortunately this is a limitation of hardware acceleration in Android, at least as of Android 5. The invalidate rect is ignored and the entire view is always redrawn, requiring you to draw the entire area. If you attempt to draw only part of the view, anything else that was drawn in the view before will disappear.
I've read posts where it is claimed that Android does not re-render the whole view but only the part that changed, but this seems wrong because when I try to only render the area that was in the rect that I passed to invalidate, the rest of the view disappears. If it was true that Android only re-rendered the changed area, then I would expect the rest of the custom drawing in the view to stay visible.
iOS has much better performance with it's drawRect method and setNeedsDisplayInRect. I would have expected Android to work the same way, but alas it does not.
Related
Android View has three versions of invalidate(): one that invalidates the whole view, and two that invalidate only a portion of it. But it only has one onDraw(), which draws the entire canvas. There must be some use that the system makes of the hint that I only want to invalidate part of the view, but I'm unclear on what it is.
I have a view that does custom drawing in onDraw(). Do I have a way to find out which parts of the canvas are invalid, so I only draw those?
When Android gets ready to render changes to the screen, it does so by creating a union of all of the individual rectangle areas of the screen that need to be redrawn (all the regions that have been invalidated.)
When your view's onDraw(Canvas canvas) method is called, you can check to see if the Canvas has a clip bounds.
If there is a non-empty clip bounds, you can use this information to determine what you will and won't need to draw thus saving time.
If the clip bounds is empty, you should assume Android wants you to draw the entire area of your view.
Something like this:
private Rect clipBounds = new Rect();
#Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
boolean isClipped = canvas.getClipBounds(clipBounds);
// If isClipped == false, assume you have to draw everything
// If isClipped == true, check to see if the thing you are going to draw is within clipBounds, else don't draw it
}
There must be some use that the system makes of the hint that I only want to
invalidate part of the view, but I'm unclear on what it is.
Yes, it is. The method invalidate (int l, int t, int r, int b) has four parameters which are used by the View's parent View to calculate the mLocalDirtyRect which is a filed of the View class. And the mLocalDirtyRect is used by the getHardwareLayer() method in the View class, here is its description:
/**
* <p>Returns a hardware layer that can be used to draw this view again
* without executing its draw method.</p>
*
* #return A HardwareLayer ready to render, or null if an error occurred.
*/
HardwareLayer getHardwareLayer() {
Means that Android can refresh part of your view without call the View's onDraw() method. So you don't need to try drawing part of the View yourself, because Android will do it for you when you tell it the dirty part of your View.
Finally, I think you can refer to the source code of View and ViewGroup for more details, here is the link that you can read them online:
https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/view/View.java
https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/view/ViewGroup.java
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.
I have a problem that I can not really solve. I have created a graphing app which will contain data with alot of data points. To enable scrolling in the graph i create a bitmap of the graph data, and the shift / move the bitmap to the right or left of the screen depending on the user input. The bitmap is always the graph view height and graph view width. When the user moves the bitmap, i shift the bitmap with:
memoryCanvas.drawBitmap(memoryBitmap, bitmapShift, 0.0f, paint);
where shift is a a float value containing the shifting values.
Now, on most devices i have tried this is, it works very nice. I have tried it on HTC Desire, Galaxy Tab and Galaxy S. When testing this on my own Galaxy S however, i get strange results that i can not explain. Do note that my Galaxy S contains a custom rom (with android 4.0.4), so that is probably the reason why i get this behavior, but i still can not understand it or properly mitigate it.
So in a normal use case behavior, the bitmaps get shifted by bitmapShift number of pixels, and then i fill in the empty space by normal line drawing. But when using my phone, dragging the bitmap either direction slowly, so the bitmapShift values are around 0.5, the bitmap does not move, or only moves sometimes. I have compared the bitmapShift on the other platforms and they are in the same range, 0.5 when dragging slowly. This behavior does of course screw up my drawings a lot. This does also happen when doing fast dragging, but its most visible when doing it slowly.
I can not really figure out what causes this behavior. The bitmap does not move according to my bitmapShift value. It does on all other platforms i have tried. If i skip using bitmaps, and only draw lines according to the shifting values, everything works fine.
Does anyone have any idea on what could cause this behavior? Im kinda running out after sitting some days trying to figure it out. The critical code is below. This code is in my onDraw function.
memoryCanvas.setBitmap(emptyBitmap); //First set another bitmap to clear
memoryCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); //Clear it
memoryCanvas.drawBitmap(memoryBitmap, bitmapShift, 0.0f, paint); //Draw shifted bitmap on it
memoryCanvas.drawLines(lineDrawPoints, 0 , cnt, paint); //Draw remaining lines
memoryCanvas.setBitmap(memoryBitmap); //Set the original
memoryCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); //Clear original
memoryCanvas.drawBitmap(emptyBitmap, 0.0f, 0.0f, paint); //Draw the final image
canvas.drawBitmap(memoryBitmap, 0, 0.0f, paint); //And finally draw on real canvas
Any help, tips, suggestions are very welcome.
Cheers
Wilhelm
When there is only a simple transform set on Canvas (a simple transform = translate only, no rotate, no scale), Skia, Android's 2D rendering library, aligns bitmaps to the pixel grid. This means that a move by less than 1 pixel might not be visible at all. A silly workaround is to set a very, very small scale or rotate transform on Canvas before drawing your bitmap. This has the side effect of not snapping bitmaps to the pixel grid.
I think I should just add a new API on Paint to let apps do subpixel positioning of bitmaps no matter what transform is set.
I am experimenting with 2D graphics in Android during my off time, and I'm intrigued by the idea of creating a sprite based "sandbox" for playing around. I've looked around online and found some good tutorials, but I'm stuck with a fairly basic problem: My sprite moves faster than the terrain and "glides" over it. Also, he slowly outpaces the terrain scrolling and moves to the edge of the view.
I've got an animated sprite that I want to move around the world. I do so by changing his absolute coordinates(setting X and Y position in an 'update()' function and applying a scalar multiple to speed up or slow down the rate at which he's moving around.
this.setXPos(currX + (speed * dx2));
this.setYPos(currY + (speed * dy2));
Underneath that sprite, I'm drawing "terrain" from a bitmap. I want to continue to move that sprite around via the coordinate accessors above, but also move my view and scroll the terrain so that the sprite stays in the same relative screen location but moves through the "world". I have an imperfect implementation where player is my sprite and field is my terrain:
#Override
protected void onDraw(Canvas canvas)
{
player.updateLocation(GameValues.speedScale);
field.update(new Point(player.getXPos(), player.getYPos()));
field.draw(canvas);
player.draw(canvas);
//more stuff here...
}
And field.update() looks like(Warning: Hard-coded scariness):
public void update(Point pt)
{
sourceRect.left = pt.x - 240;
sourceRect.right = pt.x + 240;
sourceRect.top = pt.y - 400;
sourceRect.bottom = pt.y + 400;
}
The thinking there was that I would eventually just get screen dimensions and make it less 'hack-y', but get something together quickly. This could easily be where my issue is coming from. Of immediate interest is field.draw():
#Override
public void draw(Canvas canvas)
{
try
{
canvas.drawBitmap(fieldSheet, sourceRect, destRect, null);
}
catch(Exception e)
{
//Handle Exception...
}
}
You'll notice I'm using the overload of drawBitmap() that accepts a source and destination Rect and that the field.update() method moves that sourceRect to match the movement of the sprite. How can I keep the "camera (so to speak)" centered on the sprite and scroll the terrain appropriately? I had thought that moving just the sourceRect and maintaining a constant destRect would do so, but now I'm thinking I have to move the destRect around the "world" with the sprite, while maintaining the same dimensions.
Is there a better (read functional) way of making this work? I'm coming here somewhat shamefully, since I think this should be somewhat easier than it seems to be for me. Any and all help or suggestions are appreciated. Thanks!
*Edit:*Does it make sense to also move the destination Rect at the same speed as the sprite? I think I conflated the source and destination Rect's and left the destRect immobile. I'll update with the results after I get a chance to try it (maybe during lunch).
*Edit_2:*Changing the destination rectangle didn't get me there, but using the View.scrollBy(x, y) method gets me close to totally satisfied. The remaining question to be satisfied is how to "clip" the View scrolling to a rectangle that represents the "field". I believe that the View.getLeft() and View.getTop() functions, offset by the screen width and height, can be used to specify a Rect that can be virtually moved around within the constraints of the "world" and block further deltas from being argued to the View.scrollBy() method. The reason I look toward this approach is because the View doesn't seem to be positioned in absolute space, and a View.getLeft() call, even after a View.scrollBy(x, y) where x > 0, returns a 0.
I've seen a few people ask how to zoom an entire ViewGroup (such as a RelativeLayout) in one go. At the moment this is something I need to achieve. The most obvious approach, to me, would be to hold the zoom scale factor as a single variable somewhere; and in each of the child Views' onDraw() methods, that scale factor would be applied to the Canvas prior to graphics being drawn.
However, before doing that, I have tried to be clever (ha - usually a bad idea) and extend RelativeLayout, into a class called ZoomableRelativeLayout. My idea is that any scale transformation could be applied just once to the Canvas in the overridden dispatchDraw() function, so that there would be absolutely no need to separately apply the zoom in any of the child views.
Here's what I did in my ZoomableRelativeLayout. It's just a simple extension of RelativeLayout, with dispatchDraw() being overridden:
protected void dispatchDraw(Canvas canvas){
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.scale(mScaleFactor, mScaleFactor);
super.dispatchDraw(canvas);
canvas.restore();
}
The mScaleFactor is manipulated by a ScaleListener in the same class.
It does actually work. I can pinch to zoom the ZoomableRelativeLayout, and all of the views held within properly rescale together.
Except there's a problem. Some of those child views are animated, and hence I periodically call invalidate() on them. When the scale is 1, those child views are seen to redraw periodically perfectly fine. When the scale is other than 1, those animated child views are only seen to update in a portion of their viewing area - or not at all - depending on the zoom scale.
My initial thinking was that when an individual child view's invalidate() is being called, then it's possibly being redrawn individually by the system, rather than being passed a Canvas from the parent RelativeLayout's dispatchDraw(), meaning that the child view ends up refreshing itself without the zoom scale applied. But oddly, the elements of the child views that are redrawn on the screen are to the correct zoom scale. It's almost as if the area that the system decides to actually update in the backing bitmap remains unscaled - if that makes sense. To put it another way, if I have a single animated child View and I gradually zoom in further and further from an initial scale of 1, and if we place an imaginary box on the area where that child view is when the zoom scale is 1, then the calls to invalidate() only cause a refresh of the graphics in that imaginary box. But the graphics that are seen to update are being done to the right scale. If you zoom in so far that the child view has now moved completely away from where it was with a scale of 1, then no part of it at all is seen to refresh. I'll give another example: imagine my child view is a ball that animates by switching between yellow and red. If I zoom in a little bit such that the ball moves to the right and down, at a certain point you'll just see the top-left quarter of the ball animate colours.
If I continuously zoom in and out, I see the child views animate properly and entirely. This is because the entire ViewGroup is being redrawn.
I hope this makes sense; I've tried to explain as best as I can. Am I on a bit of a loser with my zoomable ViewGroup strategy? Is there another way?
Thanks,
Trev
If you are applying a scale factor to the drawing of your children, you also need to apply the appropriate scale factor to all of the other interactions with them -- dispatching touch events, invalidates, etc.
So in addition to dispatchDraw(), you will need to override and appropriate adjust the behavior of at least these other methods. To take care of invalidates, you will need to override this method to adjust the child coordinates appropriately:
http://developer.android.com/reference/android/view/ViewGroup.html#invalidateChildInParent(int[], android.graphics.Rect)
If you want the user to be able to interact with the child views you will also need to override this to adjust touch coordinates appropriately before they are dispatched to the children:
http://developer.android.com/reference/android/view/ViewGroup.html#dispatchTouchEvent(android.view.MotionEvent)
Also I would strongly recommend you implement this all inside of a simple ViewGroup subclass that has a single child view it manages. This will get rid of any complexity of behavior that RelativeLayout is introducing in its own ViewGroup, simplifying what you need to deal with and debug in your own code. Put the RelativeLayout as a child of your special zooming ViewGroup.
Finally, one improvement to your code -- in dispatchDraw() you want to save the canvas state after applying the scaling factor. This ensures that the child can't modify the transformation you have set.
The excellent answer from hackbod has reminded me that I need to post up the solution that I eventually came to. Please note that this solution, which worked for me for the application I was doing at the time, could be further improved with hackbod's suggestions. In particular I didn't need to handle touch events, and until reading hackbod's post it did not occur to me that if I did then I would need to scale those as well.
To recap, for my application I what I needed to achieve was to have a large diagram (specifically, the floor layout of a building) with other small "marker" symbols superimposed upon it. The background diagram and foreground symbols are all drawn using vector graphics (that is, Path() and Paint() objects applied to Canvas in the onDraw() method). The reason for wanting to create all the graphics this way, as opposed to just using bitmap resources, is because the graphics are converted at run-time using my SVG image converter.
The requirement was that the diagram and associated marker symbols would all be children of a ViewGroup, and could all be pinch-zoomed together.
A lot of the code looks messy (it was a rush job for a demonstration) so rather than just copying it all in, instead I'll try to just explain how I did it with the relevant bits of code quoted.
First of all, I have a ZoomableRelativeLayout.
public class ZoomableRelativeLayout extends RelativeLayout { ...
This class includes listener classes that extend ScaleGestureDetector and SimpleGestureListener so that the layout can be panned and zoomed. Of particular interest here is the scale gesture listener, which sets a scale factor variable and then calls invalidate() and requestLayout(). I'm not strictly certain at the moment if invalidate() is necessary, but anyway - here it is:
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector){
mScaleFactor *= detector.getScaleFactor();
// Apply limits to the zoom scale factor:
mScaleFactor = Math.max(0.6f, Math.min(mScaleFactor, 1.5f);
invalidate();
requestLayout();
return true;
}
}
The next thing I had to do in my ZoomableRelativeLayout was to override onLayout(). To do this I found it useful to look at other people's attempts at a zoomable layout, and also I found it very useful to look at the original Android source code for RelativeLayout. My overridden method copies much of what's in RelativeLayout's onLayout() but with some modifications.
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
int count = getChildCount();
for(int i=0;i<count;i++){
View child = getChildAt(i);
if(child.getVisibility()!=GONE){
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams)child.getLayoutParams();
child.layout(
(int)(params.leftMargin * mScaleFactor),
(int)(params.topMargin * mScaleFactor),
(int)((params.leftMargin + child.getMeasuredWidth()) * mScaleFactor),
(int)((params.topMargin + child.getMeasuredHeight()) * mScaleFactor)
);
}
}
}
What's significant here is that when calling 'layout()' on all the children, I'm applying the scale factor to the layout parameters as well for those children. This is one step towards solving the clipping problem, and also it importantly correctly sets the x,y position of the children relative to each other for different scale factors.
A further key thing is that I am no longer attempting to scale the Canvas in dispatchDraw(). Instead each child View scales its Canvas after obtaining the scale factor from the parent ZoomableRelativeLayout via a getter method.
Next, I shall move onto what I had to do within the child Views of my ZoomableRelativeLayout. There's only one type of View I contain as children in my ZoomableRelativeLayout; it's a View for drawing SVG graphics that I call SVGView. Of course the SVG stuff is not relevant here. Here's its onMeasure() method:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
float parentScale = ((FloorPlanLayout)getParent()).getScaleFactor();
int chosenWidth, chosenHeight;
if( parentScale > 1.0f){
chosenWidth = (int) ( parentScale * (float)svgImage.getDocumentWidth() );
chosenHeight = (int) ( parentScale * (float)svgImage.getDocumentHeight() );
}
else{
chosenWidth = (int) ( (float)svgImage.getDocumentWidth() );
chosenHeight = (int) ( (float)svgImage.getDocumentHeight() );
}
setMeasuredDimension(chosenWidth, chosenHeight);
}
And the onDraw():
#Override
protected void onDraw(Canvas canvas){
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.scale(((FloorPlanLayout)getParent()).getScaleFactor(),
((FloorPlanLayout)getParent()).getScaleFactor());
if( null==bm || bm.isRecycled() ){
bm = Bitmap.createBitmap(
getMeasuredWidth(),
getMeasuredHeight(),
Bitmap.Config.ARGB_8888);
... Canvas draw operations go here ...
}
Paint drawPaint = new Paint();
drawPaint.setAntiAlias(true);
drawPaint.setFilterBitmap(true);
// Check again that bm isn't null, because sometimes we seem to get
// android.graphics.Canvas.throwIfRecycled exception sometimes even though bitmap should
// have been drawn above. I'm guessing at the moment that this *might* happen when zooming into
// the house layout quite far and the system decides not to draw anything to the bitmap because
// a particular child View is out of viewing / clipping bounds - not sure.
if( bm != null){
canvas.drawBitmap(bm, 0f, 0f, drawPaint );
}
canvas.restore();
}
Again - as a disclaimer, there are probably some warts in what I have posted there and I am yet to carefully go through hackbod's suggestions and incorporate them. I intend to come back and edit this further. In the meantime, I hope it can start to provide useful pointers to others on how to implement a zoomable ViewGroup.