Circular Layout and Scrolling – Part 2

I think for this post I’d like to continue implementing the rotary control I started in my previous post. In that example, all we really did was figure out how to arrange a series of numbers around a circle. I briefly mentioned how you could implement a rotation on the entire circle, but didn’t get into the details of how that works. So, for this post, we’ll add a bit of touch handling code to take care of rotating the entire circle based on the movement of a user’s finger on the screen.

Touch Rotation

Although the iOS multitouch system is very advanced, for this example, all we really need to is track a single finger as it rotates around the center of our circle. To do this we’ll implement the touchesBegan:withEvent and touchesMoved:withEvent methods of UIView, which are inherited from UIResponder. Since we’re now building this example out into a more full-fledged control, we’ll go ahead and create a subclass of UIImageView that will take care of displaying the same view as before, as well as handling touches to implement rotation. I’ve added the basic number layout to a “setup method” that’s called from both initWithFrame: and initWithCoder: (which is used when the view is loaded as part of a .xib file). One more method we’ll need is the one that actually calculates the angle of the rotation. What we need to know is the angle between the user’s finger and the center of our view. Now, I’ve learned some of this stuff before, but my trigonometry skills are, let’s just say a bit rusty, so when it came time to do these calculations, I was lost at first. However, Matthijs Hollemans has a nice open source rotary knob control that saved me a lot of time and headache of figuring this out. It turns out that math.h has a very helpful function called atan2(x, y). The introduction on the wikipedia article sums it up nicely: it returns the angle in radians between the positive x axis and the point given by the coordinates. What’s that mean for us? Calculate the x and y distance of a touch from the center of our view and pass that to atan2, which will return us the angle of the touch relative to our center. Once this calculation is figured out, basically all we need to do is store the angle of the initial touch in touchesBegan: and then each time touchesMoved: is called, calculate how much the angle has changed since the last call and pass this in as the rotation value on the CGAffineTransformRotate function to rotate the current transform by that angle. Here’s the source on paste bin. It’s the header and implementation in one paste. It’s just a UIImageViewSubclass, so drop it in IB or add programmatically like you would any other UIImageView to see what it looks like. Or skip the background image and just subclass UIView instead. Everything else will work exactly the same.

NB: In general, any custom touch handling object really should also implement touchesEnded: and touchesCanceled:, but in this particular case, it doesn’t really matter much to us when or how the touches stop. When our touches end (or get canceled), our touchesMoved: method stops getting called, and the UI is simply left at whatever rotation it last received. Once new touches start, we store that initial angle value and continue on from there.

Preview

There’s lots more I could show on this whole concept, especially about how to connect the rotation with a certain selection or value, and about how to display more content than what can fit around one circle, but I don’t want to go too much further right now for a few reasons: 1) My implementation works pretty well and is quite flexible, but the animations aren’t all quite right, and it’s still got a few nasty edge case bugs I’m trying to work out, so it’s not yet ready for mass consumption! 2) I’m not sure how much of this will be interesting to everybody. So, what I thought I’d do is share a little video showing at least the visual aspect of what my this thing can do, and if anyone is interested in seeing more code when it’s fully baked, then let me know what you’d like to see, and I’ll share what I’ve learned! Sorry for the bad audio; just using my earbuds microphone and trying to be quiet and not wake up my wife. Oh yeah, and the weird video aspect ratio. I wish the iPhone simulator wouldn’t act so jumpy when you rotate it. Anyway please let me know what you think and what/if you’d be interested in hearing more about with this control.

Advertisements

35 thoughts on “Circular Layout and Scrolling – Part 2

  1. Ryan, great work! I would love to see how you do the dynamic loading of wheel objects (i.e., loading more objects than can fit on the wheel at once). Trying to do exactly what you’ve done here, just missing that last piece. If you could email me details, I would be eternally grateful!

    1. It’s actually a fairly complicated thing to get working correctly and consistently. I’ll take a look and see what I might be able to explain over email. I’ve also been thinking about open sourcing the code and/or doing a few more blog posts about it. So, good to know there’s some interest in it then! Good luck, and I’ll see what I can do to help.

  2. Great, great work Ryan !!
    I followed your tutorial and made the circular scrolling. Now i’d like to simulate Convertbot app.
    My problem is when i want detect the current selection (in the convertbot is the top center) i don’t know do that. i would like detect when the user drag a label in a specific point like Convertbot.

    Thanks for help

    1. What I’ve done for selecting things like that is to figure out which thing is already positioned at the selection (e.g. the top) right from the beginning layout. In some kind of rotation variable, probably a float, store this setup as 0 rotation. Then add or subtract to this variable as things get rotated around. Since you know how large the angle of each item is (360/numberOfItems if you’re using degrees, or 2pi/numberOfItems for radians), it’s easy to work out which one is now closest to the top. For example, if you have 6 items arranged around the circle, rotating 60 degrees clockwise will mean that the next item to the left is now exactly centered at the top. By rounding the rotation value to nearest int, you can find out at any time which item is closest to the top. Hope that helps and makes sense!

      1. Thanks for the reply.
        The selection of my start is located at the bottom (eg 6 clock hours). I have seven items and I have calculated the width of each item. Every time you turn clockwise or counterclockwise, and then divide calculating grades for my corner for the location of the item current. The solution works but not always.
        In fact sometimes returns meaningless values​​.
        My problem I think working with grades is more difficult. I would now like to take your solution to set an initial value of float and from there to see how I moved.

        I would be very grateful if you could send me the code that you use so I rely on that and I hope to get out of this problem is driving me crazy.

        Thanks again for the help

  3. Is there a plan on making this code open source on GitHub, so developers can play around with it, customize it and make it better.

    1. No definite plans, but once I’m through with the project I’m using this on, I will most likely get it packaged for others to use and get it put on github. I will post some kind of announcement/link here on the blog when that happens.

      1. sounds good. I’ve gotten up to the point where its a wheel and i can add data to it, but i haven’t figured out the continuous scroll yet. I’ll just have to keep working on it.

  4. Hi, when i go to pastebin for the code, it looks like the header file is missing. The implementation part is there. There is no sign of the header in the text. Is this correct?

    Great post by the way.

  5. Great start! I’d love to see more code for it! Do you have any idea how to get the circular scrolling to scroll with inertia, like a UIScrollView?

    1. I thought about inertia, but decided I didn’t need it for my project. Of course, that decision may have been influenced by how hard it would be 🙂 The basic concept is not that complicated – measure the speed of rotation when they lift their finger and calculate how long it would take, at a constant rate of deceleration, to stop. Then, plug all this into maybe a key frame animation. Actually working out that math though and getting it to feel right as well as behave correctly when it’s interrupted mid-animation, etc. That would be quite a challenge I think!

  6. How did you manage to get the rotation angle of your view? I’ve only been able to get the absolute angle, by using the view’s transform and calling atan2(transform.b, transform.a) to get the absolute angle of the transform.

    1. Nevermind, I can just track the total angle by summing up all the angle diffs when tracking the touch movement.

  7. Great Job!!!

    I need your help: “How to calculate best fit frame for to create horizontal UILabel on each wheel view sector/slice”

    Could you please provide small code snippet for this.

    1. Sorry, I’m not sure what exactly you’re asking. Do you mean how to know what size of frame to give the string itself? If so, there are different ways this can be done. [NSString sizeWithFont:] will return a CGSize that can contain the given string drawn at that font. There are also variations that allow you to restrict it to certain size or width. UILabel also has a method called -sizeToFit that is used to make sure the frame of the view expands to contain the string that it is given.

  8. I’ve tried to achieve something similar using a UIScrollView and changing the position of its elements in scrollViewDidScroll using the circle’s equation but I don’t manage to make them follow a whole circle path. Do you think it would be possible to get it working this way?

    1. It would be great if there were a way to get UIScrollView to behave in this way, because then you could take advantage of things like deceleration. But, unfortunately, I don’t know if there’s any way to get it to work this way. If you figure something out, I’d love to hear about it!

  9. Finally I decided to start from your code instead of trying it with an UIScrollView (I got nothing but a headache). I’m trying to rotate the numbers to get an effect like the “Ferris Wheel” that you show in the video but I’m a bit stuck.

    I’m creating new UILabels and adding them to an UIView container in which I’ve got the wheel and the sectionlabels. My idea is to set the position of these new UILabels each time the wheel gets rotated but it’s not working :(. Could you please give me some advice?

    Thanks very much in advance!

    1. Sorry for the slow response. Here’s the general idea of what I do for the Ferris Wheel style – and all the other styles, for that matter:
      – Using the code as I’ve shown on the blog, instead of placing UILabels directly on the “wheel,” I first layout container UIViews around the circle.
      – Then, I add the Labels as subviews of these containers, so that…
      – The container views maintain their relative positions around the wheel when the whole thing gets rotated
      – And, I can control the position and rotation of each UILabel individually _within_ the container view, without affecting its layout around the wheel itself.

      Does that make any sense? I probably just need to get to work on getting this whole thing open sourced; just haven’t had the time!

  10. Wow. Great job!!

    I was trying to modify something here. Instead of the numbers I wanted to display some pictures randomly. How can I do that?

    Thanks.

    1. Hi Matthew. What I’ve done on my project is to follow the same basic idea, but instead of inserting, for example, the UILabels directly, I create empty UIViews which are laid out around the circle. Then, you can add images or whatever UIView class you want as subviews of those containers. Does that make sense? On a related note, I’ve been completely ignoring this blog and also getting this control open-sourced, but would still like to at some point! Sorry it’s not available yet 🙂

  11. My brother recommended I would possibly like this blog. He was once entirely right.
    This submit truly made my day. You can not believe just how much time I had spent for this info!

    Thank you!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s