Support new features of Tizen.CircularUI (#188)
author유리나/Common Platform Lab(SR)/Staff Engineer/삼성전자 <rina6350.you@samsung.com>
Mon, 6 Apr 2020 05:52:06 +0000 (14:52 +0900)
committer안주원/Common Platform Lab(SR)/Principal Engineer/삼성전자 <juwon.ahn@samsung.com>
Mon, 6 Apr 2020 05:52:06 +0000 (14:52 +0900)
* Use Forms's GetHandler (#228)

* Fix ActionButton geometry calculation (#237)

* Fix the issue that text and bg color in PopupEntry is not changed while edit text (#239)

* Fix issue of unsupporting special character in Bottom button (#244)

* Fix issue that newline and ampersand are not supported in Information&TwoButton Popup (#250)

* Adds CircleSurfaceView (#258)

* Merge branch 'circular-shell' (#264)

* Add appium test for CircularShell (#241)

* Add Appium unit testcase

* Update Shell UI Tests (#236)

Co-authored-by: shmin <shmin.dev@gmail.com>
* Update Icon Images (#245)

* Update WearableUIGallery to use Shell (#252)

* Fix TwoButtonPage issue on shell (#255)

Co-authored-by: shyun.min <shyun.min@samsung.com>
Co-authored-by: Seungkeun Lee <sngn.lee@samsung.com>
Co-authored-by: shmin <shmin.dev@gmail.com>
Co-authored-by: shyun.min <shyun.min@samsung.com>
* Add new ContentButton (#263)

* Add new ContentButton

* Change Renderer inheritance and fix typo

* Restructure ContentButton and ContentButtonRenderer

* Fix layout issue

* Remove OnLayoutUpdate method

* Fix typo

* Support the CircularShell

This commit refers to these commits

* Update CircularShell (#229)

* Update X.Forms version to 4.4.0.991537 (#227)

* Add CircularShell (#226)

by Seungkeun Lee <sngn.lee@samsung.com>, shmin <shmin.dev@gmail.com>, Kangho Hur <rookiejava+github@gmail.com>

* Fixed the invalid resource stream

29 files changed:
src/XSF/Resources/wc_visual_cue.png [new file with mode: 0644]
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/CirclePageRenderer.cs
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/CircleSurfaceViewRenderer.cs [new file with mode: 0644]
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/ContentButtonRenderer.cs [new file with mode: 0644]
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/InformationPopupImplementation.cs
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/ListViewCache.cs
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/MediaPlayerImpl.cs
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/NativeCirclePage.cs
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/PopupEntryRenderer.cs
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/IShellItemRenderer.cs [new file with mode: 0644]
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/NavigationDrawer.cs [new file with mode: 0644]
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/NavigationView.cs [new file with mode: 0644]
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellContentRenderer.cs [new file with mode: 0644]
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellItemRenderer.cs [new file with mode: 0644]
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellRenderer.cs [new file with mode: 0644]
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellRendererFactory.cs [new file with mode: 0644]
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellSectionItemsRenderer.cs [new file with mode: 0644]
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellSectionNavigationRenderer.cs [new file with mode: 0644]
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/TizenCircleSurfaceEffect.cs
src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/TwoButtonPopupImplementation.cs
src/XSF/Tizen.Wearable.CircularUI.Forms/CirclePage.cs
src/XSF/Tizen.Wearable.CircularUI.Forms/CircleSurfaceView.cs [new file with mode: 0644]
src/XSF/Tizen.Wearable.CircularUI.Forms/CircularShell.cs [new file with mode: 0644]
src/XSF/Tizen.Wearable.CircularUI.Forms/ContentButton.cs [new file with mode: 0644]
src/XSF/XSF.csproj
src/XSF/Xamarin.Forms.Platform.Tizen/Native/EmbeddingControls.cs
src/XSF/Xamarin.Forms.Platform.Tizen/Renderers/RefreshViewRenderer.cs
src/XSF/Xamarin.Forms.Platform.Tizen/Shell/ShellItemRenderer.cs
src/XSF/Xamarin.Forms.Platform.Tizen/Shell/ShellNavBar.cs

diff --git a/src/XSF/Resources/wc_visual_cue.png b/src/XSF/Resources/wc_visual_cue.png
new file mode 100644 (file)
index 0000000..beddbb7
Binary files /dev/null and b/src/XSF/Resources/wc_visual_cue.png differ
index b5e368e..5b0753b 100644 (file)
@@ -39,6 +39,11 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer
 
                public ElmSharp.Wearable.CircleSurface CircleSurface;
 
+               public void UpdateRotaryFocusObject()
+               {
+                       _circlePage.UpdateRotaryFocusObject(Element.RotaryFocusObject);
+               }
+
                protected override void OnElementChanged(ElementChangedEventArgs<CirclePage> e)
                {
                        if (_circlePage == null)
@@ -93,6 +98,18 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer
                        }
                        base.OnElementChanged(e);
                }
+
+               protected override void OnElementReady()
+               {
+                       base.OnElementReady();
+                       // A Page created by with ContentTemplate of ShellContent, was appered before create a renderer
+                       // So need to call OnPageAppearing if page already appeared
+                       if (Element.Appeared)
+                       {
+                               OnPageAppearing(Element, EventArgs.Empty);
+                       }
+               }
+
                protected override void UpdateBackgroundColor(bool initialize)
                {
                        if (initialize && Element.BackgroundColor.IsDefault)
@@ -153,11 +170,6 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer
                        }
                }
 
-               void UpdateRotaryFocusObject()
-               {
-                       _circlePage.UpdateRotaryFocusObject(Element.RotaryFocusObject);
-               }
-
                void OnPageDisappearing(object sender, EventArgs e)
                {
                        _circlePage.DeactivateRotaryWidget();
diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/CircleSurfaceViewRenderer.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/CircleSurfaceViewRenderer.cs
new file mode 100644 (file)
index 0000000..55077b5
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2018 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ * Licensed under the Flora License, Version 1.1 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://floralicense.org/license/
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using ElmSharp;
+using ElmSharp.Wearable;
+using Tizen.Wearable.CircularUI.Forms;
+using Xamarin.Forms;
+using Xamarin.Forms.Platform.Tizen;
+using XForms = Xamarin.Forms.Forms;
+using ELayout = ElmSharp.Layout;
+
+[assembly: ExportRenderer(typeof(CircleSurfaceView), typeof(Tizen.Wearable.CircularUI.Forms.Renderer.CircleSurfaceViewRenderer))]
+namespace Tizen.Wearable.CircularUI.Forms.Renderer
+{
+    public class CircleSurfaceViewRenderer : ViewRenderer<CircleSurfaceView, Box>
+    {
+
+        Dictionary<ICircleSurfaceItem, ICircleWidget> _circleSurfaceItems;
+        ELayout _surfaceLayout;
+        CircleSurface _circleSurface;
+
+        protected override void OnElementChanged(ElementChangedEventArgs<CircleSurfaceView> e)
+        {
+            if (Control == null)
+            {
+                var box = new Box(XForms.NativeParent);
+                box.SetLayoutCallback(OnLayout);
+                _surfaceLayout = new ELayout(box);
+                _circleSurface = new CircleSurface(_surfaceLayout);
+                _circleSurfaceItems = new Dictionary<ICircleSurfaceItem, ICircleWidget>();
+                box.PackEnd(_surfaceLayout);
+                _surfaceLayout.Show();
+                SetNativeControl(box);
+            }
+
+            if (e.NewElement != null)
+            {
+                var items = e.NewElement.CircleSurfaceItems as ObservableCollection<ICircleSurfaceItem>;
+                items.CollectionChanged += OnCircleSurfaceItemsChanged;
+                foreach (var item in items)
+                {
+                    AddCircleSurfaceItem(item);
+                }
+            }
+
+            if (e.OldElement != null)
+            {
+                var items = e.OldElement.CircleSurfaceItems as ObservableCollection<ICircleSurfaceItem>;
+                foreach (var item in items)
+                {
+                    RemoveCircleSurfaceItem(item);
+                }
+                items.CollectionChanged -= OnCircleSurfaceItemsChanged;
+            }
+
+            base.OnElementChanged(e);
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            if (Element != null)
+            {
+                var items = Element.CircleSurfaceItems as ObservableCollection<ICircleSurfaceItem>;
+                foreach (var item in items)
+                {
+                    RemoveCircleSurfaceItem(item);
+                }
+                items.CollectionChanged -= OnCircleSurfaceItemsChanged;
+            }
+            base.Dispose(disposing);
+        }
+
+        void OnLayout()
+        {
+            var rect = Control.Geometry;
+            Element.Layout(rect.ToDP());
+            _surfaceLayout.Geometry = rect;
+        }
+
+        void OnCircleSurfaceItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            if (e.Action == NotifyCollectionChangedAction.Add ||
+                e.Action == NotifyCollectionChangedAction.Replace)
+            {
+                foreach (ICircleSurfaceItem item in e.NewItems)
+                    AddCircleSurfaceItem(item);
+            }
+            if (e.Action == NotifyCollectionChangedAction.Remove ||
+                e.Action == NotifyCollectionChangedAction.Replace)
+            {
+                foreach (ICircleSurfaceItem item in e.OldItems)
+                    RemoveCircleSurfaceItem(item);
+            }
+        }
+
+        void AddCircleSurfaceItem(ICircleSurfaceItem item)
+        {
+            if (item is CircleProgressBarSurfaceItem)
+            {
+                var widget = new CircleProgressBarSurfaceItemImplements(item as CircleProgressBarSurfaceItem, _surfaceLayout, _circleSurface);
+                _circleSurfaceItems[item] = widget;
+            }
+            else if (item is CircleSliderSurfaceItem)
+            {
+                var widget = new CircleSliderSurfaceItemImplements(item as CircleSliderSurfaceItem, _surfaceLayout, _circleSurface);
+                _circleSurfaceItems[item] = widget;
+            }
+        }
+
+        void RemoveCircleSurfaceItem(ICircleSurfaceItem item)
+        {
+            if (_circleSurfaceItems.TryGetValue(item, out var widget))
+            {
+                EvasObject obj = widget as EvasObject;
+                obj?.Unrealize();
+                _circleSurfaceItems.Remove(item);
+            }
+        }
+    }
+}
diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/ContentButtonRenderer.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/ContentButtonRenderer.cs
new file mode 100644 (file)
index 0000000..593e7fd
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2018 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ * Licensed under the Flora License, Version 1.1 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://floralicense.org/license/
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+using Tizen.Wearable.CircularUI.Forms;
+using Tizen.Wearable.CircularUI.Forms.Renderer;
+using Xamarin.Forms;
+using Xamarin.Forms.Platform.Tizen;
+using Xamarin.Forms.Platform.Tizen.Native;
+using XForms = Xamarin.Forms.Forms;
+using XFLayout = Xamarin.Forms.Layout;
+using EButton = ElmSharp.Button;
+using EColor = ElmSharp.Color;
+using System.ComponentModel;
+
+[assembly: ExportRenderer(typeof(ContentButton), typeof(ContentButtonRenderer))]
+
+namespace Tizen.Wearable.CircularUI.Forms.Renderer
+{
+    public class ContentButtonRenderer : LayoutRenderer
+    {
+        EButton _button;
+
+        ContentButton Button => Element as ContentButton;
+
+        protected override void OnElementChanged(ElementChangedEventArgs<XFLayout> e)
+        {
+            base.OnElementChanged(e);
+            Initialize();
+        }
+
+        void Initialize()
+        {
+            if (_button == null)
+            {
+                _button = new EButton(XForms.NativeParent);
+                _button.BackgroundColor = EColor.Transparent;
+                _button.SetPartColor("effect", EColor.Transparent);
+                _button.SetPartColor("effect_pressed", EColor.Transparent);
+                _button.Show();
+
+                _button.Pressed += OnPressed;
+                _button.Released += OnReleased;
+                _button.Clicked += OnClicked;
+
+                Control.PackEnd(_button);
+            }
+        }
+
+        protected override void UpdateLayout()
+        {
+            base.UpdateLayout();
+
+            _button.Geometry = Control.Geometry;
+            _button.RaiseTop();
+        }
+
+        void OnPressed(object sender, EventArgs args)
+        {
+            Button?.SendPressed();
+        }
+
+        void OnReleased(object sender, EventArgs args)
+        {
+            Button?.SendReleased();
+        }
+
+        void OnClicked(object sender, EventArgs args)
+        {
+            Button?.SendClicked();
+        }
+    }
+}
index 87b39d3..d867e35 100644 (file)
@@ -85,7 +85,7 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer
                                        _box = null;
                                }
 
-                               if(_progress != null)
+                               if (_progress != null)
                                {
                                        _progress.Unrealize();
                                        _progress = null;
@@ -181,7 +181,7 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer
 
                void UpdateProcessVisibility()
                {
-                       if(!XForms.IsInitialized)
+                       if (!XForms.IsInitialized)
                        {
                                Log.Debug(FormsCircularUI.Tag, "Tizen Forms is not initialized");
                                return;
@@ -241,7 +241,7 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer
                                        Style = "bottom"
                                };
 
-                               if (!string.IsNullOrEmpty(BottomButton.Text))_bottomButton.Text = BottomButton.Text;
+                               if (!string.IsNullOrEmpty(BottomButton.Text)) _bottomButton.Text = BottomButton.Text;
 
                                if (!BottomButton.IconImageSource.IsNullOrEmpty())
                                {
@@ -274,9 +274,14 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer
 
                void UpdateTitle()
                {
+                       string title = _title?.Replace("&", "&amp;")
+                                                                 .Replace("<", "&lt;")
+                                                                 .Replace(">", "&gt;")
+                                                                 .Replace(Environment.NewLine, "<br>");
+
                        if (!_isProgressRunning)
                        {
-                               _layout.SetPartText("elm.text.title", _title);
+                               _layout.SetPartText("elm.text.title", title);
                        }
                        else
                        {
@@ -286,14 +291,19 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer
 
                void UpdateText()
                {
+                       string text = _text?.Replace("&", "&amp;")
+                                                               .Replace("<", "&lt;")
+                                                               .Replace(">", "&gt;")
+                                                               .Replace(Environment.NewLine, "<br>");
+
                        if (!_isProgressRunning)
                        {
-                               _layout.SetPartText("elm.text", _text);
+                               _layout.SetPartText("elm.text", text);
                        }
                        else
                        {
                                _layout.SetPartText("elm.text", null);
-                               if (!string.IsNullOrEmpty(_text))
+                               if (!string.IsNullOrEmpty(text))
                                {
                                        if (_progressLabel == null)
                                        {
@@ -302,7 +312,7 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer
                                                        TextStyle = "DEFAULT ='font=Tizen:style=Light color=#F9F9F9FF font_size=32 align=center valign=top wrap=word'",
                                                };
                                        }
-                                       _progressLabel.Text = _text;
+                                       _progressLabel.Text = text;
                                        _progressLabel.Show();
                                        if (_box != null)
                                        {
index 98b6f20..bb64770 100644 (file)
@@ -19,7 +19,6 @@ using System.Collections.Generic;
 using ElmSharp;
 using Xamarin.Forms;
 using Xamarin.Forms.Platform.Tizen;
-using Xamarin.Forms.Internals;
 using XForms = Xamarin.Forms.Forms;
 
 namespace Tizen.Wearable.CircularUI.Forms.Renderer
index 056c8d7..1ec4e05 100644 (file)
@@ -21,8 +21,8 @@ using System.Threading.Tasks;
 using Tizen.Multimedia;
 using Tizen.Wearable.CircularUI.Forms.Renderer;
 using Xamarin.Forms;
-using Xamarin.Forms.Internals;
 using Xamarin.Forms.Platform.Tizen;
+using XForms = Xamarin.Forms.Forms;
 
 [assembly: Xamarin.Forms.Dependency(typeof(MediaPlayerImpl))]
 namespace Tizen.Wearable.CircularUI.Forms.Renderer
@@ -270,7 +270,7 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer
                        {
                                return;
                        }
-                       IMediaSourceHandler handler = global::Xamarin.Forms.Internals.Registrar.Registered.GetHandlerForObject<IMediaSourceHandler>(_source);
+                       IMediaSourceHandler handler = XForms.GetHandlerForObject<IMediaSourceHandler>(_source);
                        await handler.SetSource(_player, _source);
                }
 
@@ -302,7 +302,8 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer
                        ApplyDisplay();
                        await ApplySource();
 
-                       try {
+                       try
+                       {
                                await _player.PrepareAsync();
                                UpdateStreamInfo?.Invoke(this, EventArgs.Empty);
                                _tcsForStreamInfo?.TrySetResult(true);
index 96aff05..ba681de 100644 (file)
@@ -249,7 +249,10 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer
                                SetVisibleActionButton(ActionButton.IsVisible);
 
                                ActionButton.PropertyChanged += OnActionButtonItemChanged;
-                               _actionButton.Text = ActionButton.Text;
+                               _actionButton.Text = ActionButton.Text?.Replace("&", "&amp;")
+                                                  .Replace("<", "&lt;")
+                                                  .Replace(">", "&gt;")
+                                                  .Replace(Environment.NewLine, "<br>");
                                _actionButton.IsEnabled = ActionButton.IsEnable;
                                if (!ActionButton.IconImageSource.IsNullOrEmpty())
                                {
@@ -260,11 +263,12 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer
                                        buttonImage.Show();
                                        _actionButton.SetPartContent("elm.swallow.content", buttonImage);
                                }
-
-                               if (ActionButton.BackgroundColor != Xamarin.Forms.Color.Default)
+                               else
                                {
-                                       _actionButton.BackgroundColor = ActionButton.BackgroundColor.ToNative();
+                                       _actionButton.SetPartContent("elm.swallow.content", null);
                                }
+
+                               _actionButton.BackgroundColor = ActionButton.BackgroundColor.ToNative();
                        }
                        else
                        {
@@ -310,7 +314,7 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer
                                var btnW = Math.Max(_actionButton.MinimumWidth, btnRect.Width);
                                var btnH = Math.Max(_actionButton.MinimumHeight, btnRect.Height);
                                var btnX = rect.X + (rect.Width - btnW) / 2;
-                               var btnY = rect.Height - btnH;
+                               var btnY = rect.Y + rect.Height - btnH;
                                _actionButton.Geometry = new Rect(btnX, btnY, btnW, btnH);
                                _actionButton.StackAbove(prev);
                                prev = _actionButton;
index f7d0e63..6e4b6cf 100644 (file)
 
 using ElmSharp;
 using System;
+using System.ComponentModel;
 using Xamarin.Forms;
 using Xamarin.Forms.Platform.Tizen;
 using Xamarin.Forms.Platform.Tizen.Native;
 using EColor = ElmSharp.Color;
 using ELayout = ElmSharp.Layout;
 using XForms = Xamarin.Forms.Forms;
+using XEntry = Xamarin.Forms.Entry;
 
 [assembly: ExportRenderer(typeof(Tizen.Wearable.CircularUI.Forms.PopupEntry), typeof(Tizen.Wearable.CircularUI.Forms.Renderer.PopupEntryRenderer))]
 
@@ -64,6 +66,21 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer
                        }
                }
 
+               protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
+               {
+                       base.OnElementPropertyChanged(sender, e);
+                       if (_IsPopupOpened && _editor != null)
+                       {
+                               if (e.PropertyName == XEntry.TextColorProperty.PropertyName ||
+                                       e.PropertyName == XEntry.FontSizeProperty.PropertyName ||
+                                       e.PropertyName == XEntry.FontFamilyProperty.PropertyName ||
+                                       e.PropertyName == XEntry.FontAttributesProperty.PropertyName)
+                               {
+                                       _editor.TextStyle = Control.TextStyle;
+                               }
+                       }
+               }
+
                protected override void Dispose(bool disposing)
                {
                        if (disposing)
@@ -213,6 +230,11 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer
                        {
                                _popupBackgroundColor = bgColor.ToNative();
                        }
+
+                       if (_IsPopupOpened == true && _editorPopup != null)
+                       {
+                               _editorPopup.Color = _popupBackgroundColor;
+                       }
                }
 
                void OnTextChanged(object sender, EventArgs e)
diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/IShellItemRenderer.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/IShellItemRenderer.cs
new file mode 100644 (file)
index 0000000..bdeaa59
--- /dev/null
@@ -0,0 +1,12 @@
+using System;
+using ElmSharp;
+using Xamarin.Forms;
+
+namespace Tizen.Wearable.CircularUI.Forms.Renderer
+{
+    public interface IShellItemRenderer : IDisposable
+    {
+        BaseShellItem Item { get; }
+        EvasObject NativeView { get; }
+    }
+}
diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/NavigationDrawer.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/NavigationDrawer.cs
new file mode 100644 (file)
index 0000000..3e0cb46
--- /dev/null
@@ -0,0 +1,542 @@
+using ElmSharp;
+using ElmSharp.Wearable;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Xamarin.Forms;
+using Xamarin.Forms.Platform.Tizen;
+using EColor = ElmSharp.Color;
+using EImage = ElmSharp.Image;
+using ELayout = ElmSharp.Layout;
+using EWidget = ElmSharp.Widget;
+using EButton = ElmSharp.Button;
+
+namespace Tizen.Wearable.CircularUI.Forms.Renderer
+{
+    public class NavigationDrawer : ELayout, IAnimatable
+    {
+        static readonly int TouchWidth = 50;
+        static readonly int IconSize = 40;
+        static readonly string DefaultIcon = "Tizen.Wearable.CircularUI.Forms.Renderer.res.wc_visual_cue.png";
+
+        Box _mainLayout;
+        Box _contentGestureBox;
+        Box _contentBox;
+        Box _drawerBox;
+        Box _drawerContentBox;
+        Box _drawerIconBox;
+
+        EvasObject _content;
+        EvasObject _drawerContent;
+
+        EImage _drawerIcon;
+        EButton _touchArea;
+
+        GestureLayer _gestureOnContent;
+        GestureLayer _gestureOnDrawer;
+
+        ImageSource _drawerIconSource;
+
+        bool _isOpen;
+        bool _isDefaultIcon;
+
+        CancellationTokenSource _fadeInCancelTokenSource = null;
+
+        bool HasDrawer => _drawerBox != null;
+
+        public NavigationDrawer(EvasObject parent) : base(parent)
+        {
+            Initialize();
+        }
+
+        public int HandlerHeight { get; set; } = 40;
+
+        public bool IsOpen
+        {
+            get
+            {
+                return _isOpen;
+            }
+            set
+            {
+                if (_isOpen != value)
+                {
+                    if (value)
+                    {
+                        Open();
+                    }
+                    else
+                    {
+                        Close();
+                    }
+                }
+            }
+        }
+
+        EColor _handlerBackgroundColor = EColor.Transparent;
+        public EColor HandlerBackgroundColor
+        {
+            get => _handlerBackgroundColor;
+            set
+            {
+                _handlerBackgroundColor = value;
+                UpdateHandlerBackgroundColor();
+            }
+        }
+
+        public event EventHandler Toggled;
+
+        public void SetMainContent(EvasObject content)
+        {
+            if (content == null)
+            {
+                UnsetMainContent();
+                return;
+            }
+
+            _content = content;
+            _content.Show();
+            _contentBox.PackEnd(_content);
+            _content.Geometry = _contentBox.Geometry;
+        }
+
+        public void SetDrawerContent(EvasObject content)
+        {
+            InitializeDrawerBox();
+
+            if (content == null)
+            {
+                UnsetDrawerContent();
+                return;
+            }
+
+            _drawerContent = content;
+            _drawerContent.Show();
+            _drawerContentBox.PackEnd(_drawerContent);
+
+            _drawerContentBox.Show();
+            _drawerIconBox.Show();
+
+            if (_drawerContent is NavigationView nv)
+            {
+                nv.Dragged += (s, e) =>
+                {
+                    if (e.State == DraggedState.EdgeTop)
+                    {
+                        Close();
+                    }
+                };
+            }
+        }
+
+        public void UpdateDrawerIcon(ImageSource source)
+        {
+            _drawerIconSource = source;
+            if (HasDrawer)
+            {
+                SetDrawerIcon(_drawerIconSource);
+            }
+        }
+
+        public async void Open(uint length = 300)
+        {
+            if (!HasDrawer)
+                return;
+
+            var toMove = _drawerBox.Geometry;
+            toMove.Y = 0;
+
+            await RunMoveAnimation(_drawerBox, toMove, length);
+
+            if (!_isOpen)
+            {
+                _isOpen = true;
+                Toggled?.Invoke(this, EventArgs.Empty);
+            }
+            OnLayout();
+            OnDrawerLayout();
+            FlipIcon();
+        }
+
+        public async void Close(uint length = 300)
+        {
+            if (!HasDrawer)
+                return;
+
+            var toMove = _drawerBox.Geometry;
+            toMove.Y = Geometry.Height - HandlerHeight;
+
+            await RunMoveAnimation(_drawerBox, toMove, length);
+
+            if (_isOpen)
+            {
+                _isOpen = false;
+                Toggled?.Invoke(this, EventArgs.Empty);
+            }
+            OnLayout();
+            OnDrawerLayout();
+            ResetIcon();
+            StartHighlightAnimation(_drawerIcon);
+        }
+
+        void IAnimatable.BatchBegin()
+        {
+        }
+
+        void IAnimatable.BatchCommit()
+        {
+        }
+
+        protected override IntPtr CreateHandle(EvasObject parent)
+        {
+            _mainLayout = new Box(parent);
+            return _mainLayout.Handle;
+        }
+
+        void Initialize()
+        {
+            _mainLayout.SetLayoutCallback(OnLayout);
+
+            _contentGestureBox = new Box(_mainLayout);
+            _contentGestureBox.Show();
+            _mainLayout.PackEnd(_contentGestureBox);
+
+            _contentBox = new Box(_mainLayout);
+            _contentBox.SetLayoutCallback(OnContentLayout);
+            _contentBox.Show();
+            _mainLayout.PackEnd(_contentBox);
+        }
+
+        void InitializeDrawerBox()
+        {
+            if (_drawerBox != null)
+                return;
+
+            _drawerBox = new Box(_mainLayout);
+            _drawerBox.SetLayoutCallback(OnDrawerLayout);
+            _drawerBox.Show();
+            _mainLayout.PackEnd(_drawerBox);
+
+            _drawerContentBox = new Box(_drawerBox);
+            _drawerBox.PackEnd(_drawerContentBox);
+
+            _drawerIconBox = new Box(_drawerBox)
+            {
+                BackgroundColor = _handlerBackgroundColor
+            };
+            _drawerBox.PackEnd(_drawerIconBox);
+
+            _drawerIcon = new EImage(_drawerIconBox)
+            {
+                AlignmentY = 0.5,
+                AlignmentX = 0.5,
+                MinimumHeight = IconSize,
+                MinimumWidth = IconSize,
+            };
+            _drawerIcon.Show();
+            _drawerIconBox.PackEnd(_drawerIcon);
+            SetDrawerIcon(_drawerIconSource);
+
+            _touchArea = new EButton(_drawerBox)
+            {
+                Color = EColor.Transparent,
+                BackgroundColor = EColor.Transparent,
+            };
+            _touchArea.SetPartColor("effect", EColor.Transparent);
+            _touchArea.Show();
+            _touchArea.RepeatEvents = true;
+            _touchArea.Clicked += OnIconClicked;
+
+            _drawerBox.PackEnd(_touchArea);
+
+            _gestureOnContent = new GestureLayer(_contentGestureBox);
+            _gestureOnContent.SetMomentumCallback(GestureLayer.GestureState.Start, OnContentDragStarted);
+            _gestureOnContent.SetMomentumCallback(GestureLayer.GestureState.End, OnContentDragEnded);
+            _gestureOnContent.SetMomentumCallback(GestureLayer.GestureState.Abort, OnContentDragEnded);
+            _gestureOnContent.Attach(_contentGestureBox);
+            _contentBox.RepeatEvents = true;
+
+            _gestureOnDrawer = new GestureLayer(_drawerIconBox);
+            _gestureOnDrawer.SetMomentumCallback(GestureLayer.GestureState.Move, OnDrawerDragged);
+            _gestureOnDrawer.SetMomentumCallback(GestureLayer.GestureState.End, OnDrawerDragEnded);
+            _gestureOnDrawer.SetMomentumCallback(GestureLayer.GestureState.Abort, OnDrawerDragEnded);
+            _gestureOnDrawer.Attach(_drawerIconBox);
+
+            RotaryEventManager.Rotated += OnRotateEventReceived;
+        }
+
+        void SetDrawerIcon(ImageSource source)
+        {
+            if (source == null)
+            {
+                _drawerIcon.LoadFromImageSourceAsync(ImageSource.FromResource(DefaultIcon, GetType().Assembly));
+                _isDefaultIcon = true;
+            }
+            else
+            {
+                _isDefaultIcon = false;
+                if (source is FileImageSource fsource)
+                {
+                    _drawerIcon.Load(fsource.ToAbsPath());
+                }
+                else
+                {
+                    _drawerIcon.LoadFromImageSourceAsync(source);
+                }
+            }
+        }
+
+        void UpdateHandlerBackgroundColor()
+        {
+            if (_drawerIconBox != null)
+            {
+                _drawerIconBox.BackgroundColor = _handlerBackgroundColor;
+            }
+        }
+
+        void OnIconClicked(object sender, EventArgs e)
+        {
+            if (IsOpen)
+                Close();
+            else
+                Open();
+        }
+
+        async Task<bool> ShowAsync(EWidget target, Easing easing = null, uint length = 300, CancellationToken cancelltaionToken = default(CancellationToken))
+        {
+            var tcs = new TaskCompletionSource<bool>();
+
+            await Task.Delay(1000);
+
+            if (cancelltaionToken.IsCancellationRequested)
+            {
+                cancelltaionToken.ThrowIfCancellationRequested();
+            }
+
+            target.Show();
+            var opacity = target.Opacity;
+
+            if (opacity == 255 || opacity == -1)
+                return true;
+
+            new Animation((progress) =>
+            {
+                target.Opacity = opacity + (int)((255 - opacity) * progress);
+
+            }).Commit(this, "FadeIn", length: length, finished: (p, e) =>
+            {
+                target.Opacity = 255;
+                tcs.SetResult(true);
+                StartHighlightAnimation(_drawerIcon);
+            });
+
+            return await tcs.Task;
+        }
+
+        void OnLayout()
+        {
+            var bound = Geometry;
+            _contentGestureBox.Geometry = bound;
+            _contentBox.Geometry = bound;
+            if (_drawerBox != null)
+            {
+                bound.Y = _isOpen ? 0 : (bound.Height - HandlerHeight);
+                _drawerBox.Geometry = bound;
+            }
+        }
+
+        void OnContentLayout()
+        {
+            if (_content != null)
+            {
+                _content.Geometry = _contentBox.Geometry;
+            }
+        }
+
+        void OnDrawerLayout()
+        {
+            this.AbortAnimation("HighlightAnimation");
+
+            var bound = _drawerBox.Geometry;
+
+            var currentY = bound.Y;
+            var ratio = currentY / (double)(Geometry.Height - HandlerHeight);
+
+            var contentBound = bound;
+            contentBound.Y += (int)(HandlerHeight * ratio);
+            _drawerContentBox.Geometry = contentBound;
+
+            var drawerHandleBound = bound;
+            drawerHandleBound.Height = HandlerHeight;
+            _drawerIconBox.Geometry = drawerHandleBound;
+
+            var drawerTouchBound = drawerHandleBound;
+            drawerTouchBound.Width = TouchWidth;
+            drawerTouchBound.X = drawerHandleBound.X + (drawerHandleBound.Width - TouchWidth) / 2;
+            _touchArea.Geometry = drawerTouchBound;
+        }
+
+        async Task<bool> HideAsync(EWidget target, Easing easing = null, uint length = 300)
+        {
+            var tcs = new TaskCompletionSource<bool>();
+
+            var opacity = target.Opacity;
+            if (opacity == -1)
+                opacity = 255;
+
+            new Animation((progress) =>
+            {
+                target.Opacity = opacity - (int)(progress * opacity);
+
+            }).Commit(this, "FadeOut", length: length, finished: (p, e) =>
+            {
+                target.Opacity = 0;
+                target.Hide();
+                tcs.SetResult(true);
+            });
+
+            return await tcs.Task;
+        }
+
+        void StartHighlightAnimation(EWidget target)
+        {
+            if (!_isDefaultIcon || this.AnimationIsRunning("HighlightAnimation"))
+                return;
+
+            int count = 2;
+            var bound = target.Geometry;
+            var y = bound.Y;
+            var dy = bound.Y - bound.Height / 3;
+
+            var anim = new Animation();
+
+            var transfAnim = new Animation((f) =>
+            {
+                bound.Y = (int)f;
+                var map = new EvasMap(4);
+                map.PopulatePoints(bound, 0);
+                target.IsMapEnabled = true;
+                target.EvasMap = map;
+            }, y, dy);
+
+            var opacityAnim = new Animation(f => target.Opacity = (int)f, 255, 40);
+
+            anim.Add(0, 1, opacityAnim);
+            anim.Add(0, 1, transfAnim);
+
+            anim.Commit(this, "HighlightAnimation", 16, 800, finished: (f, b) =>
+            {
+                target.Opacity = 255;
+                target.IsMapEnabled = false;
+            }, repeat:() => --count > 0);
+        }
+
+        async void OnRotateEventReceived(EventArgs args)
+        {
+            _fadeInCancelTokenSource?.Cancel();
+            _fadeInCancelTokenSource = new CancellationTokenSource();
+
+            if (!_isOpen)
+            {
+                var token = _fadeInCancelTokenSource.Token;
+                await HideAsync(_drawerBox);
+                _ = ShowAsync(_drawerBox, cancelltaionToken: token);
+            }
+        }
+
+        void OnContentDragStarted(GestureLayer.MomentumData moment)
+        {
+            _fadeInCancelTokenSource?.Cancel();
+            _fadeInCancelTokenSource = null;
+
+            if (!_isOpen)
+            {
+                _ = HideAsync(_drawerBox);
+            }
+        }
+
+        void OnContentDragEnded(GestureLayer.MomentumData moment)
+        {
+            _fadeInCancelTokenSource = new CancellationTokenSource();
+            _ = ShowAsync(_drawerBox, cancelltaionToken: _fadeInCancelTokenSource.Token);
+        }
+
+        void OnDrawerDragged(GestureLayer.MomentumData moment)
+        {
+            var toMove = _drawerBox.Geometry;
+            toMove.Y = (moment.Y2 < 0) ? 0 : moment.Y2;
+            _drawerBox.Geometry = toMove;
+            OnDrawerLayout();
+        }
+
+        void OnDrawerDragEnded(GestureLayer.MomentumData moment)
+        {
+            if (_drawerBox.Geometry.Y < (_mainLayout.Geometry.Height / 2))
+            {
+                Open();
+            }
+            else
+            {
+                Close();
+            }
+        }
+
+        void FlipIcon()
+        {
+            if (_isDefaultIcon)
+            {
+                _drawerIcon.Orientation = ImageOrientation.FlipVertical;
+            }
+        }
+
+        void ResetIcon()
+        {
+            _drawerIcon.Orientation = ImageOrientation.None;
+        }
+
+        Task RunMoveAnimation(EvasObject target, Rect dest, uint length, Easing easing = null)
+        {
+            var tcs = new TaskCompletionSource<bool>();
+
+            var dx = target.Geometry.X - dest.X;
+            var dy = target.Geometry.Y - dest.Y;
+
+            new Animation((progress) =>
+            {
+                var toMove = dest;
+                toMove.X += (int)(dx * (1 - progress));
+                toMove.Y += (int)(dy * (1 - progress));
+                target.Geometry = toMove;
+                OnDrawerLayout();
+            }).Commit(this, "Move", length: length, finished: (s, e) =>
+            {
+                target.Geometry = dest;
+                tcs.SetResult(true);
+            });
+            return tcs.Task;
+        }
+
+        void UnsetMainContent()
+        {
+            if (_content != null)
+            {
+                _contentBox.UnPack(_content);
+                _content.Hide();
+                _content = null;
+            }
+        }
+
+        void UnsetDrawerContent()
+        {
+            if (_drawerContent != null)
+            {
+                _drawerContentBox.UnPack(_drawerContent);
+                _drawerContent.Hide();
+                _drawerContent = null;
+
+                _drawerContentBox.Hide();
+                _drawerIconBox.Hide();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/NavigationView.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/NavigationView.cs
new file mode 100644 (file)
index 0000000..aecddf4
--- /dev/null
@@ -0,0 +1,296 @@
+using ElmSharp;
+using ElmSharp.Wearable;
+using System;
+using System.Collections.Generic;
+using Xamarin.Forms;
+using Xamarin.Forms.Platform.Tizen;
+using ELayout = ElmSharp.Layout;
+using EColor = ElmSharp.Color;
+
+namespace Tizen.Wearable.CircularUI.Forms.Renderer
+{
+    public class NavigationView : ELayout
+    {
+        readonly int _dafaultIconSize = 60;
+
+        class Item
+        {
+            public Element Source { get; set; } 
+            public string Text { get; set; }
+            public string Icon { get; set; }
+        }
+
+        Box _outterBox;
+        ELayout _surfaceLayout;
+        CircleSurface _surface;
+        CircleGenList _naviMenu;
+
+        GenItemClass _defaultClass;
+        SmartEvent _draggedUpCallback;
+        SmartEvent _draggedDownCallback;
+
+        GenListItem _header;
+        GenListItem _footer;
+
+        List<List<Element>> _itemCache;
+        List<GenListItem> _items = new List<GenListItem>();
+
+        public NavigationView(EvasObject parent) : base(parent)
+        {
+            InitializeComponent();
+        }
+
+        public event EventHandler<SelectedItemChangedEventArgs> ItemSelected;
+
+        public event EventHandler<DraggedEventArgs> Dragged;
+
+
+        EColor _backgroundColor = EColor.Black;
+        public override EColor BackgroundColor
+        {
+            get => _backgroundColor;
+            set
+            {
+                _backgroundColor = value.IsDefault ? EColor.Black : value;
+                UpdateBackgroundColor();
+            }
+        }
+
+        EColor _foregroundColor = EColor.Default;
+        public EColor ForegroundColor
+        {
+            get => _foregroundColor;
+            set
+            {
+                _foregroundColor = value;
+                UpdateForegroundColor();
+            }
+        }
+
+
+        public void Build(List<List<Element>> items)
+        {
+            // Only update when items was changed
+            if (!IsUpdated(items))
+            {
+                return;
+            }
+            _itemCache = items;
+
+            _naviMenu.Clear();
+            _items.Clear();
+            // header
+            _header = _naviMenu.Append(_defaultClass, new Item { Text = "" });
+
+            // TODO. need to improve, need to support group
+            foreach (var group in items)
+            {
+                foreach (var item in group)
+                {
+                    var data = new Item
+                    {
+                        Source = item
+                    };
+                    if (item is BaseShellItem shellItem)
+                    {
+                        data.Text = shellItem.Title;
+                        data.Icon = (shellItem.Icon as FileImageSource)?.ToAbsPath();
+                    }
+                    else if (item is MenuItem menuItem)
+                    {
+                        data.Text = menuItem.Text;
+                        data.Icon = (menuItem.IconImageSource as FileImageSource)?.ToAbsPath();
+                    }
+                    var genitem = _naviMenu.Append(_defaultClass, data, GenListItemType.Normal);
+                    genitem.SetPartColor("bg", _backgroundColor);
+                    _items.Add(genitem);
+                }
+            }
+            _footer = _naviMenu.Append(_defaultClass, new Item { Text = "" });
+        }
+
+        public void Activate()
+        {
+            (_naviMenu as IRotaryActionWidget)?.Activate();
+        }
+        public void Deactivate()
+        {
+            (_naviMenu as IRotaryActionWidget)?.Deactivate();
+        }
+
+        protected override IntPtr CreateHandle(EvasObject parent)
+        {
+            _outterBox = new Box(parent);
+            return _outterBox.Handle;
+        }
+
+        void InitializeComponent()
+        {
+            _outterBox.SetLayoutCallback(OnLayout);
+
+            _surfaceLayout = new ELayout(this);
+            _surfaceLayout.Show();
+            _surface = new CircleSurface(_surfaceLayout);
+
+            _naviMenu = new CircleGenList(this, _surface)
+            {
+                Homogeneous = true,
+                BackgroundColor = _backgroundColor
+            };
+            _naviMenu.Show();
+
+            _draggedUpCallback = new SmartEvent(_naviMenu, "drag,start,up");
+            _draggedUpCallback.On += (s, e) =>
+            {
+                if (_footer.TrackObject.IsVisible)
+                {
+                    Dragged?.Invoke(this, new DraggedEventArgs(DraggedState.EdgeBottom));
+                }
+                else
+                {
+                    Dragged?.Invoke(this, new DraggedEventArgs(DraggedState.Up));
+                }
+            };
+
+            _draggedDownCallback = new SmartEvent(_naviMenu, "drag,start,down");
+            _draggedDownCallback.On += (s, e) =>
+            {
+                if (_header.TrackObject.IsVisible)
+                {
+                    Dragged?.Invoke(this, new DraggedEventArgs(DraggedState.EdgeTop));
+                }
+                else
+                {
+                    Dragged?.Invoke(this, new DraggedEventArgs(DraggedState.Down));
+                }
+            };
+
+            _outterBox.PackEnd(_naviMenu);
+            _outterBox.PackEnd(_surfaceLayout);
+
+            _surfaceLayout.StackAbove(_naviMenu);
+
+            _defaultClass = new GenItemClass("1icon_1text")
+            {
+                GetTextHandler = (obj, part) =>
+                {
+                    if (part == "elm.text")
+                    {
+                        var text = (obj as Item).Text;
+                        if (_foregroundColor != EColor.Default)
+                            return $"<span color='{_foregroundColor.ToHex()}'>{text}</span>";
+                        else
+                            return text;
+                    }
+                    return null;
+                },
+                GetContentHandler = (obj, part) =>
+                {
+                    if (part == "elm.swallow.icon" && obj is Item menuItem && !string.IsNullOrEmpty(menuItem.Icon))
+                    {
+                        var icon = new ElmSharp.Image(Xamarin.Forms.Forms.NativeParent)
+                        {
+                            AlignmentX = -1,
+                            AlignmentY = -1,
+                            WeightX = 1.0,
+                            WeightY = 1.0,
+                            MinimumWidth = _dafaultIconSize,
+                            MinimumHeight = _dafaultIconSize,
+                        };
+                        icon.Show();
+                        icon.Load(menuItem.Icon);
+                        return icon;
+                    }
+                    return null;
+                }
+            };
+
+            _naviMenu.ItemSelected += OnItemSelected;
+
+        }
+
+        void OnItemSelected(object sender, GenListItemEventArgs e)
+        {
+            ItemSelected?.Invoke(this, new SelectedItemChangedEventArgs((e.Item.Data as Item).Source, -1));
+        }
+
+        void OnLayout()
+        {
+            _surfaceLayout.Geometry = Geometry;
+            _naviMenu.Geometry = Geometry;
+        }
+
+        void UpdateBackgroundColor()
+        {
+            _naviMenu.BackgroundColor = _backgroundColor;
+            foreach (var item in _items)
+            {
+                item.SetPartColor("bg", _backgroundColor);
+            }
+        }
+
+        void UpdateForegroundColor()
+        {
+            foreach (var item in _items)
+            {
+                item.Update();
+            }
+        }
+
+        bool IsUpdated(List<List<Element>> items)
+        {
+            if (_itemCache == null)
+                return true;
+
+            if (_itemCache.Count != items.Count)
+                return true;
+
+            for (int i = 0; i < items.Count; i++)
+            {
+                if (_itemCache[i].Count != items[i].Count)
+                    return true;
+
+                for (int j = 0; j < items[i].Count; j++)
+                {
+                    if (_itemCache[i][j] != items[i][j])
+                        return true;
+                }
+            }
+            return false;
+        }
+
+    }
+    public enum DraggedState
+    {
+        EdgeTop,
+        Up,
+        Down,
+        EdgeBottom,
+    }
+
+    public class DraggedEventArgs
+    {
+        public DraggedState State { get; private set; }
+
+        public DraggedEventArgs(DraggedState state)
+        {
+            State = state;
+        }
+    }
+
+    static class FileImageSourceEX
+    {
+        public static string ToAbsPath(this FileImageSource source)
+        {
+            return ResourcePath.GetPath(source.File);
+        }
+    }
+
+    static class ColorEX
+    {
+        public static string ToHex(this EColor c)
+        {
+            return string.Format("#{0:X2}{1:X2}{2:X2}{3:X2}", c.R, c.G, c.B, c.A);
+        }
+    }
+}
diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellContentRenderer.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellContentRenderer.cs
new file mode 100644 (file)
index 0000000..d6f31cb
--- /dev/null
@@ -0,0 +1,40 @@
+using ElmSharp;
+using Xamarin.Forms;
+using Xamarin.Forms.Platform.Tizen;
+
+namespace Tizen.Wearable.CircularUI.Forms.Renderer
+{
+    public class ShellContentRenderer : IShellItemRenderer
+    {
+        public ShellContentRenderer(ShellContent content)
+        {
+            ShellContent = content;
+            NativeView = GetNativeView(content);
+        }
+
+        public ShellContent ShellContent { get; protected set; }
+
+        public BaseShellItem Item => ShellContent;
+
+        public EvasObject NativeView { get; protected set; }
+
+        public void Dispose()
+        {
+            Dispose(true);
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                NativeView?.Unrealize();
+            }
+        }
+
+        static EvasObject GetNativeView(ShellContent content)
+        {
+            var page = (content as IShellContentController).GetOrCreateContent();
+            return Platform.GetOrCreateRenderer(page).NativeView;
+        }
+    }
+}
diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellItemRenderer.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellItemRenderer.cs
new file mode 100644 (file)
index 0000000..9f74894
--- /dev/null
@@ -0,0 +1,97 @@
+using ElmSharp;
+using System.Collections.Generic;
+using System.ComponentModel;
+using Xamarin.Forms;
+using XForms = Xamarin.Forms.Forms;
+
+namespace Tizen.Wearable.CircularUI.Forms.Renderer
+{
+    public class ShellItemRenderer : IShellItemRenderer
+    {
+        Box _mainLayout;
+        EvasObject _currentItem;
+        Dictionary<BaseShellItem, IShellItemRenderer> _rendererCache = new Dictionary<BaseShellItem, IShellItemRenderer>();
+
+        public ShellItemRenderer(ShellItem item)
+        {
+            ShellItem = item;
+            ShellItem.PropertyChanged += OnItemPropertyChanged;
+            InitializeComponent();
+            UpdateCurrentItem();
+        }
+
+        public ShellItem ShellItem { get; protected set; }
+
+        public BaseShellItem Item => ShellItem;
+
+        public EvasObject NativeView => _mainLayout;
+
+        public void Dispose()
+        {
+            Dispose(true);
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                ResetCurrentItem();
+                ShellItem.PropertyChanged -= OnItemPropertyChanged;
+            }
+        }
+
+        void InitializeComponent()
+        {
+            _mainLayout = new Box(XForms.NativeParent);
+            _mainLayout.SetLayoutCallback(OnLayout);
+        }
+
+        void UpdateCurrentItem()
+        {
+            ResetCurrentItem();
+            var currentItem = ShellItem.CurrentItem;
+            if (currentItem != null)
+            {
+                if (!_rendererCache.TryGetValue(currentItem, out IShellItemRenderer renderer))
+                {
+                    renderer = ShellRendererFactory.Default.CreateShellNavigationRenderer(currentItem);
+                    _rendererCache[currentItem] = renderer;
+                }
+                SetCurrentItem(renderer.NativeView);
+            }
+        }
+
+        void SetCurrentItem(EvasObject item)
+        {
+            _currentItem = item;
+            _currentItem.Show();
+            _mainLayout.PackEnd(_currentItem);
+        }
+
+        void ResetCurrentItem()
+        {
+            if (_currentItem != null)
+            {
+                _mainLayout.UnPack(_currentItem);
+                _currentItem.Hide();
+                _currentItem = null;
+            }
+        }
+
+        void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
+        {
+            if (e.PropertyName == nameof(ShellItem.CurrentItem))
+            {
+                UpdateCurrentItem();
+            }
+        }
+
+        void OnLayout()
+        {
+            if (_currentItem != null)
+            {
+                _currentItem.Geometry = _mainLayout.Geometry;
+            }
+        }
+    }
+}
diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellRenderer.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellRenderer.cs
new file mode 100644 (file)
index 0000000..dca82a1
--- /dev/null
@@ -0,0 +1,242 @@
+using ElmSharp;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Xamarin.Forms;
+using Xamarin.Forms.Platform.Tizen;
+using XForms = Xamarin.Forms.Forms;
+using XShell = Xamarin.Forms.Shell;
+using Tizen.Wearable.CircularUI.Forms;
+
+[assembly: ExportRenderer(typeof(CircularShell), typeof(Tizen.Wearable.CircularUI.Forms.Renderer.ShellRenderer))]
+namespace Tizen.Wearable.CircularUI.Forms.Renderer
+{
+    public class ShellRenderer : VisualElementRenderer<XShell>
+    {
+        NavigationDrawer _drawer;
+        NavigationView _navigationView;
+
+        Dictionary<BaseShellItem, IShellItemRenderer> _rendererCache = new Dictionary<BaseShellItem, IShellItemRenderer>();
+
+        public ShellRenderer()
+        {
+            RegisterPropertyHandler(XShell.CurrentItemProperty, UpdateCurrentItem);
+            RegisterPropertyHandler(XShell.FlyoutIsPresentedProperty, UpdateFlyoutIsPresented);
+            RegisterPropertyHandler(XShell.FlyoutBehaviorProperty, UpdateFlyoutBehavior);
+            RegisterPropertyHandler(XShell.FlyoutIconProperty, UpdateFlyoutIcon);
+            RegisterPropertyHandler(XShell.FlyoutBackgroundColorProperty, UpdateFlyoutBackgroundColor);
+            RegisterPropertyHandler(CircularShell.FlyoutIconBackgroundColorProperty, UpdateFlyoutIconBackgroundColor);
+            RegisterPropertyHandler(CircularShell.FlyoutForegroundColorProperty, UpdateFlyoutForegroundColor);
+        }
+
+        protected override void OnElementChanged(ElementChangedEventArgs<XShell> e)
+        {
+            InitializeComponent();
+            base.OnElementChanged(e);
+        }
+
+        protected override void OnElementReady()
+        {
+            base.OnElementReady();
+            UpdateFlyoutMenu();
+            (Element as IShellController).StructureChanged += OnNavigationStructureChanged;
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                foreach (var renderer in _rendererCache.Values)
+                {
+                    renderer.Dispose();
+                }
+                (Element as IShellController).StructureChanged -= OnNavigationStructureChanged;
+            }
+            base.Dispose(disposing);
+        }
+
+        void InitializeComponent()
+        {
+            if (_drawer == null)
+            {
+                _drawer = new NavigationDrawer(XForms.NativeParent);
+                _drawer.IsOpen = Element.FlyoutIsPresented;
+                _drawer.Toggled += OnNavigationDrawerToggled;
+                SetNativeView(_drawer);
+            }
+        }
+
+        void OnNavigationStructureChanged(object sender, EventArgs e)
+        {
+            UpdateFlyoutMenu();
+        }
+
+        void UpdateFlyoutMenu()
+        {
+            if (Element.FlyoutBehavior == FlyoutBehavior.Disabled)
+                return;
+
+            var flyoutItems = (Element as IShellController).GenerateFlyoutGrouping();
+            int itemCount = 0;
+            foreach (var item in flyoutItems)
+            {
+                itemCount += item.Count;
+            }
+
+            if (itemCount > 1)
+            {
+                InitializeNavigationDrawer();
+                _navigationView.Build(flyoutItems);
+            }
+            else
+            {
+                DeinitializeNavigationView();
+            }
+        }
+
+        void InitializeNavigationDrawer()
+        {
+            if (_navigationView != null)
+            {
+                return;
+            }
+
+            _navigationView = new NavigationView(XForms.NativeParent)
+            {
+                AlignmentX = -1,
+                AlignmentY = -1,
+                WeightX = 1,
+                WeightY = 1,
+            };
+            _navigationView.Show();
+            _navigationView.ItemSelected += OnMenuItemSelected;
+
+            _drawer.SetDrawerContent(_navigationView);
+        }
+
+        void OnNavigationDrawerToggled(object sender, EventArgs e)
+        {
+            if (_drawer.IsOpen)
+            {
+                _navigationView.Activate();
+            }
+            else
+            {
+                _navigationView.Deactivate();
+
+                var stack = (Element.CurrentItem.CurrentItem as ShellSection)?.Stack;
+                var currentPage = stack?.LastOrDefault<Page>();
+
+                if (currentPage == null)
+                {
+                   currentPage = (Element.CurrentItem.CurrentItem.CurrentItem as IShellContentController)?.Page;
+                }
+
+                if (currentPage != null)
+                {
+                    var renderer = Platform.GetOrCreateRenderer(currentPage);
+                    (renderer as CirclePageRenderer)?.UpdateRotaryFocusObject();
+                }
+            }
+
+            Element.SetValueFromRenderer(XShell.FlyoutIsPresentedProperty, _drawer.IsOpen);
+        }
+
+        void DeinitializeNavigationView()
+        {
+            if (_navigationView == null)
+                return;
+            _drawer.SetDrawerContent(null);
+            _navigationView.Unrealize();
+            _navigationView = null;
+        }
+
+        void OnMenuItemSelected(object sender, SelectedItemChangedEventArgs e)
+        {
+            ((IShellController)Element).OnFlyoutItemSelected(e.SelectedItem as Element);
+        }
+
+        void UpdateCurrentItem()
+        {
+            ResetCurrentItem();
+            if (Element.CurrentItem != null)
+            {
+                if (!_rendererCache.TryGetValue(Element.CurrentItem, out IShellItemRenderer renderer))
+                {
+                    renderer = ShellRendererFactory.Default.CreateItemRenderer(Element.CurrentItem);
+                    _rendererCache[Element.CurrentItem] = renderer;
+                }
+                SetCurrentItem(renderer.NativeView);
+            }
+        }
+
+        void UpdateFlyoutBehavior(bool init)
+        {
+            if (init)
+                return;
+
+            if (Element.FlyoutBehavior == FlyoutBehavior.Disabled)
+            {
+                DeinitializeNavigationView();
+            }
+            else if (Element.FlyoutBehavior == FlyoutBehavior.Flyout)
+            {
+                UpdateFlyoutMenu();
+            }
+            else if (Element.FlyoutBehavior == FlyoutBehavior.Locked)
+            {
+                // Locked behavior is not supported on circularshell
+            }
+        }
+
+        void UpdateFlyoutIcon(bool init)
+        {
+            if (init && Element.FlyoutIcon == null)
+                return;
+
+            _drawer.UpdateDrawerIcon(Element.FlyoutIcon);
+        }
+
+        void UpdateFlyoutBackgroundColor(bool init)
+        {
+            if (init && Element.FlyoutBackgroundColor.IsDefault)
+                return;
+
+            if (_navigationView != null)
+            {
+                _navigationView.BackgroundColor = Element.FlyoutBackgroundColor.ToNative();
+            }
+        }
+
+        void UpdateFlyoutForegroundColor(bool init)
+        {
+            if (init && CircularShell.GetFlyoutForegroundColor(Element).IsDefault)
+                return;
+
+            if (_navigationView != null)
+            {
+                _navigationView.ForegroundColor = CircularShell.GetFlyoutForegroundColor(Element).ToNative();
+            }
+        }
+
+        void UpdateFlyoutIconBackgroundColor()
+        {
+            _drawer.HandlerBackgroundColor = CircularShell.GetFlyoutIconBackgroundColor(Element).ToNative();
+        }
+
+        void UpdateFlyoutIsPresented()
+        {
+            _drawer.IsOpen = Element.FlyoutIsPresented;
+        }
+
+        void SetCurrentItem(EvasObject item)
+        {
+            _drawer.SetMainContent(item);
+        }
+
+        void ResetCurrentItem()
+        {
+            _drawer.SetMainContent(null);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellRendererFactory.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellRendererFactory.cs
new file mode 100644 (file)
index 0000000..e24034c
--- /dev/null
@@ -0,0 +1,53 @@
+using Xamarin.Forms;
+
+namespace Tizen.Wearable.CircularUI.Forms.Renderer
+{
+    public class ShellRendererFactory
+    {
+        static ShellRendererFactory _instance;
+        public static ShellRendererFactory Default
+        {
+            get
+            {
+                if (_instance == null)
+                {
+                    _instance = new ShellRendererFactory();
+                }
+                return _instance;
+            }
+            set
+            {
+                _instance = value;
+            }
+
+        }
+
+        public virtual IShellItemRenderer CreateItemRenderer(ShellItem item)
+        {
+            if (item.Items.Count == 1)
+            {
+                return CreateShellNavigationRenderer(item.CurrentItem);
+            }
+            return new ShellItemRenderer(item);
+        }
+
+        public virtual IShellItemRenderer CreateShellNavigationRenderer(ShellSection item)
+        {
+            return new ShellSectionNavigationRenderer(item);
+        }
+
+        public virtual IShellItemRenderer CreateItemRenderer(ShellSection item)
+        {
+            if (item.Items.Count == 1)
+            {
+                return CreateItemRenderer(item.CurrentItem);
+            }
+            return new ShellSectionItemsRenderer(item);
+        }
+
+        public virtual IShellItemRenderer CreateItemRenderer(ShellContent item)
+        {
+            return new ShellContentRenderer(item);
+        }
+    }
+}
diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellSectionItemsRenderer.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellSectionItemsRenderer.cs
new file mode 100644 (file)
index 0000000..7b6d684
--- /dev/null
@@ -0,0 +1,290 @@
+using ElmSharp;
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using Xamarin.Forms;
+using XForms = Xamarin.Forms.Forms;
+
+namespace Tizen.Wearable.CircularUI.Forms.Renderer
+{
+    public class ShellSectionItemsRenderer : IShellItemRenderer
+    {
+        const int ItemMaxCount = 20;
+        const int OddMiddleItem = 10;
+        const int EvenMiddleItem = 11;
+
+        Box _mainLayout;
+        Index _indexIndicator;
+        Scroller _scroller;
+        Box _innerContainer;
+        List<ItemHolder> _items = new List<ItemHolder>();
+
+        int _currentIndex = -1;
+        Rect _lastLayoutBound;
+        int _updateByCode;
+
+
+        public ShellSectionItemsRenderer(ShellSection shellSection)
+        {
+            ShellSection = shellSection;
+            ShellSection.PropertyChanged += OnSectionPropertyChanged;
+            (ShellSection.Items as INotifyCollectionChanged).CollectionChanged += OnItemsChanged;
+            InitializeComponent();
+            UpdateItems();
+        }
+
+        public ShellSection ShellSection { get; protected set; }
+
+        public BaseShellItem Item => ShellSection;
+
+        public EvasObject NativeView => _mainLayout;
+
+        public void Dispose()
+        {
+            Dispose(true);
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _mainLayout?.Unrealize();
+                (ShellSection.Items as INotifyCollectionChanged).CollectionChanged -= OnItemsChanged;
+                ShellSection.PropertyChanged -= OnSectionPropertyChanged;
+            }
+        }
+
+        void InitializeComponent()
+        {   
+            _mainLayout = new Box(XForms.NativeParent)
+            {
+                AlignmentX = -1,
+                AlignmentY = -1,
+                WeightX = 1,
+                WeightY = 1,
+            };
+            _mainLayout.Show();
+            _mainLayout.SetLayoutCallback(OnLayout);
+
+            _indexIndicator = new Index(_mainLayout)
+            {
+                IsHorizontal = true,
+                AutoHide = false,
+                Style = IndexStyle.Circle,
+            };
+            _indexIndicator.Show();
+
+            _scroller = new Scroller(_mainLayout);
+            _scroller.PageScrolled += OnPageScrolled;
+            _scroller.DragStart += OnDragStarted;
+
+            // Disables the visibility of the scrollbar in both directions:
+            _scroller.HorizontalScrollBarVisiblePolicy = ScrollBarVisiblePolicy.Invisible;
+            _scroller.VerticalScrollBarVisiblePolicy = ScrollBarVisiblePolicy.Invisible;
+            // Sets the limit of scroll to one page maximum:
+            _scroller.HorizontalPageScrollLimit = 1;
+            _scroller.SetPageSize(1.0, 1.0);
+            _scroller.SetAlignment(-1, -1);
+            _scroller.SetWeight(1.0, 1.0);
+            _scroller.Show();
+
+            _innerContainer = new Box(_mainLayout);
+            _innerContainer.SetLayoutCallback(OnInnerLayoutUpdate);
+            _innerContainer.SetAlignment(-1, -1);
+            _innerContainer.SetWeight(1.0, 1.0);
+            _innerContainer.Show();
+            _scroller.SetContent(_innerContainer);
+
+            _mainLayout.PackEnd(_indexIndicator);
+            _mainLayout.PackEnd(_scroller);
+            _indexIndicator.StackAbove(_scroller);
+        }
+
+        void OnDragStarted(object sender, EventArgs e)
+        {
+            if (_currentIndex - 1 >= 0 && !_items[_currentIndex - 1].IsRealized)
+            {
+                RealizeItem(_items[_currentIndex - 1]);
+            }
+            if (_currentIndex + 1 < _items.Count && !_items[_currentIndex + 1].IsRealized)
+            {
+                RealizeItem(_items[_currentIndex + 1]);
+            }
+        }
+
+        void UpdateItems()
+        {
+            _items.Clear();
+            _indexIndicator.Clear();
+            _innerContainer.UnPackAll();
+            _lastLayoutBound = default(Rect);
+
+            foreach (var item in ShellSection.Items)
+            {
+                var indexItem = _indexIndicator.Append(null);
+                indexItem.Style = GetItemStyle(ShellSection.Items.Count, _items.Count);
+                _items.Add(new ItemHolder
+                {
+                    IsRealized = false,
+                    IndexItem = indexItem,
+                    Item = item
+                });
+            }
+            _indexIndicator.Update(0);
+            UpdateCurrentPage(ShellSection.Items.IndexOf(ShellSection.CurrentItem));
+        }
+
+        void RealizeItem(ItemHolder item)
+        {
+            var renderer = ShellRendererFactory.Default.CreateItemRenderer(item.Item);
+            renderer.NativeView.Show();
+            item.NativeView = renderer.NativeView;
+            item.IsRealized = true;
+            _innerContainer.PackEnd(item.NativeView);
+            item.NativeView.StackBelow(_indexIndicator);
+            item.NativeView.Geometry = item.Bound;
+        }
+
+        void UpdateCurrentPage(int index)
+        {
+            if (_currentIndex == index)
+                return;
+
+            _currentIndex = index;
+            UpdateCurrentIndexIndicator();
+            if (!_items[index].IsRealized)
+            {
+                RealizeItem(_items[index]);
+            }
+            UpdateFocusPolicy();
+        }
+
+        void UpdateFocusPolicy()
+        {
+            foreach (var item in _items)
+            {
+                if (item.IsRealized)
+                {
+                    if (item.NativeView is ElmSharp.Widget widget)
+                    {
+                        widget.AllowTreeFocus = (_items[_currentIndex] == item);
+                    }
+                }
+            }
+        }
+
+        void UpdateCurrentIndexIndicator()
+        {
+            if (_currentIndex >= 0 && _currentIndex < _items.Count)
+            {
+                _items[_currentIndex].IndexItem.Select(true);
+            }
+        }
+        void OnItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            UpdateItems();
+        }
+
+        void OnPageScrolled(object sender, EventArgs e)
+        {
+            if (_updateByCode > 0)
+            {
+                return;
+            }
+
+            if (_currentIndex < 0 || ShellSection.Items.Count <= _currentIndex)
+            {
+                return;
+            }
+
+            UpdateCurrentPage(_scroller.HorizontalPageIndex);
+            var currentItem = ShellSection.Items[_currentIndex];
+            ShellSection.SetValueFromRenderer(ShellSection.CurrentItemProperty, currentItem);
+        }
+
+        void OnSectionPropertyChanged(object sender, PropertyChangedEventArgs e)
+        {
+            if (e.PropertyName == nameof(ShellSection.CurrentItem))
+            {
+                var newIndex = ShellSection.Items.IndexOf(ShellSection.CurrentItem);
+                if (_currentIndex != newIndex)
+                {
+                    UpdateCurrentPage(newIndex);
+                    _updateByCode++;
+                    _scroller.ScrollTo(newIndex, 0, false);
+                    _updateByCode--;
+                }
+            }
+        }
+
+        void OnLayout()
+        {
+            _indexIndicator.Geometry = _mainLayout.Geometry;
+            _scroller.Geometry = _mainLayout.Geometry;
+        }
+
+        void OnInnerLayoutUpdate()
+        {
+            if (_lastLayoutBound == _innerContainer.Geometry)
+            {
+                return;
+            }
+            _lastLayoutBound = _innerContainer.Geometry;
+
+            var layoutBound = _innerContainer.Geometry.Size;
+            int baseX = _innerContainer.Geometry.X;
+
+            Rect bound = _scroller.Geometry;
+            int index = 0;
+            foreach (var item in _items)
+            {
+                bound.X = baseX + index * bound.Width;
+                item.Bound = bound;
+                if (item.IsRealized)
+                {
+                    item.NativeView.Geometry = bound;
+                }
+                index++;
+            }
+            _innerContainer.MinimumWidth = _items.Count * bound.Width;
+
+            if (_items.Count > _currentIndex && _currentIndex >= 0)
+            {
+                _updateByCode++;
+                _scroller.ScrollTo(_currentIndex, 0, false);
+                _updateByCode--;
+            }
+        }
+
+        static string GetItemStyle(int itemCount, int offset)
+        {
+            string returnValue = string.Empty;
+            int startItem;
+            int styleNumber;
+
+            if (itemCount % 2 == 0)  //Item count is even.
+            {
+                startItem = EvenMiddleItem - itemCount / 2;
+                styleNumber = startItem + offset;
+                returnValue = "item/even_" + styleNumber;
+            }
+            else  //Item count is odd.
+            {
+                startItem = OddMiddleItem - itemCount / 2;
+                styleNumber = startItem + offset;
+                returnValue = "item/odd_" + styleNumber;
+            }
+            return returnValue;
+        }
+
+        class ItemHolder
+        {
+            public bool IsRealized { get; set; }
+            public Rect Bound { get; set; }
+            public EvasObject NativeView { get; set; }
+            public IndexItem IndexItem { get; set; }
+            public ShellContent Item { get; set; }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellSectionNavigationRenderer.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellSectionNavigationRenderer.cs
new file mode 100644 (file)
index 0000000..7a860e7
--- /dev/null
@@ -0,0 +1,208 @@
+using ElmSharp;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Xamarin.Forms;
+using Xamarin.Forms.Internals;
+using Xamarin.Forms.Platform.Tizen;
+using XForms = Xamarin.Forms.Forms;
+
+namespace Tizen.Wearable.CircularUI.Forms.Renderer
+{
+    class SimpleViewStack : Box
+    {
+        EvasObject _lastTop;
+
+        public SimpleViewStack(EvasObject parent) : base(parent)
+        {
+            InternalStack = new List<EvasObject>();
+            SetLayoutCallback(OnLayout);
+        }
+
+        List<EvasObject> InternalStack { get; set; }
+
+        public IReadOnlyList<EvasObject> Stack => InternalStack;
+
+        public void Push(EvasObject view)
+        {
+            InternalStack.Add(view);
+            PackEnd(view);
+            UpdateTopView();
+        }
+
+        public void Pop()
+        {
+            if (_lastTop != null)
+            {
+                var tobeRemoved = _lastTop;
+                InternalStack.Remove(tobeRemoved);
+                UnPack(tobeRemoved);
+                UpdateTopView();
+
+                // if Pop was called by removed page,
+                // Unrealize cause deletation of NativeCallback, it could be a cause of crash
+                Device.BeginInvokeOnMainThread(() =>
+                {
+                    tobeRemoved.Unrealize();
+                });
+            }
+        }
+
+        public void PopToRoot()
+        {
+            while (InternalStack.Count > 1)
+            {
+                Pop();
+            }
+        }
+
+        public void Insert(EvasObject before, EvasObject view)
+        {
+            view.Hide();
+            var idx = InternalStack.IndexOf(before);
+            InternalStack.Insert(idx, view);
+            PackEnd(view);
+            UpdateTopView();
+        }
+
+        public void Remove(EvasObject view)
+        {
+            InternalStack.Remove(view);
+            UnPack(view);
+            UpdateTopView();
+            Device.BeginInvokeOnMainThread(() =>
+            {
+                view?.Unrealize();
+            });
+        }
+
+        void UpdateTopView()
+        {
+            if (_lastTop != InternalStack.LastOrDefault())
+            {
+                _lastTop?.Hide();
+                _lastTop = InternalStack.LastOrDefault();
+                _lastTop.Show();
+            }
+        }
+
+        void OnLayout()
+        {
+            foreach (var view in Stack)
+            {
+                view.Geometry = Geometry;
+            }
+        }
+        
+    }
+
+    public class ShellSectionNavigationRenderer : IShellItemRenderer
+    {
+        SimpleViewStack _viewStack;
+        IShellItemRenderer _rootPageRenderer;
+
+        public ShellSectionNavigationRenderer(ShellSection item)
+        {
+            ShellSection = item;
+            (ShellSection as IShellSectionController).NavigationRequested += OnNavigationRequested;
+            InitializeComponent();
+        }
+
+        public ShellSection ShellSection { get; protected set; }
+
+        public BaseShellItem Item => ShellSection;
+
+        public EvasObject NativeView => _viewStack;
+
+        public void Dispose()
+        {
+            Dispose(true);
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _rootPageRenderer?.Dispose();
+                _viewStack?.Unrealize();
+                (ShellSection as IShellSectionController).NavigationRequested -= OnNavigationRequested;
+            }
+        }
+
+        void InitializeComponent()
+        {
+            _viewStack = new SimpleViewStack(XForms.NativeParent);
+            _viewStack.Show();
+
+            _rootPageRenderer = ShellRendererFactory.Default.CreateItemRenderer(ShellSection);
+            _viewStack.Push(_rootPageRenderer.NativeView);
+        }
+
+        void OnInsertRequest(NavigationRequestedEventArgs request)
+        {
+            var before = Platform.GetRenderer(request.BeforePage)?.NativeView ?? null;
+            if (before == null)
+            {
+                request.Task = Task.FromException<bool>(new ArgumentException("Can't found page on stack", nameof(request.BeforePage)));
+                return;
+            }
+            var renderer = Platform.GetOrCreateRenderer(request.Page);
+            _viewStack.Insert(before, renderer.NativeView);
+            request.Task = Task.FromResult(true);
+        }
+
+        void OnPushRequest(NavigationRequestedEventArgs request)
+        {
+            var renderer = Platform.GetOrCreateRenderer(request.Page);
+            _viewStack.Push(renderer.NativeView);
+            request.Task = Task.FromResult(true);
+        }
+
+        void OnPopRequest(NavigationRequestedEventArgs request)
+        {
+            _viewStack.Pop();
+            request.Task = Task.FromResult(true);
+        }
+
+        void OnPopToRootRequest(NavigationRequestedEventArgs request)
+        {
+            _viewStack.PopToRoot();
+            request.Task = Task.FromResult(true);
+        }
+
+        void OnRemoveRequest(NavigationRequestedEventArgs request)
+        {
+            var renderer = Platform.GetRenderer(request.Page);
+            if (renderer == null)
+            {
+                request.Task = Task.FromException<bool>(new ArgumentException("Can't found page on stack", nameof(request.Page)));
+                return;
+            }
+            _viewStack.Remove(renderer.NativeView);
+            request.Task = Task.FromResult(true);
+        }
+
+        void OnNavigationRequested(object sender, NavigationRequestedEventArgs e)
+        {
+            switch (e.RequestType)
+            {
+                case NavigationRequestType.Insert:
+                    OnInsertRequest(e);
+                    break;
+                case NavigationRequestType.Push:
+                    OnPushRequest(e);
+                    break;
+                case NavigationRequestType.Pop:
+                    OnPopRequest(e);
+                    break;
+                case NavigationRequestType.PopToRoot:
+                    OnPopToRootRequest(e);
+                    break;
+                case NavigationRequestType.Remove:
+                    OnRemoveRequest(e);
+                    break;
+            }
+        }
+    }
+}
index f2d0b1b..1d54156 100644 (file)
@@ -22,6 +22,7 @@ using System.Linq;
 using System.Text;
 using Xamarin.Forms;
 using Xamarin.Forms.Platform.Tizen;
+using XForms = Xamarin.Forms.Forms;
 
 [assembly: ExportEffect(typeof(Tizen.Wearable.CircularUI.Forms.Renderer.TizenCircleSurfaceEffect), "CircleSurfaceEffect")]
 namespace Tizen.Wearable.CircularUI.Forms.Renderer
@@ -43,7 +44,7 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer
                {
                        var rect = Control.Geometry;
 
-                       _surfaceLayout = new ElmSharp.Layout(Container);
+                       _surfaceLayout = new ElmSharp.Layout(XForms.NativeParent);
                        _surfaceLayout.Show();
                        _surface = new ElmSharp.Wearable.CircleSurface(_surfaceLayout);
                        _surfaceLayout.Geometry = rect;
index 632dd82..078ab7a 100644 (file)
@@ -332,12 +332,20 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer
 
                void UpdateTitle()
                {
-                       _layout.SetPartText("elm.text.title", _title);
+                       string title = _title?.Replace("&", "&amp;")
+                                                                 .Replace("<", "&lt;")
+                                                                 .Replace(">", "&gt;")
+                                                                 .Replace(Environment.NewLine, "<br>");
+                       _layout.SetPartText("elm.text.title", title);
                }
 
                void UpdateText()
                {
-                       _layout.SetPartText("elm.text", _text);
+                       string text = _text?.Replace("&", "&amp;")
+                                                               .Replace("<", "&lt;")
+                                                               .Replace(">", "&gt;")
+                                                               .Replace(Environment.NewLine, "<br>");
+                       _layout.SetPartText("elm.text", text);
                }
 
                public void Show()
index dbf3c4c..77c8668 100644 (file)
  * limitations under the License.
  */
 
-using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Collections.Specialized;
+using System.ComponentModel;
 using Xamarin.Forms;
 
 namespace Tizen.Wearable.CircularUI.Forms
@@ -62,6 +62,9 @@ namespace Tizen.Wearable.CircularUI.Forms
                /// <since_tizen> 4 </since_tizen>
                public IList<ICircleSurfaceItem> CircleSurfaceItems { get; }
 
+               [EditorBrowsable(EditorBrowsableState.Never)]
+               public bool Appeared { get; set; }
+
                /// <summary>
                /// Gets or sets ActionButton that presents a menu item and associates it with a command
                /// </summary>
@@ -92,6 +95,18 @@ namespace Tizen.Wearable.CircularUI.Forms
                        }
                }
 
+               protected override void OnAppearing()
+               {
+                       base.OnAppearing();
+                       Appeared = true;
+               }
+
+               protected override void OnDisappearing()
+               {
+                       base.OnDisappearing();
+                       Appeared = false;
+               }
+
                void OnSurfaceItemsChanged(object sender, NotifyCollectionChangedEventArgs args)
                {
                        if (args.Action != NotifyCollectionChangedAction.Add) return;
diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms/CircleSurfaceView.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms/CircleSurfaceView.cs
new file mode 100644 (file)
index 0000000..ccdc95b
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2018 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ * Licensed under the Flora License, Version 1.1 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://floralicense.org/license/
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using Xamarin.Forms;
+
+namespace Tizen.Wearable.CircularUI.Forms
+{
+    public class CircleSurfaceView : View
+    {
+        public IList<ICircleSurfaceItem> CircleSurfaceItems { get; internal set; }
+
+        public CircleSurfaceView()
+        {
+            var circleSurfaceItems = new ObservableCollection<ICircleSurfaceItem>();
+            circleSurfaceItems.CollectionChanged += OnCircleObjectItemsCollectionChanged;
+            CircleSurfaceItems = circleSurfaceItems;
+        }
+
+        protected override void OnBindingContextChanged()
+        {
+            base.OnBindingContextChanged();
+            foreach (CircleSurfaceItem item in CircleSurfaceItems)
+            {
+                SetInheritedBindingContext(item, BindingContext);
+            }
+        }
+
+        void OnCircleObjectItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
+        {
+            if (args.Action != NotifyCollectionChangedAction.Add)
+                return;
+            foreach (Element item in args.NewItems)
+                item.Parent = this;
+        }
+    }
+}
diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms/CircularShell.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms/CircularShell.cs
new file mode 100644 (file)
index 0000000..54b6ff3
--- /dev/null
@@ -0,0 +1,52 @@
+using Xamarin.Forms;
+
+namespace Tizen.Wearable.CircularUI.Forms
+{
+    public class CircularShell : Shell
+    {
+        public static readonly BindableProperty FlyoutIconBackgroundColorProperty = BindableProperty.CreateAttached("FlyoutIconBackgroundColor", typeof(Color), typeof(Shell), Color.Default);
+        public static readonly BindableProperty FlyoutForegroundColorProperty = BindableProperty.CreateAttached("FlyoutForegroundColor", typeof(Color), typeof(Shell), Color.Default);
+
+        public static Color GetFlyoutIconBackgroundColor(BindableObject element)
+        {
+            return (Color)element.GetValue(FlyoutIconBackgroundColorProperty);
+        }
+
+        public static void SetFlyoutIconBackgroundColor(BindableObject element, Color color)
+        {
+            element.SetValue(FlyoutIconBackgroundColorProperty, color);
+        }
+
+        public static Color GetFlyoutForegroundColor(BindableObject element)
+        {
+            return (Color)element.GetValue(FlyoutForegroundColorProperty);
+        }
+
+        public static void SetFlyoutForegroundColor(BindableObject element, Color color)
+        {
+            element.SetValue(FlyoutForegroundColorProperty, color);
+        }
+
+        public Color FlyoutIconBackgroundColor
+        {
+            get => (Color)GetValue(FlyoutIconBackgroundColorProperty);
+            set => SetValue(FlyoutIconBackgroundColorProperty, value);
+        }
+
+        public Color FlyoutForegroundColor
+        {
+            get => (Color)GetValue(FlyoutForegroundColorProperty);
+            set => SetValue(FlyoutForegroundColorProperty, value);
+        }
+
+        protected override bool OnBackButtonPressed()
+        {
+            if (FlyoutIsPresented)
+            {
+                FlyoutIsPresented = false;
+                return true;
+            }
+            return base.OnBackButtonPressed();
+        }
+    }
+}
diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms/ContentButton.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms/ContentButton.cs
new file mode 100644 (file)
index 0000000..47b9e2a
--- /dev/null
@@ -0,0 +1,184 @@
+/*
+ * Copyright (c) 2018 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ * Licensed under the Flora License, Version 1.1 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://floralicense.org/license/
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+using System.ComponentModel;
+using System.Windows.Input;
+using Xamarin.Forms;
+
+namespace Tizen.Wearable.CircularUI.Forms
+{
+    /// <summary>
+    /// The ContentButton is a Button, which allows you to customize the View to be displayed.
+    /// </summary>
+    /// <since_tizen> 4 </since_tizen>
+    public class ContentButton : ContentView, IButtonController
+    {
+        /// <summary>
+        /// BindableProperty. Identifies the Command bindable property.
+        /// </summary>
+        /// <since_tizen> 4 </since_tizen>
+        public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(ContentButton), null, 
+            propertyChanging: OnCommandChanging, propertyChanged: OnCommandChanged);
+
+        /// <summary>
+        /// BindableProperty. Identifies the CommandParameter bindable property.
+        /// </summary>
+        /// <since_tizen> 4 </since_tizen>
+        public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(nameof(CommandParameter), typeof(object), typeof(ContentButton), null, 
+            propertyChanged: (bindable, oldvalue, newvalue) => CommandCanExcuteChanged(bindable, EventArgs.Empty));
+
+        /// <summary>
+        /// Gets or sets command that is executed when the button is clicked.
+        /// </summary>
+        /// <since_tizen> 4 </since_tizen>
+        public ICommand Command
+        {
+            get => (ICommand)GetValue(CommandProperty);
+            set => SetValue(CommandProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets command paramter that is executed when the button is clicked.
+        /// </summary>
+        /// <since_tizen> 4 </since_tizen>
+        public object CommandParameter
+        {
+            get => GetValue(CommandParameterProperty);
+            set => SetValue(CommandParameterProperty, value);
+        }
+
+        /// <summary>
+        /// Occurs when the button is clicked.
+        /// </summary>
+        /// <since_tizen> 4 </since_tizen>
+        public event EventHandler Clicked;
+
+        /// <summary>
+        /// Occurs when the button is pressed.
+        /// </summary>
+        /// <since_tizen> 4 </since_tizen>
+        public event EventHandler Pressed;
+
+        /// <summary>
+        /// Occurs when the button is released.
+        /// </summary>
+        /// <since_tizen> 4 </since_tizen>
+        public event EventHandler Released;
+
+        bool IsEnabledCore
+        {
+            set => SetValueCore(IsEnabledProperty, value);
+        }
+
+        /// <summary>
+        /// For internal use.
+        /// </summary>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public void SendClicked()
+        {
+            if (IsEnabled)
+            {
+                Command?.Execute(CommandParameter);
+                Clicked?.Invoke(this, EventArgs.Empty);
+            }
+        }
+
+        /// <summary>
+        /// For internal use.
+        /// </summary>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public void SendPressed()
+        {
+            if (IsEnabled)
+            {
+                Pressed?.Invoke(this, EventArgs.Empty);
+            }
+        }
+
+        /// <summary>
+        /// For internal use.
+        /// </summary>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public void SendReleased()
+        {
+            if (IsEnabled)
+            {
+                Released?.Invoke(this, EventArgs.Empty);
+            }
+        }
+
+        protected override void OnBindingContextChanged()
+        {
+            base.OnBindingContextChanged();
+
+            View content = Content;
+            if (content != null)
+            {
+                SetInheritedBindingContext(content, BindingContext);
+            }
+        }
+
+        static void OnCommandChanged(BindableObject bindable, object oldCommand, object newCommand)
+        {
+            ContentButton button = (ContentButton)bindable;
+            if (newCommand is ICommand command)
+            {
+                command.CanExecuteChanged += button.OnCommandCanExecuteChanged;
+            }
+            CommandChanged(button);
+        }
+
+        static void CommandChanged(ContentButton button)
+        {
+            if(button.Command != null)
+            {
+                CommandCanExcuteChanged(button, EventArgs.Empty);
+            }
+            else
+            {
+                button.IsEnabledCore = true;
+            }
+        }
+
+        static void OnCommandChanging(BindableObject bindable, object oldCommand, object newCommand)
+        {
+            ContentButton button = (ContentButton)bindable;
+            if (oldCommand != null)
+            {
+                (oldCommand as ICommand).CanExecuteChanged -= button.OnCommandCanExecuteChanged;
+            }
+        }
+
+        static void CommandCanExcuteChanged(object sender, EventArgs e)
+        {
+            var button = (ContentButton)sender;
+            if (button.Command != null)
+            {
+                button.IsEnabledCore = button.Command.CanExecute(button.CommandParameter);
+            }
+        }
+
+        void OnCommandCanExecuteChanged(object sender, EventArgs e)
+        {
+            ContentButton button = (ContentButton)sender;
+            if (button.Command != null)
+            {
+                button.IsEnabledCore = button.Command.CanExecute(button.CommandParameter);
+            }
+        }
+    }
+}
index 49d1c22..7ee4110 100644 (file)
@@ -20,6 +20,7 @@
     <EmbeddedResource Include="Resources\img_button_pause.png" />
     <EmbeddedResource Include="Resources\img_button_play.png" />
     <EmbeddedResource Include="Resources\circle.png" />
+    <EmbeddedResource Include="Resources\wc_visual_cue.png" />
   </ItemGroup>
 
 </Project>
\ No newline at end of file
index f50b0b3..52cee7c 100644 (file)
@@ -10,8 +10,8 @@ namespace Xamarin.Forms.Platform.Tizen.Native
 {
        public class EmbeddingControls : ContentView
        {
-               static readonly string PlayImagePath = "XSF.Resource.img_button_play.png";
-               static readonly string PauseImagePath = "XSF.Resource.img_button_pause.png";
+               static readonly string PlayImagePath = "XSF.Resources.img_button_play.png";
+               static readonly string PauseImagePath = "XSF.Resources.img_button_pause.png";
 
                public ImageButton PlayImage { get; private set; }
                public ImageButton PauseImage { get; private set; }
index 38b7d6b..9b58aed 100644 (file)
@@ -10,7 +10,7 @@ namespace Xamarin.Forms.Platform.Tizen
        {
                public static readonly int IconSize = 48;
                static readonly Color DefaultColor = Color.FromHex("#6200EE");
-               static readonly string IconPath = "XSF.Resource.refresh_48dp.png";
+               static readonly string IconPath = "XSF.Resources.refresh_48dp.png";
 
                bool _isPlaying;
                Image _icon;
index bc06fb1..4ed6201 100644 (file)
@@ -31,7 +31,7 @@ namespace Xamarin.Forms.Platform.Tizen
                bool _disposed = false;
                EColor _backgroudColor = ShellRenderer.DefaultBackgroundColor.ToNative();
                // The source of icon resources is https://materialdesignicons.com/
-               const string _dotsIcon = "XSF.Resource.dots_horizontal.png";
+               const string _dotsIcon = "XSF.Resources.dots_horizontal.png";
 
                public ShellItemRenderer(IFlyoutController flyoutController, ShellItem item)
                {
index d14215d..5693152 100644 (file)
@@ -26,8 +26,8 @@ namespace Xamarin.Forms.Platform.Tizen
                EColor _foregroudColor = ShellRenderer.DefaultForegroundColor.ToNative();
 
                // The source of icon resources is https://materialdesignicons.com/
-               const string _menuIcon = "XSF.Resource.menu.png";
-               const string _backIcon = "XSF.Resource.arrow_left.png";
+               const string _menuIcon = "XSF.Resources.menu.png";
+               const string _backIcon = "XSF.Resources.arrow_left.png";
 
                bool _hasBackButton = false;