UPDATE #3 (3/4/2015): Fixed a bug that caused a “completed” event to be triggered upon creating the progress ring. Now “completed” events will only be triggered by ring:goTo() calls made after the ring’s creation. Thanks to Nick for finding this mistake and pointing it out in the comments!
UPDATE #2 (1/23/2015): Fixed a bug that only manifests in Corona SDK build #2015.2544 and later. In build #2015.2544, Corona Labs fixed the finalize() bug that prevented objects’ finalize events from being triggered when their parent display group was removed. I had utilized a brilliant workaround developed by Sergey Lerg, but there was an error in my implementation of his fix that caused a crash when run on build 2015.2544 and later. I fixed that crash by adding a 1ms timer when checking to see if finalize events are being triggered as they should. The fix is not necessary for up-to-date Corona builds, but I’m leaving it in for folks who might be running legacy Corona builds.
UPDATE #1 (1/7/2015): The progressRing module has been updated to ensure that the procedurally-generated mask image reliably meets the requirements for mask images. Previously, it didn’t always work when running on devices whose screen size did not match up with the content width/height defined in an app’s config.lua file.
Something that is occasionally asked about on the Corona Labs forums is how to achieve a circular progress indicator (what I call a “progress ring”). The uses for this type of widget are limitless, but here are a few examples:
- player/enemy health bars in a RTS-style game (see Ravenmark)
- countdown timer to show when a player’s turn is up in a turn-based multiplayer or puzzle game
- a more visually interesting percentage indicator than the usual horizontal or vertical bar (in a business app)
A number of solutions have been proposed in the forums, often involving the use of sprite objects with X number of frames to cycle through depending on the position of the progress bar. These solutions can work, of course, but I set out to create my own progress ring solution with the following goals in mind:
- The movement of the progress ring should be seamless and smooth, regardless of which direction it was moving and/or how quickly (something that can’t happen with sprite-based solutions unless you use a prohibitively high number of frames)
- The progress ring must be created solely with code, so that it can be modularized into a single lua file and added to a project with one line of code (again, something that isn’t possible with sprite-based solutions).
- The progress ring must be visually customizable through code: colors, size, depth of the “donut hole” (or no donut hole at all), stroke width, etc.
- A progress ring object must have simple and easy-to-use methods for controlling it after it’s been created.
And I’m happy to report that I’ve come up with a solution that meets all those requirements and makes it very easy to add a progressRing to any Corona project with just one line of code!
How to Make a Progress Ring
Here’s a simple step-by-step guide to adding a basic progress ring to your Corona app with just one line of code:
- Download progressRing.lua and place it in your project’s root directory (where you keep main.lua).
- Require the module into your app by adding this line of code:
local progressRing = require("progressRing")
- Create a progressRing object in your app by adding this line of code:
local ringObject = progressRing.new()
That’s it! Following those steps will create a very basic progressRing with some default parameters. The returned progress ring is just a Corona display group, so you can easily manipulate its x and y coordinates, rotate it, scale it, add it to other display groups such as a Composer scene group, and get rid of it by calling display.remove() on it. Any method you can call on a Corona display object can be called on your progress ring.
Customizing your Progress Ring
The above steps are the fastest way to add a progress ring to your app, but the returned progress ring will be very minimal. However, it’s very easy to customize the look of your progress ring by passing a single table as an argument when you call progressRing.new(). The table can include any of the following key/value pairs, but none are required:
- radius: a number representing the radius of your ring in pixels. Defaults to 100.
- ringColor: a table containing 4 numbers between 0 and 1, representing the RGBA values of your ring’s bar color. Defaults to {1, 1, 1, 1} (white).
- bgColor: a table containing 4 numbers between 0 and 1, representing the RBGA values of your ring’s background color. Defaults to {0, 0, 0, 1} (black).
- ringDepth: a number between 0 and 1 representing the depth of your ring. A ringDepth of 1 will result in a fully-round ring (“all donut, no hole”), while a ringDepth of 0 would result in an invisible ring (“all hole, no donut”). Defaults to .33.
- strokeColor: a table containing 4 numbers between 0 and 1, representing the RBGA values of your ring’s stroke (border) color. Defaults to whatever your bgColor is.
- strokeWidth: a number representing the width of your ring’s stroke (border) in pixels. Defaults to 0 (no stroke).
- counterclockwise: a boolean (true/false) value indicating whether or not the ring should advance in a counter-clockwise manner. Defaults to false.
- hideBG: a boolean (true/false) value indicating whether or not the background should be visible. Defaults to false.
- time: a number representing the amount of time (in milliseconds) your ring will take to make a full rotation, from 0 to 360 degrees. Defaults to 10000 (10 seconds).
- position: a number between 0 and 1 representing the starting position of your progress bar. 0 would result in no visible progress bar (i.e. 0 degrees). 1 would result in a full progress bar (i.e. 360 degrees). .5 would result in a halfway-full progress bar (i.e. 180 degrees). Defaults to 0.
- bottomImage: a string representing the path to an image file (i.e. “images/bottom.png”) that will appear “underneath” or “behind” your progress ring. Automatically supports dynamic image scaling (@2x, @3x, etc.). Defaults to nil.
- topImage: a string representing the path to an image file (i.e. “images/top.png”) that will appear “on top of” or “in front of” your progress ring. Automatically supports dynamic image scaling (@2x, @3x, etc.). Defaults to nil.
As an example, here is some code that would create a progress ring with a radius of 200 pixels, a red background, a green ring, a ring depth of 33%, and that takes 4 seconds to make a full rotation:
local ringObject = progressRing.new({ radius = 200, bgColor = {1, 0, 0, 1}, ringColor = {0, 1, 0, 1}, ringDepth = .33, time = 4000, })
Download the sample project to make adjustments to the available parameters on the fly and see them reflected on-screen in real time.
Let’s Get Moving!
Now that you’ve created your progress ring, you can call several methods in order to make it advance or retreat as needed. Here are the available methods:
- ringObject:goTo(position, [time]) is used to advance/retreat the position of the progress ring. Note:
- position (required) is a number between 0 and 1 representing the position the bar should advance or retreat to.
- time (optional) is a number representing the amount of time (in milliseconds) it will take for your ring to reach the position you defined. By default, this is determined by the time you set for a full rotation (i.e. if you set a time of 10 seconds for a full 360-degree rotation, and your ring is advancing 180 degrees, it will take 5 seconds). Setting a time of 0 will result in an immediate repositioning of your progress ring.
- ringObject:pause() will pause your progress ring while advancing or retreating.
- ringObject:resume() will resume a paused progress ring.
- ringObject:reset() will return your progress ring to the starting position you defined when creating the object.
A Note About Nested Mask Limits and Snapshots
In general, this module should perform well on any device capable of running a Corona app. However, there are a few things that you may need to take into account when adding a progress ring to your app:
- Each progress ring object utilizes two masks that are created on-the-fly, through code. One is to create the “donut hole” effect, and the other is to mask the movement of the ring “slices” as they appear from a zero position. Corona has a nested masking limit of 3, so take care when inserting your progress ring into other masked objects, such as display containers, to make sure you don’t exceed that limit.
- I wanted to allow my progress rings to have semi-transparent ring colors, but the only way to do that without ugly visible seams when the ring’s “slices” overlap was to utilize Corona’s display.newSnapshot() API. Snapshots do come with a bit of a performance penalty, so you should take that into consideration if you assign an alpha value of less than 1 to your ringColor. Also, keep in mind that the snapshot API is only available for Pro/Enterprise Corona users. (Note: the module only utilizes the snapshot API if you specify an alpha value of less than 1 in your ringColor table, otherwise it uses regular display groups. So if you have a fully-opaque ringColor, these concerns don’t apply.)
Have You Used It?
This module is free to download, free to use, and you are free to modify it to your heart’s content. I hope that you find it useful! But if you do use it in one of your apps, please share a link to your app in the comments, or contact me so I can see how you integrated it into your app. I’d love nothing more than to see to see my code getting used out in the wild. Enjoy!
This is excellent! I’m definitely adding this to my toolkit. I can see all kinds of great uses for this.
Thanks for taking the time to produce such an elegant solution and for sharing it freely.
Holy wow!!! This is pretty danged glorious!! Thanks!! Let’s see what we can do with it!!
-Mario
Can’t get it to work. Require the lib in, create the object, (just the two lines, no custom params) and get only a suggestion of a shape in the upper left corner. No errors. Any thoughts? Seems like a coordinate thing…
Hi Kevin – sorry for the delayed response. For some reason I wasn’t notified about your comment. Anyhow, if all you are calling is:
local progressRing = require("progressRing")
local ringObject = progressRing.new()
Then the progress ring will be centered at position {0, 0}, which is the upper-left hand corner, as is the case with most Corona display objects. You can assign a position to your progress ring as such, though:
local progressRing = require("progressRing")
local ringObject = progressRing.new()
ringObject.x, ringObject.y = display.contentWidth/2, display.contentHeight/2
The above code would place the progress ring right in the center of the screen. Please give that a try, and if it’s still not working, let me know (you can email me at apps {at} jasonschroeder {dot} com).
Thanks again – I am a fan of NuOffer, by the way! You guys did great work on that app. 🙂
Hey, I don’t get notifications either 🙂 but checked back yesterday since I was in search of a circular progress ring. Installed, looks great in sim, but distorted due mask issues on device. Here’s a +1 for the plugin you were thinking about!
It should also be noted that an awesome color picker also comes in the sample project.
It’s a two-fer!
Thanks for the compliment, Ed! For anybody interested, here’s a link to that color picker as a standalone module, with documentation: https://www.jasonschroeder.com/2014/03/24/add-a-color-picker-to-your-corona-app-with-one-line-of-code/
Impressive code ! Smart, with good explanations, demo video and download link. I’m really impressed.
Thx and congrats !
I tried the module (in a code with no mask).
At the end of timer, I don’t get a disc, I have a mushroom ! (at -90°) and in the console :
“Fixing finalize() bug…”
Could you help ?
Hi Antheor,
I’m not sure what you mean by a “mushroom” – can you please send me a screenshot and any other details that might help? I’d love to fix any bugs that might be in the code. You can send it to apps {at} jasonschroeder {dot} com. Thanks!
Thx for your help.
Actually, I tried another thing for my code. But, I’ll probably try your module on another project. I’ll be back then 🙂
I’m happy to test your module.
I tried
ringObject.onComplete = function ( self )
print(“I did it in “.. self.time..”ms”)
end
with no sucess…
Will this module work for graphics 1.0? I have a project that is still being developed in 1.0 and this doesn’t seem to work.
Hi Mark,
The quick answer to your question is “no.” The module will not work as-is with Graphics 1.0. You may be able to make it work in a Graphics 1.0 project if you take the time to replace any anchor points with the deprecated “setReferencePoint” method, and adjust any RGBA color values with the older “0-255” scale. However, I’m pretty sure that the display.newSnapshot API is only available in Graphics 2.0 builds of Corona, so be sure not to include an alpha channel on your ringColor, or else you’d probably get a Runtime error. All of this is very much “your mileage may vary,” though – I haven’t tested any of it, and I’m personally no longer writing code for deprecated Corona APIs. If you get a working G1 version of it, feel free to share it in the Corona forums!
Great Work Jason,
Is it possible to make the timer start at a certain segment angle eg 270 degrees or 0.75 and then transition back to 0. Like a count down rather than a count up. I thought the ‘counterClockwise ‘ option might provide this function but it doesn’t seem to. Perhaps a ‘reverse’ argument that can be set to true or false.
Or am i missing something and can this already be done?
Cheers
Nick
Hi Nick,
What you are looking for is the “position” parameter you can set when setting up your progress ring. If you set it to .75, the progress ring will default to the 270 degree position you’re looking for. Then just call ring:goTo(0) and it will move in the negative direction, down to zero. Or alternately, you can set it up as a counter-clockwise ring, set the starting position to .25, and call ring:goTo(1). Thanks for checking it out!
Best,
Jason
Thanks for the explanation Jason. That works well.
Now.. I just tested the following code and it generates two completed events in output. I was hoping to see if i can use the timer to control another proceedure. Am i using this feature correctly. Heres my test code and output below.
cheers
Nick
local progressRing = require(“progressRing”)
local ringObject = progressRing.new({
radius = 200,
bgColor = {0, 0, 0, 1},
ringColor = {0, 1, 0, 1},
ringDepth = .33,
time = 4000,
position = 0.75
})
ringObject.x, ringObject.y = display.contentWidth/2, display.contentHeight/2
ringObject:goTo(0,10000)
— Setup listener
local myListener = function( event )
print( “Event ” .. event.name )
print( “ring has completed”)
end
ringObject:addEventListener(“completed”, myListener)
Output at terminal
[]
[] Version: 3.0.0
[] Build: 2014.2511
[] Fixing finalize() bug…
[] Event completed
[] ring has completed
[] Event completed
[] ring has completed
Hi Nick,
Thanks for catching this bug! I’ve updated the module to fix this issue. You can re-download it using the link at the top of the post. Before, a “completed” event was being triggered anytime a “goTo” call was made on a ring object – and I used a “goTo” call in order to set the ring at its starting position, which is why you saw duplicate events getting fired in your listener. I’ve set up a flag now to prevent those events from getting fired on the initial “goTo” call that sets the ring at its start position. If you run that same code again, you should only see the one “completed” event that you expected. Sorry for the mistake, and thanks for catching it and reporting it! Cheers!
No worries Jason, thanks for the fix
Hi Jason,
Should the ring object support positioning with the anchorX and anchorY properties? I tried the following, but it still behaved as if anchorY was the default 0.5
ringObject.anchorY = 1
ringObject.x = centerX
ringObject.y = screenBottom - 10
Also, if others think it would be useful, I’ve added topImageW, topImageH, bottomImageW, and bottomImageH parameters to the progressRing.lua module, so that you can scale your top/bottom images to whatever height you need. I can send that your way if you’d like.
Hi Mark,
I’m sure you could get anchorX / anchorY positioning working by going in and adjusting some of the code, starting with setting the returned display group’s .anchorChildren parameter to true, but adjustments to the anchorChildren setting often results in unintuitive and unexpected behavior – I tried setting anchorChildren true on the ring in the sample project, and while it did reposition the ring based on adjusting its anchorX / anchorY values, it wasn’t always where I expected it to be. If you manage to get it working reliably, please do share here – I’m sure others would love to have that additional functionality. Thanks for checking it out, and for commenting!
Best,
Jason
Hi Jason,
Your example code produces totally bizarre output on test device (blaupunkt running android 4.1.1) . I tried the test program after i started seeing a segmentation error in my game after adding the progress ring. Everything worked fine in simulation mode but crashes with Fatal signal 11 (SIGSEGV) at 0x5c556000 (code=1), thread 4923 (Thread-117). If i remove the ring all works fine again any idea? I took some screenshots of your program running if you want them let me know.
Hi Steve,
This is the first I’d heard of any sort of fatal crashing behavior tied to the module – there is a known issue related to how I dynamically create the circular mask that presents itself in certain situations when using Corona’s content scaling (i.e. “letterbox”, “zoomEven”, etc.) – but this shouldn’t cause a crash. It would only result in a bizarre-looking progress ring. Without knowing all the details, I’d suggest you maybe use a pre-rendered circular mask image instead of the dynamically-generated one and see if that fixes things. I may not be able to respond as quickly as you’d like, but feel free to email me at apps {at} jasonschroeder {dot} com with screenshots and/or crash logs and I’ll see if there’s anything I can do. Have you tried running it on any other devices? If you are willing to send me an APK I’m happy to load it up on one or two of my own Android test devices and see if I get the same issue.
Thanks,
Jason
Great coding Jason! How I would “loop” the ring, I mean start it over after completation?
Thanks and congrats,
Eduardo
Hi Eduardo,
It’s an undocumented feature, but each progressRing dispatches a “completed” event when it finishes any “goTo” operation, so you can add an event listener to your ring object that starts up a new “goTo” operation on completion. For example:
local function onComplete(event)
if ring.position == 1 then
ring:goTo(0)
elseif ring.position == 0 then
ring:goTo(1)
end
end
ring:addEventListener("completed", onComplete)
Hope that helps!
Ok, ring.position. Thanks for the help Jason!
Hello, congrats for the great work
I tried your sample code and it works perfectly but when tried integrating it into my project it comes as still with no animations as if it is an image i used the following code :
local progressRing = require(“progressRing”)
local ringObject = progressRing.new({
radius = 200,
bgColor = {1, 0, 0, 1},
ringColor = {0, 1, 0, 1},
ringDepth = .33,
time = 4000,
})
ringObject.x=centerX
ringObject.y=centerY
Please give me a solution there are no ring rotating animatins only a still image with the defined params is shown
thanks
Hi @sanket,
I think all you need to do is start the ring by calling ringObject:goTo(1). Until you tell the ring to progress to a specific value between 0 and 1, it won’t do anything. Give that a try – based on your post, I think that should take care of things for you.
Thanks,
Jason
Oh, thank you so much for quick reply i am new to corona and game development
Thanks
When i use it in a composer scene it ia not working as it should only white color ring is made and the size of the ring increases gradually and at last it become something like a moon don’t know what is happening
Without composer scene working nicely
I am also working on composer,at initially it stills like a image,but after ring:goTo(1) statement, it works awesome .
Thank you
For Apple devices, letterbox works fine for any device in the simulator but not on the actual device (iPad mini or iPhone 4 for example) – it is distorted.
Is there some fix for this or do you think there will be?
Thanks in advance,
David
Hi David,
Unfortunately, this is a side-effect of the way I dynamically generate mask files “on the fly” – when you’re using dynamic content scaling to support different devices, the generated mask files sometimes don’t fit the strict requirements for mask images. You can get around this by eliminating the code that generates the mask file and using your own, if you want. I don’t like making modules that can’t be dropped in using a single Lua file, which is why I tried to make the mask image dynamically. But I may repackage the module as a plugin soon, which would allow me to package pre-made mask images without the added overhead for the end user – you could just require the plugin and automatically get those images. Sorry this is still an issue, but I’ll try to put together that plugin soon. If/when I do, it’ll be free.
Thanks,
Jason
Hi, Jason!
Thanks for your great module. Beautiful in the simulator — but I’m having some trouble working with it on device. Basically, in the simulator, it looks like this — a thin donut shaped progress ring, which is just as it’s supposed to be:
http://imgur.com/x0PBl9w
However, on device is a different story. The progress ring becomes a larger collection of triangles without a donut hole, and the background color is just a solid circle — again, with no donut hole. Here are two pics of this happening (ignore the black flowery thing for now).
http://imgur.com/a/2qsqF
I have gotten the progress ring to show nicely, but there is a line in my code which causes the problem. It’s:
pic = display.newImage(picName)
This is a line which displays pieces of the black flowery thing. I don’t know why displaying this image is causing the progress ring to screw up. It seems like it might be a problem with using containers or masks in your code. Do you have any idea why this would happen? I’d love to use the progress ring in my game, but it doesn’t seem to be working. I’m happy to send you my project (it’s quite small), if that helps.
Thanks!
Simon
Hi, I have exactly the same problem in my game. Did you found a solution?
Thanks!
Matthias
Oh man, first I was so thrilled, ’cause this module was so stylish and flexible. But… it works (only) on Simulator, but not in device. This is a screen capture from iPhone 6 (two rings and the code is from the example above):
https://s17.postimg.io/hcuw87hrz/device.jpg
Jason, do you think you’ll get this fixed?
I read from the Corona forums that we could get around this problem:
“In the meantime, you can get around that issue by making your own mask image and modifying the code to use that pre-fabricated image.”
Could you please share the code what I should change, thanks 🙂
@Tommi: the code you would need to replace begins on line 223 of the module. Basically you can remove most of the code that “creates circular mask for slices.” Instead of all that effort to create and save a mask image (which doesn’t always save to the spec required by Corona for mask images when using dynamic scaling, just load your own mask image as it does on line 253 and then set the mask on the “bg” object (see line 256) and the “sliceGroup” object (see line 258). That’s a distillation of exactly what you’ll need to do, but it should get you on your way.
this is WONDERFUL!
thank you very much…
Is it possible to add/subtract time on the fly? so imagine some code will pause the progress bar, subtract or add whatever time to the existing time and then will resume again.
Is this possible? Great feature!
@Dip: check out the progressRing:goTo() method, which allows you to set a custom time parameter for how long it should take to complete a rotation. That would allow you to make it move faster or slower as you need. Note that this wouldn’t actually change the default behavior on the ring object, but it would be a way to achieve what you describe. Or – feel free to modify the module any way you see fit! 🙂
Thanks for visiting the site – hope you find it useful!
I think this is a great concept and it surprises me Corona doesn’t have this built in! I realize this was built two years ago and there are probably some kinks with more recent versions of Corona, but I can’t seem to get it to work at all. If you’re still actively working on this any help would be fantastic. I’ve added the following code:
local progressRing = require(“progressRing”)
local myProgressRing = progressRing.new({radius = myRadius, time = 5000})
myProgressRing.x = location.x
myProgressRing.y = location.y
sceneGroup:insert(myProgressRing)
myProgressRing.goTo(1, 5000)
But unfortunately all I get is a black circle that makes no movement whatsoever… Any advice?
Also, when I run the sample app on the simulator, it shows a circle with a moving bar that turns the circle orange, so that works – but it also turns the area around the circle orange, making a square on one half, and a larger semi-circle on the other half… very bizarre shape!
Well, I figured out the first issue – it’s myProgressRing:goTo…, not myProgressRing.goTo. Whoops.
I noticed the same strange background behavior in my app as I had noticed in the sample app. The good news is that setting bgColor = {.3, .3, .3, .3} fixed this issue – beats me as to why, but it works for me, since that’s the background color I want.
Now the only thing left bothering me is that there is a noticeable hitch when I create the progress ring. I create it at the very start of the scene, and when I switch to that scene, it “blinks”, for lack of a better word. This happens both on the simulator and a real (android) device. It’s noticeable enough that even though the progress ring looks fantastic now, I’m not sure that I want to use it… Jason, if you happen to see this, please let me know if you have any idea why that’s happening!
Also, thanks again for building this. Like I said, it looks fantastic now!
It should be noted this ring works fine on any device when the config.sys scale = “adaptive”. No distortions.
Is it possible to modify this to accept a manual value between 0 and 1 that would fill the ring? 0=empty, 1=full, 0.25=quarter filled, etc.?
Thanks in advance Jason.