{"id":2162,"date":"2015-09-08T21:35:31","date_gmt":"2015-09-09T04:35:31","guid":{"rendered":"https:\/\/www.microsoft.com\/reallifecode\/index.php\/2015\/09\/08\/rotary-wheel-control-with-heavenfresh\/"},"modified":"2020-03-18T16:50:00","modified_gmt":"2020-03-18T23:50:00","slug":"rotary-wheel-control-with-heavenfresh","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/ise\/rotary-wheel-control-with-heavenfresh\/","title":{"rendered":"Rotary Wheel Control with HeavenFresh"},"content":{"rendered":"<p>Windows 10 introduces the Universal Windows Platform (UWP) providing a common app platform enabling the ability to install the same app package onto every Windows 10 device: phone, desktop, Xbox, IoT, Surface Hub, etc. We worked with <a href=\"https:\/\/twitter.com\/heavenfresh\">@HeavenFresh<\/a> to create a UWP application and, in the process, a rotary wheel user control. This case study describes the approach taken in building the resulting user control including the mechanics involved with drawing a rotary wheel, supporting user interaction, and the usage of storyboards for animations.<\/p>\n<h2 id=\"customer-problem\">Customer Problem<\/h2>\n<p><a href=\"http:\/\/www.heavenfresh.com\/\">HeavenFresh<\/a> makes a plethora of products for the home including AllJoyn connected air purifiers and humidifers. In preparation for <a href=\"http:\/\/b2b.ifa-berlin.com\/\">IFA Berlin 2015<\/a>, we held a hackfest to build a <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/windows\/apps\/Dn958439.aspx\">Universal Windows Platform<\/a> application to control HeavenFresh devices from a Windows 10 device. During this collaboration, we wanted to have a single user interaction element that could be used to configure different settings of the humidifier\/purifier (e.g. fan speed, humidity level, etc). This led us to building a rotary wheel <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/windows\/apps\/windows.ui.xaml.controls.usercontrol\">user control<\/a> which allowed us to reuse the same component across the application.<\/p>\n<h2 id=\"overview-of-the-solution\">Overview of the Solution<\/h2>\n<p>Shown below is the stylized rotary wheel control used in HeavenFresh\u2019s application:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/cse\/wp-content\/uploads\/sites\/55\/2020\/03\/2015-09-09-Rotary-Wheel-Control-with-HeavenFresh-rotary_wheel_styled.gif\" alt=\"Rotary Wheel\" \/><\/p>\n<p>The same control was re-used across the application to configure different settings of the air humidifier and purifier.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/cse\/wp-content\/uploads\/sites\/55\/2020\/03\/2015-09-09-Rotary-Wheel-Control-with-HeavenFresh-same-control-different-settings.png\" alt=\"Reusable control\" \/><\/p>\n<h2 id=\"implementation\">Implementation<\/h2>\n<p>Given the labels, the user control creates the necessary number of equally-sized slices to build the wheel. Touch and mouse events are supported, and when the user completes manipulation of the wheel, the wheel animates back to the center of the selected slice.<\/p>\n<p>The rotary wheel control can be broken down into the following components:<\/p>\n<ul>\n<li>Individual slice<\/li>\n<li>Collection of slices making a wheel<\/li>\n<li>User manipulation to rotate wheel<\/li>\n<li>Animation of centering to the selected slice<\/li>\n<\/ul>\n<h3 id=\"slice\">Slice<\/h3>\n<p>In its basic form, the rotary wheel is comprised of an arbitrary number of equally sized slices. Leveraging <a href=\"http:\/\/blog.jerrynixon.com\/2012\/06\/windows-8-animated-pie-slice.html\">Jerry Nixon\u2019s<\/a> blog post, three arguments are required to build a slice:<\/p>\n<ul>\n<li><code class=\"highlighter-rouge\">StartAngle<\/code> &#8211; start angle of the slice<\/li>\n<li><code class=\"highlighter-rouge\">Angle<\/code> &#8211; total angle of the slice<\/li>\n<li><code class=\"highlighter-rouge\">Radius<\/code> &#8211; radius of pie slice<\/li>\n<\/ul>\n<p>In order to center the slice\u2019s label in the middle of the slice, the label requires two <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/windows.ui.xaml.uielement.rendertransform.aspx\">render transformations<\/a>:<\/p>\n<ol>\n<li><em>rotate<\/em> &#8211; align the label with the angle of the slice<\/li>\n<li><em>translate<\/em> &#8211; move the label to the center of the slice<\/li>\n<\/ol>\n<p>The resulting XAML of the slice is shown below:<\/p>\n<div class=\"highlighter-rouge\">\n<pre class=\"highlight\"><code>&lt;Grid x:Name=\"layoutRoot\"&gt;\r\n    &lt;!-- Slice Path (refer to Jerry Nixon post) --&gt;\r\n    &lt;userControl:PieSlicePath x:Name=\"pieSlicePath\" Canvas.ZIndex=\"1\" \/&gt;\r\n\r\n    &lt;!-- Label --&gt;\r\n    &lt;TextBlock x:Name=\"textBlock\" Canvas.ZIndex=\"2\" Text=\"{Binding Label}\" RenderTransformOrigin=\"0.5, 0.5\"&gt;\r\n        &lt;TextBlock.RenderTransform&gt;\r\n            &lt;TransformGroup&gt;\r\n                &lt;RotateTransform x:Name=\"textBlockRotate\" \/&gt;\r\n                &lt;TranslateTransform x:Name=\"textBlockTranslate\" \/&gt;\r\n            &lt;\/TransformGroup&gt;\r\n        &lt;\/TextBlock.RenderTransform&gt;\r\n    &lt;\/TextBlock&gt;\r\n&lt;\/Grid&gt;\r\n<\/code><\/pre>\n<\/div>\n<h4 id=\"rotate\">Rotate<\/h4>\n<p>Firstly, we configure the <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/windows\/apps\/windows.ui.xaml.uielement.rendertransformorigin\">RenderTransformOrigin<\/a> property of the textblock. <code class=\"highlighter-rouge\">RenderTransformOrigin<\/code> accepts a value between 0 and 1 and indicates the origin point of the <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/windows\/apps\/windows.ui.xaml.media.rotatetransform.aspx\">rotate transform<\/a>. As we wish to rotate based off the center of the textblock, the property is set to <code class=\"highlighter-rouge\">RenderTransformOrigin=\"0.5, 0.5\"<\/code>.<\/p>\n<p>Given the total angle of the slice is <code class=\"highlighter-rouge\">Angle<\/code>, to align the label with the slice, the rotate transform angle is <code class=\"highlighter-rouge\">StartAngle + Angle\/2<\/code>.<\/p>\n<h4 id=\"translate\">Translate<\/h4>\n<p>To appear centered in the middle of the slice, the label needs to be <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/windows\/apps\/windows.ui.xaml.media.translatetransform.aspx\">translated<\/a> approximately 4\/5 the slice radius.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/cse\/wp-content\/uploads\/sites\/55\/2020\/03\/2015-09-09-Rotary-Wheel-Control-with-HeavenFresh-label-center.png\" alt=\"diagram of pie slice with a centered label\" \/><\/p>\n<p>A closer look at the above diagram results in the following right-angle triangle:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/cse\/wp-content\/uploads\/sites\/55\/2020\/03\/2015-09-09-Rotary-Wheel-Control-with-HeavenFresh-triangle.png\" alt=\"trigonometry\" \/><\/p>\n<p>The sides of the triangle can be calculated through basic trigonometry.<\/p>\n<div class=\"highlighter-rouge\">\n<pre class=\"highlight\"><code>\/\/ where quadrant is an enumeration\r\n\/\/ NE = 0, SE = 1, SW = 2, NW = 4\r\nvar quadrantAngle = startAngle + angle\/2 - 90*(int)quadrant;\r\n\r\nvar adjacent = Math.Cos(Math.PI\/180* quadrantAngle) *radius;\r\nvar opposite = Math.Sin(Math.PI\/180* quadrantAngle) *radius;\r\n<\/code><\/pre>\n<\/div>\n<p>In the grid system, translations are relative to the top-left most point of the element (up = negative; down = positive; left = negative; right = positive). The below diagram displays the cartesian grid with (0,0) being the top-left most point of the element.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/cse\/wp-content\/uploads\/sites\/55\/2020\/03\/2015-09-09-Rotary-Wheel-Control-with-HeavenFresh-cartesian.png\" alt=\"cartesian grid\" \/><\/p>\n<p>As such, depending on which quadrant the slice is in, the polarities of the X,Y translation will vary.<\/p>\n<h3 id=\"wheel\">Wheel<\/h3>\n<p>The wheel is a collection of slices with differing <code class=\"highlighter-rouge\">StartAngle<\/code>. The <code class=\"highlighter-rouge\">Angle<\/code> of each slice remains constant and is calculated as <code class=\"highlighter-rouge\">360\/&lt;total number of slices&gt;<\/code>.<\/p>\n<div class=\"highlighter-rouge\">\n<pre class=\"highlight\"><code>var sliceLabels = new[] {'high', 'med', 'low'};\r\nvar sliceSize = 360\/sliceLabels.Count();\r\nforeach (var slice in sliceLabels)\r\n{\r\n    var pieSlice = new PieSlice\r\n    {\r\n        StartAngle = startAngle,\r\n        Angle = sliceSize,\r\n        ...\r\n    };\r\n\r\n    \/\/ add pie slice to canvas\r\n    _pieSlices.Add(pieSlice);\r\n    startAngle += sliceSize;\r\n}\r\n<\/code><\/pre>\n<\/div>\n<h3 id=\"user-manipulation\">User Manipulation<\/h3>\n<p>In order to support user manipulation of the wheel, the parent grid housing the pie chart is given a rotate transform in which the <code class=\"highlighter-rouge\">Angle<\/code> property will be updated during <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/windows\/apps\/windows.ui.xaml.uielement.manipulationdelta.aspx\">ManipulationDelta<\/a> events. During the callback, the angle in which to rotate the wheel is calculated based on the touch point.<\/p>\n<div class=\"highlighter-rouge\">\n<pre class=\"highlight\"><code>&lt;StackPanel x:Name=\"layoutRoot\"\r\n            ManipulationMode=\"All\"\r\n            ManipulationDelta=\"layoutRoot_ManipulationDelta\"\r\n            ManipulationCompleted=\"layoutRoot_ManipulationCompleted\"&gt;\r\n    &lt;Grid x:Name=\"layoutWheel\"&gt;\r\n        &lt;!-- slices are programmatically added here --&gt;\r\n\r\n        &lt;Grid.RenderTransform&gt;\r\n            &lt;RotateTransform x:Name=\"gridRotateTransform\" Angle=\"{Binding Angle}\" \/&gt;\r\n        &lt;\/Grid.RenderTransform&gt;\r\n    &lt;\/Grid&gt;\r\n&lt;\/StackPanel&gt;\r\n<\/code><\/pre>\n<\/div>\n<h3 id=\"animation\">Animation<\/h3>\n<p>As an added effect, upon the completion of the user interaction, the wheel will rotate itself to the center of the selected slice. Animations are accomplished through <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/windows\/apps\/windows.ui.xaml.media.animation.storyboard\">Storyboards<\/a>.<\/p>\n<p>Our <code class=\"highlighter-rouge\">Storyboard<\/code> will target the <code class=\"highlighter-rouge\">Angle<\/code> property of the <code class=\"highlighter-rouge\">gridRotateTransform<\/code> object. As the <code class=\"highlighter-rouge\">Angle<\/code> property is of type <code class=\"highlighter-rouge\">Double<\/code>, we apply a <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/system.windows.media.animation.doubleanimation(v=vs.95).aspx\">DoubleAnimation<\/a> to transition the property between two <code class=\"highlighter-rouge\">Double<\/code> values over a specified duration.<\/p>\n<div class=\"highlighter-rouge\">\n<pre class=\"highlight\"><code>&lt;UserControl.Resources&gt;\r\n    &lt;Storyboard x:Name=\"storyBoard\"&gt;\r\n        &lt;DoubleAnimation\r\n            x:Name=\"doubleAnimation\"\r\n            Storyboard.TargetName=\"gridRotateTransform\"\r\n            Storyboard.TargetProperty=\"(Angle)\"\r\n            Duration=\"0:0:0.5\"\/&gt;\r\n        &lt;\/Storyboard&gt;\r\n&lt;\/UserControl.Resources&gt;\r\n<\/code><\/pre>\n<\/div>\n<p>When the user finishes manipulation of the control, a <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/windows\/apps\/windows.ui.xaml.uielement.manipulationcompleted.aspx\">ManipulationCompleted<\/a> event is fired. During the event callback, we determine:<\/p>\n<ol>\n<li>the selected slice<\/li>\n<li>the configured angle of said selected slice, and<\/li>\n<li>start the storyboard to rotate the pie chart to the center of the selected slice.<\/li>\n<\/ol>\n<div class=\"highlighter-rouge\">\n<pre class=\"highlight\"><code>private void layoutRoot_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)\r\n{\r\n    var angleFromYAxis = 360 - Angle;\r\n    SelectedItem = _pieSlices.SingleOrDefault(p =&gt; p.StartAngle &lt;= angleFromYAxis &amp;&amp; (p.StartAngle + p.Angle) &gt; angleFromYAxis);\r\n\r\n    var finalAngle = SelectedItem.StartAngle + SelectedItem.Angle \/ 2;\r\n\r\n    doubleAnimation.From = Angle;\r\n    doubleAnimation.To = 360 - finalAngle;\r\n    storyBoard.Begin();\r\n\r\n    Angle = 360 - finalAngle;\r\n}\r\n<\/code><\/pre>\n<\/div>\n<h2 id=\"challenges\">Challenges<\/h2>\n<p>With UWP and its ability to deploy to various devices, developing a user interface element for varying form factors and differing viewing options is difficult. As such, emphasis was placed on using responsive layouts such as stack panels and hard-coded sizing of control widths\/lengths was refrained from.<\/p>\n<h2 id=\"opportunities-for-reuse\">Opportunities for Reuse<\/h2>\n<p>The rotary wheel user control is published on <a href=\"https:\/\/github.com\/jpoon\/RotaryWheel\">GitHub<\/a> and can be used for any UWP application. Additionally, the described approach of leveraging render transformations, manipulation events, and storyboards is applicable when building your own user control.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Building a UWP app with a rotary wheel user control, including the mechanics involved with drawing a rotary wheel, supporting user interaction and the usage of storyboards for animations. <\/p>\n","protected":false},"author":21365,"featured_media":12762,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[16],"tags":[35,199,364,370,383],"class_list":["post-2162","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops","tag-alljoyn","tag-heavenfresh","tag-ui","tag-universal-windows-platform-uwp","tag-web-development"],"acf":[],"blog_post_summary":"<p>Building a UWP app with a rotary wheel user control, including the mechanics involved with drawing a rotary wheel, supporting user interaction and the usage of storyboards for animations. <\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/2162","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/users\/21365"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/comments?post=2162"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/2162\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media\/12762"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media?parent=2162"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/categories?post=2162"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/tags?post=2162"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}