Touch Event propagation, deleting a control while inside it's touch listener function.

The following is a discussion about what should happen when you delete a control from inside its touch listener.

take for instance the following code:

local r1 = display.newRect(300,300,300,300) r1:setFillColor(1,0,0,1) local r2 = display.newRect(300,300,100,100) r1:addEventListener("touch", function(event) print("r1 touch event", event.phase)      if(event.phase=="ended" or event.phase=="cancelled")then event.target.focus=false display.getCurrentStage():setFocus(nil) elseif(event.phase=="began")then event.target.focus = true display.getCurrentStage():setFocus(event.target) end return true end) r2:addEventListener("touch", function(event) print("r2 touch event", event.phase) if(event.phase=="ended" or event.phase=="cancelled")then event.target.focus=false display.getCurrentStage():setFocus(nil) elseif(event.phase=="began")then event.target.focus = true display.getCurrentStage():setFocus(event.target) r2:removeSelf() r2 = nil end return true end)

If you touch the white square you will get a print out of the events that are handled by each rectangles touch listener.

Normally events propagate from top to bottom unless your listener returns true in which case the event is consumed and not propagated to other objects underneath. 

In this case we delete the control in the middle of handling its ended event. Therefore we do not return true. 

As you can see there is never an “ended” event fired for the r2 rectangle and yet an “ended” event is still propagated to the object below (r1).

Why does this matter?

In my application I have two overlapping menus. The top menu has a close button. When I press the button its deletes the top menu and would normally stop the touch event. However because of the behavior described above it propagates the event to the menu below calling its close menu as well. In case of other menus it may cause undesired interaction with controls on menu below the one being closed.

Should it be events from chaining at the moment that the object is deleted?

In this case we delete the control in the middle of handling its ended event. Therefore we do not return true. 

As you can see there is never an “ended” event fired for the r2 rectangle and yet an “ended” event is still propagated to the object below (r1).

No, that’s not right.  You still return true.  Deleting the object does not stop the execution of the event handler code.  You still need to return true to stop propagation or false to allow it to continue.

Here is a working example that (I think) does what you want:

local onTouch = function( self, event ) local phase = event.phase local myName = self.myName if( phase ~= "moved" ) then print("Touched " .. myName .. "; Phase: " .. phase .. " @ " .. system.getTimer() ) end if( phase == "ended" or phase == "cancelled" ) then self.focus = false display.getCurrentStage():setFocus( nil ) display.remove( self ) print( "Removed " .. myName .. " @ " .. system.getTimer() ) elseif( phase == "began" )then self.focus = true display.getCurrentStage():setFocus( self ) end return true end local r1 = display.newRect(300,300,300,300) r1:setFillColor(1,0,0,1) r1.myName = "R1" r1.touch = onTouch r1:addEventListener("touch") local r2 = display.newRect(300,300,100,100) r2:setFillColor(0,1,0,1) r2.myName = "R2" r2.touch = onTouch r2:addEventListener("touch")

Roaming gamer,

Thank you for the example.

Making small changes and comparing with your code I was able to figure out what the Corona “_ bug " or " Undocumented behavior _” is. 

The only difference between the two blocks of code below is which event phase the delete (display.remove) happens in.

The following code :

---------------------------------------- --Rectangle Delete occurs in began event ----touch event handler function t(self, event) print("touch event", event.target.myName, event.phase) if( event.phase == "ended" or event.phase == "cancelled" )then event.target.focus=false display.getCurrentStage():setFocus(nil) elseif( event.phase == "began" )then self.focus = true print("deleting", event.target.myName) display.remove(event.target) end return true end local r1 = display.newRect(300,300,300,300) r1.myName="r1" r1:setFillColor(1,0,0,1) r1.touch = t r1:addEventListener("touch") local r2 = display.newRect(300,300,100,100) r2.myName="r2" r2.touch = t r2:addEventListener("touch")

Has the following output:

touch event     r2      began

deleting        r2

touch event     r1      ended

While the following code: 

---------------------------------------- --Rectangle Delete occurs in began event ----touch event handler function t(self, event) print("touch event", event.target.myName, event.phase) if( event.phase == "ended" or event.phase == "cancelled" )then event.target.focus=false display.getCurrentStage():setFocus(nil) print("deleting", event.target.myName) display.remove(event.target) elseif( event.phase == "began" )then self.focus = true end return true end local r1 = display.newRect(300,300,300,300) r1.myName="r1" r1:setFillColor(1,0,0,1) r1.touch = t r1:addEventListener("touch") local r2 = display.newRect(300,300,100,100) r2.myName="r2" r2.touch = t r2:addEventListener("touch")

has the following output:

touch event     r2      began

touch event     r2      ended

deleting        r2

As clearly shown above, there is some sort of undocumented behavior or bug, when removing an object in its “began” phase from within its touch event function.

  1. There were errors in your original code I didn’t correct because I didn’t want to side-track the discussion.  Namely, you were setting a flag called ‘isFocus’, but not using it.  That field could be named anything, it isn’t something that Corona looks for.  The purpose of setting a flag is to aid in the construction of your touch logic.  I’m providing sample code below that fixes your usage (see version 2  of touch in the function)

  2. You’re incorrect.  There is no bug or undocumented behavior here.  Its just very subtle and easy to miss.

In your code, you set the current stage focus to ‘point to’ the object you then tried to delete. This effectively creates a reference to the object and keeps it from being garbage collected.  It also keeps the ‘touch’ listener from being automatically removed.   

You need to remove the focus before deleting the object, and in fact if you delete the object in the began phase you should not use the focus feature at all.

See my code below for the correct way to achieve your desired result in either began or ended cases.

Also, just in case, what version of the SDK are you using? I’m using build 2015.2625

local function runTest( version ) -- Forward delcare 'touch' local touch -- Define touch -- if ( version == 1 ) then touch = function(self, event) if( event.phase == "began" ) then print("touch()", event.target.myName, event.phase) print("deleting in BEGAN phase", event.target.myName) display.remove(event.target) return true elseif( self.isFocus ) then if( event.phase == "ended " ) then print("touch()", event.target.myName, event.phase) return true end end return false end else touch = function(self, event) if( event.phase == "began" ) then print("touch()", event.target.myName, event.phase) self.isFocus = true display.getCurrentStage():setFocus(self) return true elseif( self.isFocus ) then if( event.phase == "ended" ) then print("touch()", event.target.myName, event.phase) self.isFocus = false display.getCurrentStage():setFocus(nil) print("deleting in ENDED phase", event.target.myName) display.remove(event.target) return true end end return false end end -- Choose offset X -- local offsetX = (version == 2) and 150 or 0 -- Build blocks with offset and use touch -- local r1 = display.newRect( 100+offsetX, 100, 100, 100 ) r1.myName="r1" if( version == 1 ) then r1:setFillColor( 1, 0, 0, 1 ) else r1:setFillColor( 0, 1, 0, 1 ) end r1.touch = touch r1:addEventListener("touch") local r2 = display.newRect( 100+offsetX, 100, 20, 20 ) r2.myName="r2" r2.touch = touch r2:addEventListener("touch") end runTest(1) runTest(2)

Touching the white cube over the red cube (began) produces this output:

touch() r2 began deleting in BEGAN phase r2

Touching the white cube over the green cube (ended) produces this output:

touch() r2 began touch() r2 ended deleting in ENDED phase r2

Roaminggamer,
 
Thank you for your insights, 

I am not sure about what the setFocus method is supposed to be used for. I  did not find any documentation  

explaining this method.The self.focus property was left over from when I copied it from the example code. 

I was using the latest public release  2014.2511. I downloaded the latest daily build. 2015.2627 to use for future tests.

 
I have been trying to simplify the code as much as possible. 
 
 

The following code removes focus before delete.  

 
 

---------------------------------------- --Rectangle Delete occurs in began event phase ----touch event handler function t(self, event) print("touch event", event.target.myName, event.phase) if( event.phase == "began" )then display.getCurrentStage():setFocus(nil) display.remove(event.target) end return true end local r1 = display.newRect(300,300,300,300) r1.myName="r1" r1:setFillColor(1,0,0,1) r1.touch = t r1:addEventListener("touch") local r2 = display.newRect(300,300,100,100) r2.myName="r2" r2.touch = t r2:addEventListener("touch")

 
However the output was:
 
touch event     r2      began
touch event     r1      ended
 
When I try this code (and I touch the r2 box):
 

---------------------------------------- --Rectangle Delete occurs in end event phase ----touch event handler function t(self, event) print("touch event", event.target.myName, event.phase) if( event.phase == "ended" or event.phase == "cancelled" )then display.getCurrentStage():setFocus(nil) display.remove(event.target) end return true end local r1 = display.newRect(300,300,300,300) r1.myName="r1" r1:setFillColor(1,0,0,1) r1.touch = t r1:addEventListener("touch") local r2 = display.newRect(300,300,100,100) r2.myName="r2" r2.touch = t r2:addEventListener("touch")

 It seems to work.

The output is:

touch event     r2      began  

touch event     r2      ended  

It is not working in one case but is in another.

I have tried to drop the setFocus calls but the results were the same.

Not sure why I did not find this yesterday

A setFocus() method explanation:

https://docs.coronalabs.com/api/type/StageObject/setFocus.html

In this case we delete the control in the middle of handling its ended event. Therefore we do not return true. 

As you can see there is never an “ended” event fired for the r2 rectangle and yet an “ended” event is still propagated to the object below (r1).

No, that’s not right.  You still return true.  Deleting the object does not stop the execution of the event handler code.  You still need to return true to stop propagation or false to allow it to continue.

Here is a working example that (I think) does what you want:

local onTouch = function( self, event ) local phase = event.phase local myName = self.myName if( phase ~= "moved" ) then print("Touched " .. myName .. "; Phase: " .. phase .. " @ " .. system.getTimer() ) end if( phase == "ended" or phase == "cancelled" ) then self.focus = false display.getCurrentStage():setFocus( nil ) display.remove( self ) print( "Removed " .. myName .. " @ " .. system.getTimer() ) elseif( phase == "began" )then self.focus = true display.getCurrentStage():setFocus( self ) end return true end local r1 = display.newRect(300,300,300,300) r1:setFillColor(1,0,0,1) r1.myName = "R1" r1.touch = onTouch r1:addEventListener("touch") local r2 = display.newRect(300,300,100,100) r2:setFillColor(0,1,0,1) r2.myName = "R2" r2.touch = onTouch r2:addEventListener("touch")

Roaming gamer,

Thank you for the example.

Making small changes and comparing with your code I was able to figure out what the Corona “_ bug " or " Undocumented behavior _” is. 

The only difference between the two blocks of code below is which event phase the delete (display.remove) happens in.

The following code :

---------------------------------------- --Rectangle Delete occurs in began event ----touch event handler function t(self, event) print("touch event", event.target.myName, event.phase) if( event.phase == "ended" or event.phase == "cancelled" )then event.target.focus=false display.getCurrentStage():setFocus(nil) elseif( event.phase == "began" )then self.focus = true print("deleting", event.target.myName) display.remove(event.target) end return true end local r1 = display.newRect(300,300,300,300) r1.myName="r1" r1:setFillColor(1,0,0,1) r1.touch = t r1:addEventListener("touch") local r2 = display.newRect(300,300,100,100) r2.myName="r2" r2.touch = t r2:addEventListener("touch")

Has the following output:

touch event     r2      began

deleting        r2

touch event     r1      ended

While the following code: 

---------------------------------------- --Rectangle Delete occurs in began event ----touch event handler function t(self, event) print("touch event", event.target.myName, event.phase) if( event.phase == "ended" or event.phase == "cancelled" )then event.target.focus=false display.getCurrentStage():setFocus(nil) print("deleting", event.target.myName) display.remove(event.target) elseif( event.phase == "began" )then self.focus = true end return true end local r1 = display.newRect(300,300,300,300) r1.myName="r1" r1:setFillColor(1,0,0,1) r1.touch = t r1:addEventListener("touch") local r2 = display.newRect(300,300,100,100) r2.myName="r2" r2.touch = t r2:addEventListener("touch")

has the following output:

touch event     r2      began

touch event     r2      ended

deleting        r2

As clearly shown above, there is some sort of undocumented behavior or bug, when removing an object in its “began” phase from within its touch event function.

  1. There were errors in your original code I didn’t correct because I didn’t want to side-track the discussion.  Namely, you were setting a flag called ‘isFocus’, but not using it.  That field could be named anything, it isn’t something that Corona looks for.  The purpose of setting a flag is to aid in the construction of your touch logic.  I’m providing sample code below that fixes your usage (see version 2  of touch in the function)

  2. You’re incorrect.  There is no bug or undocumented behavior here.  Its just very subtle and easy to miss.

In your code, you set the current stage focus to ‘point to’ the object you then tried to delete. This effectively creates a reference to the object and keeps it from being garbage collected.  It also keeps the ‘touch’ listener from being automatically removed.   

You need to remove the focus before deleting the object, and in fact if you delete the object in the began phase you should not use the focus feature at all.

See my code below for the correct way to achieve your desired result in either began or ended cases.

Also, just in case, what version of the SDK are you using? I’m using build 2015.2625

local function runTest( version ) -- Forward delcare 'touch' local touch -- Define touch -- if ( version == 1 ) then touch = function(self, event) if( event.phase == "began" ) then print("touch()", event.target.myName, event.phase) print("deleting in BEGAN phase", event.target.myName) display.remove(event.target) return true elseif( self.isFocus ) then if( event.phase == "ended " ) then print("touch()", event.target.myName, event.phase) return true end end return false end else touch = function(self, event) if( event.phase == "began" ) then print("touch()", event.target.myName, event.phase) self.isFocus = true display.getCurrentStage():setFocus(self) return true elseif( self.isFocus ) then if( event.phase == "ended" ) then print("touch()", event.target.myName, event.phase) self.isFocus = false display.getCurrentStage():setFocus(nil) print("deleting in ENDED phase", event.target.myName) display.remove(event.target) return true end end return false end end -- Choose offset X -- local offsetX = (version == 2) and 150 or 0 -- Build blocks with offset and use touch -- local r1 = display.newRect( 100+offsetX, 100, 100, 100 ) r1.myName="r1" if( version == 1 ) then r1:setFillColor( 1, 0, 0, 1 ) else r1:setFillColor( 0, 1, 0, 1 ) end r1.touch = touch r1:addEventListener("touch") local r2 = display.newRect( 100+offsetX, 100, 20, 20 ) r2.myName="r2" r2.touch = touch r2:addEventListener("touch") end runTest(1) runTest(2)

Touching the white cube over the red cube (began) produces this output:

touch() r2 began deleting in BEGAN phase r2

Touching the white cube over the green cube (ended) produces this output:

touch() r2 began touch() r2 ended deleting in ENDED phase r2

Roaminggamer,
 
Thank you for your insights, 

I am not sure about what the setFocus method is supposed to be used for. I  did not find any documentation  

explaining this method.The self.focus property was left over from when I copied it from the example code. 

I was using the latest public release  2014.2511. I downloaded the latest daily build. 2015.2627 to use for future tests.

 
I have been trying to simplify the code as much as possible. 
 
 

The following code removes focus before delete.  

 
 

---------------------------------------- --Rectangle Delete occurs in began event phase ----touch event handler function t(self, event) print("touch event", event.target.myName, event.phase) if( event.phase == "began" )then display.getCurrentStage():setFocus(nil) display.remove(event.target) end return true end local r1 = display.newRect(300,300,300,300) r1.myName="r1" r1:setFillColor(1,0,0,1) r1.touch = t r1:addEventListener("touch") local r2 = display.newRect(300,300,100,100) r2.myName="r2" r2.touch = t r2:addEventListener("touch")

 
However the output was:
 
touch event     r2      began
touch event     r1      ended
 
When I try this code (and I touch the r2 box):
 

---------------------------------------- --Rectangle Delete occurs in end event phase ----touch event handler function t(self, event) print("touch event", event.target.myName, event.phase) if( event.phase == "ended" or event.phase == "cancelled" )then display.getCurrentStage():setFocus(nil) display.remove(event.target) end return true end local r1 = display.newRect(300,300,300,300) r1.myName="r1" r1:setFillColor(1,0,0,1) r1.touch = t r1:addEventListener("touch") local r2 = display.newRect(300,300,100,100) r2.myName="r2" r2.touch = t r2:addEventListener("touch")

 It seems to work.

The output is:

touch event     r2      began  

touch event     r2      ended  

It is not working in one case but is in another.

I have tried to drop the setFocus calls but the results were the same.

Not sure why I did not find this yesterday

A setFocus() method explanation:

https://docs.coronalabs.com/api/type/StageObject/setFocus.html