Android Question Howto realize UNDO for ImageView/Canvas?

Steini1980

Active Member
Licensed User
Longtime User
Hi,

how can I realize an UNDO Function for my drawn Canvas?
Here is my Examplecode, I don't know what to do...

B4X:
Sub Globals
   Dim c As Canvas
   Dim b As Bitmap
     Private ImageView1 As ImageView
     Dim r As Reflector
     Private Button1 As Button
     Dim undo As List
End Sub

Sub Activity_Create(FirstTime As Boolean)
     Activity.LoadLayout("Main")
 
   b.Initialize(File.DirAssets, "pic1.png")   
   c.Initialize(ImageView1)
   ImageView1.Invalidate
     r.Target = ImageView1
     r.SetOnTouchListener("img_touch")
     undo.Initialize
     undo.Add(ImageView1)
End Sub

Sub img_touch(viewtag As Object, action As Int, X As Float, Y As Float, motionevent As Object) As Boolean
  undo.Add(ImageView1)
  c.DrawCircle(X,Y,5dip,Colors.Red,True,1dip)
  ImageView1.Invalidate  
End Sub

Sub Button1_Click
  ImageView1 = undo.Get(undo.Size-1)
  undo.RemoveAt(undo.Size-1)
  ImageView1.Invalidate
End Sub
 

Dave O

Well-Known Member
Licensed User
Longtime User
It looks like you're doing the basics right:
- on touch, save the image first to your undo list, then make changes to it
- on button push (Undo button, I assume), pop the most recent image off the stack and put it back in the image.

However, you probably don't want to save/restore the entire ImageView. Really you just need the bitmap that is being drawn on. So your Undo list would be a list of bitmaps.

If you're drawing on a large ImageView, and running out of memory becomes a problem, you can drastically cut down on memory use by only saving the part of the bitmap that was affected by the latest drawing. In that case, you need to save the bitmap and its location (e.g. left and top coordinates) so you can restore it to the right place in the ImageView later.

Minor note: Your Undo button will need to check that the list is not empty. If empty, you can disable the button.

I just added multiple Undo to my sketching app, so happy to post some code examples if that helps.
 
Upvote 0

Steini1980

Active Member
Licensed User
Longtime User
Hi Dave,
thank you response. The postet code was just a first prototype/testing app without catching any errors, that's the reason why I do not check if list is empty. To save just the part of bitmap that needed is a great idea, otherwise I could also limit the undo to a few steps.

Why I'm posting the code here is my prototype doesn't works. Either I safe the whole ImageView1 object or excract/reimport just the bitmap I got always the newest version. It seems the Object is just referenced and not a copy of the last version.

It would be great if you could post some example code.
 
Upvote 0

JordiCP

Expert
Licensed User
Longtime User
Depending on which are the canvas operations allowed, you can drastically save memory setting a "starting_point_bitmap" and the operations (its parameters) made to it.

If your ops are, for instance
circle --> you need to save X,Y,radius, filled, color, strokewidth,...
free line --> list of X,Y points to connect , color,... (for instance, save all the pairs between ACTION_DOWN and ACTION_UP)
straight line --> X_start,Y_start,X_stop,Y_stop, color, ...

You will need to define a structure for each op and save them in a list or somewhere.

When you click on "undo", you simply regenerate the bitmap from the starting point applying all the saved operations except the last one (which is dropped from the list).

If you want an "undo" depth of, let's say 20, and your list has reached this depth, update the "starting_point_bitmap" with oldest ops and drop them from the list


I made something similar in an app for remote drawing on the other user's screen, so that I didn't have to send the new bitmap each time but only the ops made to it.
 
Upvote 0

JordiCP

Expert
Licensed User
Longtime User
Hi again,

Regarding your code, this works

B4X:
Sub Globals
   Dim c As Canvas
   Dim b As Bitmap
     Private ImageView1 As ImageView
     Dim r As Reflector
     Private Button1 As Button
     Dim undo As List
End Sub

Sub Activity_Create(FirstTime As Boolean)
   '  Activity.LoadLayout("Main")
   ImageView1.Initialize("img")
   Activity.AddView(ImageView1,0,0,100%X,80%Y)
   
   Button1.Initialize("Button1")
   Activity.AddView(Button1,30%X,80%Y,40%X,40%X)

    b.InitializeMutable(100%X,80%Y)
    c.Initialize2(b)
    Dim Rect1 As Rect
    Rect1.Initialize(0,0,100%X,80%Y)
    c.DrawBitmap(LoadBitmap(File.DirAssets,"pic1.png"),Null,Rect1)
   
    ImageView1.Bitmap=b
   ImageView1.Invalidate
     r.Target = ImageView1
     r.SetOnTouchListener("img_touch")
     undo.Initialize
  '   undo.Add(ImageView1.Bitmap) 'Not needed the first time
End Sub

Sub img_touch(viewtag As Object, action As Int, X As Float, Y As Float, motionevent As Object) As Boolean

    'Make a new copy of the existing bitmap and save it
  Dim bt As Bitmap
  bt.InitializeMutable(b.Width,b.Height)
  Dim cv As Canvas
  cv.Initialize2(bt)
  Dim Rect1 As Rect
  Rect1.Initialize(0,0,b.Width,b.Height)
  cv.DrawBitmap(b,Null,Rect1)
  undo.Add(bt)  

  c.DrawCircle(X,Y,5dip,Colors.Red,True,1dip)
  ImageView1.Invalidate 

End Sub

Sub Button1_Click
    If (undo.Size>0) Then
       b = undo.Get(undo.Size-1)
       'Log(undo.Size)
       ImageView1.Bitmap = b
       c.Initialize2(b)
       undo.RemoveAt(undo.Size-1)
       ImageView1.Invalidate
  End If
End Sub
 
Upvote 0

Dave O

Well-Known Member
Licensed User
Longtime User
Jordi's "save the original and all subsequent actions" approach works well for structured drawing strokes (circles, lines, etc.) that are easily reproducible.

If you allow freehand (pixel-by-pixel) drawing, another approach is to save a series of bitmaps instead of a series of drawing operations. My first version of multiple Undo saved the entire drawing area (pretty much the whole screen), which was easy to code, but used a lot of memory. It ran out of memory after about 10 Undo actions.

Then I made my code smarter by only saving the part of the screen that had changed (a rectangle of the min and max coordinates of the drawing stroke). Because most individual strokes only cover a small part of the screen, the Undo limit went from 10 (very easy to exceed) to 100+ (very unlikely to exceed).

I'll post some code snippets that may be useful.
 
Upvote 0

Dave O

Well-Known Member
Licensed User
Longtime User
Here's an excerpt of the drawing and undo code:

B4X:
Sub drawingPanel_gesture_onTouch(Action As Int, X As Float, Y As Float, MotionEvent As Object) As Boolean
     If Action = gd.ACTION_DOWN Then             'start a new stroke
       resetMinMaxXY
       updateMinMaxXY(X, Y)
       minY = Y
       maxY = Y
       oldX = X
       oldY = Y
     Else If Action = gd.ACTION_MOVE Then         'continue the current stroke
         drawLineOnPanel(oldX, oldY, X, Y)
         oldX = X
         oldY = Y
         updateMinMaxXY(X, Y)
     Else If Action = gd.ACTION_UP Then           'finished the stroke
       updateMinMaxXY(X, Y)
       padMinMaxXY                         'account for stroke width outside the rect
       Dim tempRect As Rect
       tempRect.Initialize(minX, minY, maxX, maxY)
       storeForUndo(tempRect)
       setUndo(UNDO_ALLOW)
     End If
   drawingPanel.Invalidate
   Return True                 'consume the event
End Sub

'reset min/max X and Y to values that will disappear on next update
Sub resetMinMaxXY
   minX = 999999
   minY = 999999
   maxX = -1
   maxY = -1
End Sub

'track the min and max values of the screen region covered by the drawn stroke
Sub updateMinMaxXY(xArg As Float, yArg As Float)
   minX = Min(minX, xArg)
   minY = Min(minY, yArg)
   maxX = Max(maxX, xArg)
   maxY = Max(maxY, yArg)
End Sub

'pad the undo rect to allow for half the stroke being drawn outside the region
Sub padMinMaxXY
   Dim padding As Float = (currentStrokeWidth / 2) + 1
   minX = Max(0, minX - padding)
   minY = Max(0, minY - padding)
   maxX = Min(drawingPanel.Width, maxX + padding)
   maxY = Min(drawingPanel.Height, maxY + padding)
End Sub

Sub resetUndo
   undoHistory.clear
   undoPanelBitmap.Initialize3(panelCanvas.Bitmap)     'store the starting screen so we can crop a part of it next time
End Sub

Sub storeForUndo(rectArg As Rect)
   'if we're short on memory, discard oldest screen to make room
   If Not(enoughMemoryForUndo) AND (undoHistory.Size > 0) Then
     undoHistory.RemoveAt(0)       '~we should really remove as many items as required for the new rect
   End If
   Dim tempUndoItem As undoItem
   tempUndoItem.Initialize
   tempUndoItem.rectBitmap.Initialize3(bmPlus.Crop(undoPanelBitmap, rectArg.Left, rectArg.Top, (rectArg.Right - rectArg.Left), (rectArg.Bottom - rectArg.top)))
   tempUndoItem.x = rectArg.Left
   tempUndoItem.y = rectArg.Top
   undoHistory.Add(tempUndoItem)                 'add crop of old screen to history
   undoTotal = undoHistory.Size
   undoPanelBitmap.Initialize3(panelCanvas.Bitmap)     'store the current screen so we can crop a part of it next time
End Sub

Sub enoughMemoryForUndo As Boolean
   Dim memoryForScreen As Long = drawingPanel.Width * drawingPanel.Height * 4     '4 bytes per pixel
   Log("memcheck free/needed: " & common.appCache.FreeMemory & "/" & memoryForScreen)
   Return common.appCache.FreeMemory > (memoryForScreen * 3)               '3x the screen size, to give us a wide margin
End Sub

'undo the most recent stroke by drawing the matching rect of the previous screen
Sub undoButton_Click
   Dim tempUndoItem As undoItem = undoHistory.Get(undoHistory.size - 1)     'get the most recent undo item
   Dim targetRect As Rect
   targetRect.Initialize(tempUndoItem.x, tempUndoItem.y, tempUndoItem.x + tempUndoItem.rectBitmap.Width, tempUndoItem.y + tempUndoItem.rectBitmap.Height)
   panelCanvas.DrawBitmap(tempUndoItem.rectBitmap, Null, targetRect)       'redraw the previous screen
   undoHistory.RemoveAt(undoHistory.size - 1)                     'remove it once undone
   If suppressToast = False Then
     If undoHistory.size > 0 Then
        ToastMessageShow("Undo : " & (undoHistory.size) & " of " & undoTotal, False)
     Else
       ToastMessageShow("No more to undo", False)
     End If
   End If
   drawingPanel.Invalidate
   undoPanelBitmap.Initialize3(panelCanvas.Bitmap)     'store the current screen so we can crop a part of it next time
   setUndo(undoHistory.Size > 0)
End Sub
 
Last edited:
Upvote 0

Dave O

Well-Known Member
Licensed User
Longtime User
BTW, I tried a few different methods to find the free memory available to an app, and the FreeMemory function from the Cache library (by @Informatix) was the only one that seemed to work properly. (Not surprising given his programming skills.)

For example:

B4X:
Dim appCache As Cache
Log("app free bytes = " & NumberFormat2(appCache.FreeMemory, 0, 0, 0, True))
 
Upvote 0
Top