From e9eb4ce9e7aeaa35893de52c283c76c8fb47a88d Mon Sep 17 00:00:00 2001 From: =?utf8?q?=EC=9C=A0=EB=A6=AC=EB=82=98/Common=20Platform=20Lab=28SR=29?= =?utf8?q?/Staff=20Engineer/=EC=82=BC=EC=84=B1=EC=A0=84=EC=9E=90?= Date: Mon, 6 Apr 2020 14:52:06 +0900 Subject: [PATCH] Support new features of Tizen.CircularUI (#188) * 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 * Update Icon Images (#245) * Update WearableUIGallery to use Shell (#252) * Fix TwoButtonPage issue on shell (#255) Co-authored-by: shyun.min Co-authored-by: Seungkeun Lee Co-authored-by: shmin Co-authored-by: shyun.min * 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 , shmin , Kangho Hur * Fixed the invalid resource stream --- src/XSF/Resources/wc_visual_cue.png | Bin 0 -> 433 bytes .../CirclePageRenderer.cs | 22 +- .../CircleSurfaceViewRenderer.cs | 136 ++++++ .../ContentButtonRenderer.cs | 86 ++++ .../InformationPopupImplementation.cs | 24 +- .../ListViewCache.cs | 1 - .../MediaPlayerImpl.cs | 7 +- .../NativeCirclePage.cs | 14 +- .../PopupEntryRenderer.cs | 22 + .../Shell/IShellItemRenderer.cs | 12 + .../Shell/NavigationDrawer.cs | 542 +++++++++++++++++++++ .../Shell/NavigationView.cs | 296 +++++++++++ .../Shell/ShellContentRenderer.cs | 40 ++ .../Shell/ShellItemRenderer.cs | 97 ++++ .../Shell/ShellRenderer.cs | 242 +++++++++ .../Shell/ShellRendererFactory.cs | 53 ++ .../Shell/ShellSectionItemsRenderer.cs | 290 +++++++++++ .../Shell/ShellSectionNavigationRenderer.cs | 208 ++++++++ .../TizenCircleSurfaceEffect.cs | 3 +- .../TwoButtonPopupImplementation.cs | 12 +- .../Tizen.Wearable.CircularUI.Forms/CirclePage.cs | 17 +- .../CircleSurfaceView.cs | 51 ++ .../CircularShell.cs | 52 ++ .../ContentButton.cs | 184 +++++++ src/XSF/XSF.csproj | 1 + .../Native/EmbeddingControls.cs | 4 +- .../Renderers/RefreshViewRenderer.cs | 2 +- .../Shell/ShellItemRenderer.cs | 2 +- .../Shell/ShellNavBar.cs | 4 +- 29 files changed, 2393 insertions(+), 31 deletions(-) create mode 100644 src/XSF/Resources/wc_visual_cue.png create mode 100644 src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/CircleSurfaceViewRenderer.cs create mode 100644 src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/ContentButtonRenderer.cs create mode 100644 src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/IShellItemRenderer.cs create mode 100644 src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/NavigationDrawer.cs create mode 100644 src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/NavigationView.cs create mode 100644 src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellContentRenderer.cs create mode 100644 src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellItemRenderer.cs create mode 100644 src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellRenderer.cs create mode 100644 src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellRendererFactory.cs create mode 100644 src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellSectionItemsRenderer.cs create mode 100644 src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellSectionNavigationRenderer.cs create mode 100644 src/XSF/Tizen.Wearable.CircularUI.Forms/CircleSurfaceView.cs create mode 100644 src/XSF/Tizen.Wearable.CircularUI.Forms/CircularShell.cs create mode 100644 src/XSF/Tizen.Wearable.CircularUI.Forms/ContentButton.cs diff --git a/src/XSF/Resources/wc_visual_cue.png b/src/XSF/Resources/wc_visual_cue.png new file mode 100644 index 0000000000000000000000000000000000000000..beddbb7ffc682c22e4a339b52e6566258c2d0a10 GIT binary patch literal 433 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Ex!3HF)PTTVM%xNb!1@J z*w6hZkrl}2EbxddW??NMQuI!IEm{={W z*VeIw15H)*ba4#v@P2#M*NfRvpzUE`jFhRAt9cc>ZDW%{M~8yq@eA4<_YSqrXmBd~ zH^*U5i+5n>f*qxa68HVpolE^yTW+#k3G#989r6krBDY-7e?Ftx{B==x?VAUZX74M0SDsls`)2EQ_r~a+ zGY>=x-)(40y|YSSX{Xj6+v2>7Hx4Q1?6Nq_B>DD+YPLH{%LfM|K{x*(15SP5a2jgC&a{>o=T;IrNOV{l~wXa+9j~7evm^ zX4!jCX>&)o#`lQk>*hV78T_vIr>U~o9z3+-F^_1lcDB&1)rna<|JNVhU)P}WlWB*o WRE1^#Dj{I_F?hQAxvX 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 index 0000000..55077b5 --- /dev/null +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/CircleSurfaceViewRenderer.cs @@ -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 + { + + Dictionary _circleSurfaceItems; + ELayout _surfaceLayout; + CircleSurface _circleSurface; + + protected override void OnElementChanged(ElementChangedEventArgs e) + { + if (Control == null) + { + var box = new Box(XForms.NativeParent); + box.SetLayoutCallback(OnLayout); + _surfaceLayout = new ELayout(box); + _circleSurface = new CircleSurface(_surfaceLayout); + _circleSurfaceItems = new Dictionary(); + box.PackEnd(_surfaceLayout); + _surfaceLayout.Show(); + SetNativeControl(box); + } + + if (e.NewElement != null) + { + var items = e.NewElement.CircleSurfaceItems as ObservableCollection; + items.CollectionChanged += OnCircleSurfaceItemsChanged; + foreach (var item in items) + { + AddCircleSurfaceItem(item); + } + } + + if (e.OldElement != null) + { + var items = e.OldElement.CircleSurfaceItems as ObservableCollection; + 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; + 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 index 0000000..593e7fd --- /dev/null +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/ContentButtonRenderer.cs @@ -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 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(); + } + } +} diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/InformationPopupImplementation.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/InformationPopupImplementation.cs index 87b39d3..d867e35 100644 --- a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/InformationPopupImplementation.cs +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/InformationPopupImplementation.cs @@ -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("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace(Environment.NewLine, "
"); + 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("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace(Environment.NewLine, "
"); + 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) { diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/ListViewCache.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/ListViewCache.cs index 98b6f20..bb64770 100644 --- a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/ListViewCache.cs +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/ListViewCache.cs @@ -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 diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/MediaPlayerImpl.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/MediaPlayerImpl.cs index 056c8d7..1ec4e05 100644 --- a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/MediaPlayerImpl.cs +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/MediaPlayerImpl.cs @@ -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(_source); + IMediaSourceHandler handler = XForms.GetHandlerForObject(_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); diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/NativeCirclePage.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/NativeCirclePage.cs index 96aff05..ba681de 100644 --- a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/NativeCirclePage.cs +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/NativeCirclePage.cs @@ -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("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace(Environment.NewLine, "
"); _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; diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/PopupEntryRenderer.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/PopupEntryRenderer.cs index f7d0e63..6e4b6cf 100644 --- a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/PopupEntryRenderer.cs +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/PopupEntryRenderer.cs @@ -16,12 +16,14 @@ 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 index 0000000..bdeaa59 --- /dev/null +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/IShellItemRenderer.cs @@ -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 index 0000000..3e0cb46 --- /dev/null +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/NavigationDrawer.cs @@ -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 ShowAsync(EWidget target, Easing easing = null, uint length = 300, CancellationToken cancelltaionToken = default(CancellationToken)) + { + var tcs = new TaskCompletionSource(); + + 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 HideAsync(EWidget target, Easing easing = null, uint length = 300) + { + var tcs = new TaskCompletionSource(); + + 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(); + + 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 index 0000000..aecddf4 --- /dev/null +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/NavigationView.cs @@ -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> _itemCache; + List _items = new List(); + + public NavigationView(EvasObject parent) : base(parent) + { + InitializeComponent(); + } + + public event EventHandler ItemSelected; + + public event EventHandler 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> 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 $"{text}"; + 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> 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 index 0000000..d6f31cb --- /dev/null +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellContentRenderer.cs @@ -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 index 0000000..9f74894 --- /dev/null +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellItemRenderer.cs @@ -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 _rendererCache = new Dictionary(); + + 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 index 0000000..dca82a1 --- /dev/null +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellRenderer.cs @@ -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 + { + NavigationDrawer _drawer; + NavigationView _navigationView; + + Dictionary _rendererCache = new Dictionary(); + + 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 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(); + + 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 index 0000000..e24034c --- /dev/null +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellRendererFactory.cs @@ -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 index 0000000..7b6d684 --- /dev/null +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellSectionItemsRenderer.cs @@ -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 _items = new List(); + + 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 index 0000000..7a860e7 --- /dev/null +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/Shell/ShellSectionNavigationRenderer.cs @@ -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(); + SetLayoutCallback(OnLayout); + } + + List InternalStack { get; set; } + + public IReadOnlyList 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(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(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; + } + } + } +} diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/TizenCircleSurfaceEffect.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/TizenCircleSurfaceEffect.cs index f2d0b1b..1d54156 100644 --- a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/TizenCircleSurfaceEffect.cs +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/TizenCircleSurfaceEffect.cs @@ -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; diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/TwoButtonPopupImplementation.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/TwoButtonPopupImplementation.cs index 632dd82..078ab7a 100644 --- a/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/TwoButtonPopupImplementation.cs +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms.Renderer/TwoButtonPopupImplementation.cs @@ -332,12 +332,20 @@ namespace Tizen.Wearable.CircularUI.Forms.Renderer void UpdateTitle() { - _layout.SetPartText("elm.text.title", _title); + string title = _title?.Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace(Environment.NewLine, "
"); + _layout.SetPartText("elm.text.title", title); } void UpdateText() { - _layout.SetPartText("elm.text", _text); + string text = _text?.Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace(Environment.NewLine, "
"); + _layout.SetPartText("elm.text", text); } public void Show() diff --git a/src/XSF/Tizen.Wearable.CircularUI.Forms/CirclePage.cs b/src/XSF/Tizen.Wearable.CircularUI.Forms/CirclePage.cs index dbf3c4c..77c8668 100644 --- a/src/XSF/Tizen.Wearable.CircularUI.Forms/CirclePage.cs +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms/CirclePage.cs @@ -14,10 +14,10 @@ * 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 /// 4 public IList CircleSurfaceItems { get; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool Appeared { get; set; } + /// /// Gets or sets ActionButton that presents a menu item and associates it with a command /// @@ -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 index 0000000..ccdc95b --- /dev/null +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms/CircleSurfaceView.cs @@ -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 CircleSurfaceItems { get; internal set; } + + public CircleSurfaceView() + { + var circleSurfaceItems = new ObservableCollection(); + 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 index 0000000..54b6ff3 --- /dev/null +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms/CircularShell.cs @@ -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 index 0000000..47b9e2a --- /dev/null +++ b/src/XSF/Tizen.Wearable.CircularUI.Forms/ContentButton.cs @@ -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 +{ + /// + /// The ContentButton is a Button, which allows you to customize the View to be displayed. + /// + /// 4 + public class ContentButton : ContentView, IButtonController + { + /// + /// BindableProperty. Identifies the Command bindable property. + /// + /// 4 + public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(ContentButton), null, + propertyChanging: OnCommandChanging, propertyChanged: OnCommandChanged); + + /// + /// BindableProperty. Identifies the CommandParameter bindable property. + /// + /// 4 + public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(nameof(CommandParameter), typeof(object), typeof(ContentButton), null, + propertyChanged: (bindable, oldvalue, newvalue) => CommandCanExcuteChanged(bindable, EventArgs.Empty)); + + /// + /// Gets or sets command that is executed when the button is clicked. + /// + /// 4 + public ICommand Command + { + get => (ICommand)GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } + + /// + /// Gets or sets command paramter that is executed when the button is clicked. + /// + /// 4 + public object CommandParameter + { + get => GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); + } + + /// + /// Occurs when the button is clicked. + /// + /// 4 + public event EventHandler Clicked; + + /// + /// Occurs when the button is pressed. + /// + /// 4 + public event EventHandler Pressed; + + /// + /// Occurs when the button is released. + /// + /// 4 + public event EventHandler Released; + + bool IsEnabledCore + { + set => SetValueCore(IsEnabledProperty, value); + } + + /// + /// For internal use. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public void SendClicked() + { + if (IsEnabled) + { + Command?.Execute(CommandParameter); + Clicked?.Invoke(this, EventArgs.Empty); + } + } + + /// + /// For internal use. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public void SendPressed() + { + if (IsEnabled) + { + Pressed?.Invoke(this, EventArgs.Empty); + } + } + + /// + /// For internal use. + /// + [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); + } + } + } +} diff --git a/src/XSF/XSF.csproj b/src/XSF/XSF.csproj index 49d1c22..7ee4110 100644 --- a/src/XSF/XSF.csproj +++ b/src/XSF/XSF.csproj @@ -20,6 +20,7 @@ + \ No newline at end of file diff --git a/src/XSF/Xamarin.Forms.Platform.Tizen/Native/EmbeddingControls.cs b/src/XSF/Xamarin.Forms.Platform.Tizen/Native/EmbeddingControls.cs index f50b0b3..52cee7c 100644 --- a/src/XSF/Xamarin.Forms.Platform.Tizen/Native/EmbeddingControls.cs +++ b/src/XSF/Xamarin.Forms.Platform.Tizen/Native/EmbeddingControls.cs @@ -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; } diff --git a/src/XSF/Xamarin.Forms.Platform.Tizen/Renderers/RefreshViewRenderer.cs b/src/XSF/Xamarin.Forms.Platform.Tizen/Renderers/RefreshViewRenderer.cs index 38b7d6b..9b58aed 100644 --- a/src/XSF/Xamarin.Forms.Platform.Tizen/Renderers/RefreshViewRenderer.cs +++ b/src/XSF/Xamarin.Forms.Platform.Tizen/Renderers/RefreshViewRenderer.cs @@ -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; diff --git a/src/XSF/Xamarin.Forms.Platform.Tizen/Shell/ShellItemRenderer.cs b/src/XSF/Xamarin.Forms.Platform.Tizen/Shell/ShellItemRenderer.cs index bc06fb1..4ed6201 100644 --- a/src/XSF/Xamarin.Forms.Platform.Tizen/Shell/ShellItemRenderer.cs +++ b/src/XSF/Xamarin.Forms.Platform.Tizen/Shell/ShellItemRenderer.cs @@ -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) { diff --git a/src/XSF/Xamarin.Forms.Platform.Tizen/Shell/ShellNavBar.cs b/src/XSF/Xamarin.Forms.Platform.Tizen/Shell/ShellNavBar.cs index d14215d..5693152 100644 --- a/src/XSF/Xamarin.Forms.Platform.Tizen/Shell/ShellNavBar.cs +++ b/src/XSF/Xamarin.Forms.Platform.Tizen/Shell/ShellNavBar.cs @@ -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; -- 2.7.4