I have a scrollview that contains a custom view. The custom view is bigger than the area of the screen and draws properly.
The scrollview however tends to call onDraw() non-stop when scrolling, and I can't seem to make it smooth.
I used ScrollView.getDrawingRect() to calculate the visible portion of the screen and only draw to that, but it still returns the entire viewport (so it's optimized to not draw offscreen areas), and not the delta between the last position and the current one. Ideally I'd want to draw only the delta, and not the entire visible window.
If anyone can point me to more information about how to use the drawing caches, and if that will help optimize scrolling, I'd love to implement it, or any other possible solutions it would be greatly appreciated.
When content is scrolled, the entire viewport needs to be redrawn because all of the content has moved. I don't think there's anything that needs to be done to optimize a ScrollView - if scrolling is slow then it's the drawing method of your custom view that is too slow.
Try to avoid object creation in your draw methods which is usually the main culprit for poor drawing performance.
Edit: Also the scrollview could blit the old content up or down quickly that is still drawn on the screen, and then request a redraw of only the "new" portion of the screen. (only applies to opaque views).
I encountered the same problem. I solved it by using the function setDrawingCacheEnabled(true). By enabling this setting, your canvas view will be cached as bitmap, so you don't have to call canvas' draw method each time onDraw() is called.
In your custom view's constructor, you will need something like this:
public CustomView(Context context) {
setDrawingCacheEnabled(true);
drawnFlag = false;
}
In your onDraw method, you will need something like this:
public void onDraw(Canvas canvas) {
if (! drawnFlag) {
canvas.drawPath(...);
canvas.drawPath(...);
drawnFlag = true;
}
}
Now, scrolling on this custom view should be smooth since we only call the drawing methods once.
Afaik the ScrollView sets a proper clip rect on the canvas your view gets in onDraw so you only need to draw what's inside that rect.
You could also implement cache bitmaps based on the clip rect's size.
Related
Now I have a CustomView and its width is large, So I put it into a HorizontalScrollView. To make better performance, I just draw the visible rect of this view in its onDraw() method. Now I face a issue that when I scroll the view using my finger, how does the view know that its visible rect has been changed and refresh its content by calling postInvalidate().
Thanks a lot if someone can help me.
ScrollView should automatizovaly draw view when it scrolls into visible region. You dont need to call postInvalidate(). You should invalidate your view only when content of your view has changed and needs to be redrawn. Dont worry about scroll view. If you want to avoid executing unnecessary drawing code for stuff that is not visible, use quickReject methods of Canvas in your onDraw(). Or getClipBounds method of Canvas to find out what part of your view is currently visible.
I need update a small portion of a custom view in order to display a small animation. The rest portion of the view has only static image. The most straightforward would be to obtain the canvas of the view and update only that particular portion directly. But I can't really find anyway to get the view's canvas object outside of the view::onDraw method.
The only alternative I know is this: call view::invalidate() with a specified rectangle to minimize the drawing flicker. I have the code to update the entire view within onDraw. So the best thing to do is to detect the clipping rect and only run the code to update the specified area, in order to minimize CPU usage as well?
I guess I will try to answer this question myself to the best my knowledge so far.
There is no direct access to the canvas outside of the onDraw method.
Although we can detect the clipping rect with the function Canvas.getClipBounds(), the getClipBounds function always return the entire view area if GPU is enabled. When GPU is not used, getClipBounds() returns the actual dirty area. Since there is a GPU in most phones, it makes the function getClipBounds pretty much useless.
I have a custom view that extends ImageView and displays an image and a text header. The text is drawn via drawRect and drawText overriding onDraw.
The first image is the typical use case: The header is inside the canvas bounds.
I'd like to be able to draw the header outside the canvas bounds. If I just draw it outside the bounds, just like image 2, the result is correct, ie. the rectangle is drawn outside the canvas and the text too.
However, I wonder if this works by accident and what kind of problems I may expect. I suppose it's a bad thing to draw outside the canvas bounds, but it'd be very convenient for me because I won't need to further complicate this custom view, or wrap it inside a container, or create another view, etc... I've only tested it on a couple devices I own and it works...
Can anybody share some thoughts on why this is OK, it doesn't really matter, or is very wrong?
For those interested, I've found a legit way to do this:
set the bottom padding to the header size plus the blank space
draw normally, now everything's inside the canvas and the imageView is adjusted to the requested padding, leaving the bottom padding area free to draw my header :P
I know how to draw paths on a canvas and understand how to undo/redo. But now I want to draw shapes (lines, circles, rectangles) that dynamically resize depending on how I drag them - just like we have in MS Paint.
If I use "drawLine", the line is there permanently with no way of erasing it and redrawing it to my new finger location. Same with circle as I want the circle to constantly change width as I drag my finger. So the old one has to erased (keeping all the other paths on the bitmap intact) and the new one drawn in its place.
Been searching a lot for this, but haven't come across how t do it. Maybe I'm using the wrong keywords, but I don't know. Any pointers?
Each time you move the finger, call the underlying view's invalidate() function, it will trigger erasing the entire background
public void invalidate ()
Since: API Level 1
Invalidate the whole view. If the view is visible, onDraw(android.graphics.Canvas) will be called at some point in the future. This must be called from a UI thread. To call from a non-UI thread, call postInvalidate().
Then redraw your shape based on your finger's new position.
Managed to do it. I misunderstood the way the offscreen drawing thing worked. The idea is to write to the bitmap only after you have the shape you want - ie on "touch up". Till then, draw the shape using the "canvas" object in on Draw...
I have made my own canvas class which extends an imageView. My onDraw() method draws out the users gps position and I keep calling this onDraw method every time the user moves. My problem is I also want to draw out a gps trail which only needs to be drawn once (doesnt need to be updated when a user moves). I am wondering is it possible to have more than 1 onDraw method or is there any way of separating 1) the user location and 2) the gps trail??
My reason is I do not want to waste memory by redrawing the gps route everytime the users gps position changes. It is a waste.
Have you seen performance take a hit? If not, don't worry about it. I would think that this would be wasting CPU cycles if anything... not memory. So if the app seems fast enough already, don't worry about optimizing it.
If your app is a bit laggy, and you've found that the trail is the bottleneck... I would suggest caching it into a bitmap. This way, you will still have to draw the trail, but you will not have to calculate the coordinates of the trail on each frame.
I have had to solve a somewhat similar problem recently and I'll explain briefly what I did in case it's of any help.
What you can do is use multiple overlapping Views, where one may contain the background graphics that you don't want to redraw often, and a foreground View that contains the graphics that are frequently updated. Then, to gain performance, you can design the background View's onDraw() so that it is backed by a Bitmap that you then retain as a class variable. In the very first onDraw() of your background graphics, you do the relatively slow drawing to Canvas. In subsequent calls to onDraw(), you simply draw that Bitmap to Canvas.
I've just done this myself. Basically what my application does is display a number of graphical gauges. Those gauges have lots of graphics that are drawn just once (gauge face, number legends), and the pointer graphic that needs to be redrawn over and over as the model data changes. First of all, I split the background graphics and moving foreground graphics into separate overlapping Views. Now, invalidating and redrawing the foreground pointer graphic of course causes anything it overlaps to be invalidated too, so the onDraw() method for the background graphics View is being called each time the pointer View is redrawn. The background View only needs to draw the background graphics once, but retains the Bitmap that backs the canvas, and in subsequent onDraw() calls it draws this bitmap back to Canvas (which is a lot faster than initially creating the graphics using Path() objects).
Bitmap bm;
....
protected void onDraw(Canvas canvas){
if(null==bm){
bm=Bitmap.createBitmap(getMeasuredWidth(),getMeasuredHeight(),Bitmap.Config.ARGB_8888);
// do your slow vector graphics drawing to Canvas here
}
Paint drawPaint = new Paint();
drawPaint.setAntiAlias(false);
drawPaint.setFilterBitmap(false);
drawPaint.setDither(false);
canvas.drawBitmap(bm, 0, 0, drawPaint);
}
Well, there can't be more than 1 onDraw method, assuming that I understood your question correctly. You will need to think about alternate approaches about how to handle this.
#DeeV suggested, that can be a solution for you.