5

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

  1. invalidate
  2. call onDraw
  3. Do some stuff
  4. invalidate
  5. 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
  • 23,393
  • 37
  • 102
  • 162

6 Answers6

5

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

Community
  • 1
  • 1
Zhenghong Wang
  • 2,117
  • 17
  • 19
  • @ZhenghonghWang I am not sure if I get it correctly, I set something dirty, and in my code, in the onDraw method, I am not really getting the dirty part explicitly. so are you saying that in my case the entire thing is going to get rendered? Because according to the author ,he has done it in the optimal way and that will be rendering only the dirty part. – Kraken Jan 16 '14 at 10:08
  • Yes, the author you mentioned is right. But the drawing process can be optimized better. I updated my answer. – Zhenghong Wang Jan 16 '14 at 15:44
  • 2
    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. – jjxtra Oct 02 '15 at 16:32
2

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?

Community
  • 1
  • 1
Sundeep
  • 1,536
  • 5
  • 23
  • 35
1

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.

Pasquale Anatriello
  • 2,355
  • 1
  • 16
  • 16
0

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.

Dyna
  • 2,304
  • 1
  • 14
  • 25
  • Kindly follow the comments on the other answer and provide the answer to my questions please. Thanks. – Kraken Jan 09 '14 at 18:14
0

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.

pdegand59
  • 12,776
  • 8
  • 51
  • 58
  • 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? – Kraken Jan 09 '14 at 17:53
  • When I say "the entire screen", I mean the entire view. – Kraken Jan 09 '14 at 17:58
  • The entire view is not entirely redrawn. There is a really useful option on the Developer options of your device to make you screen flicker whenever its getting updated. Enable this option, then try to touch just a rectangle, and only this rectangle (and a second one) should flick – pdegand59 Jan 09 '14 at 18:01
  • But what about the logs, surely if my loops are getting executed it means that all the canvas.draw will be getting executed too? How does the system know, what piece of code will "only" redraw the dirty area? – Kraken Jan 09 '14 at 18:07
0

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.

kgbook
  • 388
  • 4
  • 16