Android Question Howto realize UNDO for ImageView/Canvas?

Discussion in 'Android Questions' started by Steini1980, Apr 13, 2015.

  1. Steini1980

    Steini1980 Member Licensed User

    Hi,

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

    Code:
    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
     
  2. Dave O

    Dave O Active Member Licensed 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.
     
    Steini1980 and thedesolatesoul like this.
  3. Steini1980

    Steini1980 Member Licensed 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.
     
  4. JordiCP

    JordiCP Well-Known Member Licensed 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.
     
  5. JordiCP

    JordiCP Well-Known Member Licensed User

    Hi again,

    Regarding your code, this works

    Code:
    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>0Then
           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
     
    Steini1980 likes this.
  6. Steini1980

    Steini1980 Member Licensed User

    Great thank you, I will test it again!
     
  7. Dave O

    Dave O Active Member Licensed 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.
     
    JordiCP likes this.
  8. Dave O

    Dave O Active Member Licensed User

    Here's an excerpt of the drawing and undo code:

    Code:
    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 > 0Then
         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: Apr 18, 2015
  9. Dave O

    Dave O Active Member Licensed 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:

    Code:
    Dim appCache As Cache
    Log("app free bytes = " & NumberFormat2(appCache.FreeMemory, 000True))
     
    lemonisdead likes this.
Loading...
  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice