[Tizen] Add RefreshView for Tizen (#8697)
authorSeungkeun Lee <sngn.lee@samsung.com>
Tue, 3 Dec 2019 10:37:19 +0000 (19:37 +0900)
committerRui Marinho <me@ruimarinho.net>
Tue, 3 Dec 2019 10:37:19 +0000 (10:37 +0000)
* Add RefreshViewRenderer in Tizen

* RefreshView support ListView

* Support RefreshView on WebView

* Review updated

* Fix async method naming

* Update to handle ItemsViewRenderer

* Fix Reload window

Stubs/Xamarin.Forms.Platform.cs
Xamarin.Forms.Platform.Tizen/FormsApplication.cs
Xamarin.Forms.Platform.Tizen/Properties/AssemblyInfo.cs
Xamarin.Forms.Platform.Tizen/Renderers/RefreshViewRenderer.cs [new file with mode: 0644]
Xamarin.Forms.Platform.Tizen/Resource/refresh_48dp.png [new file with mode: 0644]
Xamarin.Forms.Platform.Tizen/StaticRegistrar.cs
Xamarin.Forms.Platform.Tizen/Xamarin.Forms.Platform.Tizen.csproj

index f08b127..24bf1f8 100644 (file)
@@ -164,9 +164,8 @@ namespace Xamarin.Forms.Platform
        [RenderWith (typeof (PhoneMasterDetailRenderer))]
 #endif
        internal class _MasterDetailPageRenderer { }
-#if !TIZEN4_0
+
        [RenderWith(typeof(RefreshViewRenderer))]
-#endif
        internal class _RefreshViewRenderer { }
 
        [RenderWith(typeof(SwipeViewRenderer))]
index c8d4e06..e9ef611 100644 (file)
@@ -57,7 +57,7 @@ namespace Xamarin.Forms.Platform.Tizen
                        if (methodInfo != null)
                        {
                                window = (Window)methodInfo.Invoke(null, new object[] { "FormsWindow" });
-                               BaseLayout = (ELayout)window.GetType().GetProperty("BaseLayout").GetValue(window);
+                               BaseLayout = (ELayout)window.GetType().GetProperty("BaseLayout")?.GetValue(window);
                        }
                        else
                        {
index ed82935..668c6ee 100644 (file)
@@ -40,6 +40,7 @@ using Xamarin.Forms.Platform.Tizen;
 [assembly: ExportRenderer(typeof(StructuredItemsView), typeof(StructuredItemsViewRenderer))]
 [assembly: ExportRenderer(typeof(CarouselView), typeof(CarouselViewRenderer))]
 [assembly: ExportRenderer(typeof(SwipeView), typeof(SwipeViewRenderer))]
+[assembly: ExportRenderer(typeof(RefreshView), typeof(RefreshViewRenderer))]
 
 [assembly: ExportImageSourceHandler(typeof(FileImageSource), typeof(FileImageSourceHandler))]
 [assembly: ExportImageSourceHandler(typeof(StreamImageSource), typeof(StreamImageSourceHandler))]
diff --git a/Xamarin.Forms.Platform.Tizen/Renderers/RefreshViewRenderer.cs b/Xamarin.Forms.Platform.Tizen/Renderers/RefreshViewRenderer.cs
new file mode 100644 (file)
index 0000000..8be1ff0
--- /dev/null
@@ -0,0 +1,370 @@
+using ElmSharp;
+using System;
+using System.Reflection;
+using System.Threading.Tasks;
+using TWebView = Tizen.WebView.WebView;
+
+namespace Xamarin.Forms.Platform.Tizen
+{
+       class RefreshIcon : ContentView
+       {
+               public static readonly int IconSize = 48;
+               static readonly Color DefaultColor = Color.FromHex("#6200EE");
+               static readonly string IconPath = "Xamarin.Forms.Platform.Tizen.Resource.refresh_48dp.png";
+
+               bool _isPlaying;
+               Image _icon;
+
+               public RefreshIcon()
+               {
+                       HeightRequest = IconSize;
+                       WidthRequest = IconSize;
+                       var layout = new AbsoluteLayout()
+                       {
+                               HeightRequest = IconSize,
+                               WidthRequest = IconSize,
+                       };
+
+                       layout.Children.Add(new BoxView
+                       {
+                               Color = Color.White,
+                               CornerRadius = new CornerRadius(IconSize),
+                       }, new Rectangle(0.5, 0.5, IconSize, IconSize), AbsoluteLayoutFlags.PositionProportional);
+
+                       _icon = new Image
+                       {
+                               Source = ImageSource.FromResource(IconPath, typeof(ShellItemRenderer).Assembly),
+                       };
+
+                       layout.Children.Add(_icon, new Rectangle(0.5, 0.5, IconSize - 8, IconSize - 8), AbsoluteLayoutFlags.PositionProportional);
+                       Content = layout;
+
+                       IconColor = DefaultColor;
+               }
+
+               public Color IconColor
+               {
+                       get
+                       {
+                               return PlatformConfiguration.TizenSpecific.Image.GetBlendColor(_icon);
+                       }
+                       set
+                       {
+                               PlatformConfiguration.TizenSpecific.Image.SetBlendColor(_icon, value == Color.Default ? DefaultColor : value);
+                       }
+               }
+
+               public double IconRotation
+               {
+                       get
+                       {
+                               return _icon.Rotation;
+                       }
+                       set
+                       {
+                               _icon.Rotation = value;
+                       }
+               }
+
+               public void Start()
+               {
+                       Stop();
+                       _isPlaying = true;
+                       TurnInternal();
+               }
+
+               public void Stop()
+               {
+                       _isPlaying = false;
+                       _icon.AbortAnimation("RotateTo");
+               }
+
+               async void TurnInternal()
+               {
+                       await _icon.RelRotateTo(360, 1000);
+                       if (_isPlaying)
+                               TurnInternal();
+               }
+       }
+
+       class RefreshLayout : StackLayout
+       {
+               static readonly int MaximumDistance = 100;
+
+               public RefreshLayout()
+               {
+                       HeightRequest = 200;
+                       HorizontalOptions = LayoutOptions.FillAndExpand;
+
+                       RefreshIcon = new RefreshIcon
+                       {
+                               HorizontalOptions = LayoutOptions.Center,
+                               VerticalOptions = LayoutOptions.Center,
+                               TranslationY = -RefreshIcon.IconSize, 
+                               Opacity = 0.5,
+                       };
+                       Children.Add(RefreshIcon);
+               }
+
+               RefreshIcon RefreshIcon { get; set; }
+
+               public Color RefreshIconColor
+               {
+                       get => RefreshIcon.IconColor;
+                       set => RefreshIcon.IconColor = value;
+               }
+
+               public void SetDistance(double distance)
+               {
+                       var calculated = -RefreshIcon.IconSize + distance;
+                       if (calculated > MaximumDistance)
+                               calculated = MaximumDistance;
+                       RefreshIcon.TranslationY = calculated;
+                       RefreshIcon.IconRotation = 180 * (calculated / (float)MaximumDistance);
+                       RefreshIcon.Opacity = 0.5 + (calculated / (float)MaximumDistance);
+               }
+
+               public void Start()
+               {
+                       _ = RefreshIcon.TranslateTo(0, MaximumDistance / 2.0, length:200);
+                       RefreshIcon.Start();
+               }
+
+               public bool ShouldRefresh()
+               {
+                       return RefreshIcon.TranslationY > (MaximumDistance - 30);
+               }
+
+               public async Task StopAsync()
+               {
+                       _ = RefreshIcon.FadeTo(0);
+                       await RefreshIcon.ScaleTo(0.2);
+                       RefreshIcon.Stop();
+               }
+
+               public async Task ResetRefreshIconAsync()
+               {
+                       new Animation((r) =>
+                       {
+                               RefreshIcon.IconRotation = 180 * (RefreshIcon.TranslationY / (float)MaximumDistance);
+                       }).Commit(RefreshIcon, "reset", length: 250);
+                       _ = RefreshIcon.FadeTo(0.5, length: 250);
+                       await RefreshIcon.TranslateTo(0, -RefreshIcon.IconSize, length: 250);
+               }
+       }
+
+       enum RefreshState
+       {
+               Idle,
+               Drag,
+               Loading,
+       }
+
+       public class RefreshViewRenderer : LayoutRenderer
+       {
+               GestureLayer _gestureLayer;
+
+               RefreshLayout _refreshLayout;
+               IVisualElementRenderer _refreshLayoutRenderer;
+
+               public RefreshViewRenderer()
+               {
+                       RegisterPropertyHandler(RefreshView.RefreshColorProperty, UpdateRefreshColor);
+                       RegisterPropertyHandler(RefreshView.IsRefreshingProperty, UpdateIsRefreshing);
+               }
+
+               RefreshView RefreshView => Element as RefreshView;
+               RefreshState RefreshState { get; set; }
+
+
+               protected override void OnElementChanged(ElementChangedEventArgs<Layout> e)
+               {
+                       base.OnElementChanged(e);
+                       Initialize();
+               }
+
+               void Initialize()
+               {
+                       _gestureLayer?.Unrealize();
+                       _gestureLayer = new GestureLayer(NativeView);
+                       _gestureLayer.Attach(NativeView);
+
+                       _gestureLayer.SetMomentumCallback(GestureLayer.GestureState.Move, OnMoved);
+                       _gestureLayer.SetMomentumCallback(GestureLayer.GestureState.End, OnEnd);
+                       _gestureLayer.SetMomentumCallback(GestureLayer.GestureState.Abort, OnEnd);
+               }
+
+               void UpdateRefreshLayout()
+               {
+                       _refreshLayout = new RefreshLayout();
+                       _refreshLayout.RefreshIconColor = RefreshView.RefreshColor;
+                       _refreshLayoutRenderer = Platform.GetOrCreateRenderer(_refreshLayout);
+                       (_refreshLayoutRenderer as LayoutRenderer).RegisterOnLayoutUpdated();
+
+                       Control.Children.Add(_refreshLayoutRenderer.NativeView);
+                       var measured = _refreshLayout.Measure(Element.Width, Element.Height);
+                       var parentBound = NativeView.Geometry;
+                       var bound = new Rect
+                       {
+                               X = parentBound.X,
+                               Y = parentBound.Y,
+                               Width = parentBound.Width,
+                               Height = Forms.ConvertToScaledPixel(measured.Request.Height)
+                       };
+
+                       _refreshLayoutRenderer.NativeView.Geometry = bound;
+                       RefreshState = RefreshState.Drag;
+               }
+
+               bool IsEdgeScrolling()
+               {
+                       if (RefreshView.Content is ScrollView scrollview)
+                       {
+                               if (scrollview.ScrollY == 0)
+                               {
+                                       return true;
+                               }
+                       }
+                       else if (Platform.GetRenderer(RefreshView.Content) is CarouselViewRenderer carouselViewRenderer)
+                       {
+                               var collectionView = carouselViewRenderer.NativeView;
+
+                               var scroller = collectionView.GetType().GetProperty("Scroller", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(collectionView);
+
+                               if (scroller != null)
+                               {
+                                       if ((scroller as Scroller)?.CurrentRegion.Y == 0)
+                                       {
+                                               return true;
+                                       }
+                               }
+                       }
+                       else if (Platform.GetRenderer(RefreshView.Content) is StructuredItemsViewRenderer itemsViewRenderer)
+                       {
+                               var collectionView = itemsViewRenderer.NativeView;
+
+                               var scroller = collectionView.GetType().GetProperty("Scroller", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(collectionView);
+
+                               if (scroller != null)
+                               {
+                                       if ((scroller as Scroller)?.CurrentRegion.Y == 0)
+                                       {
+                                               return true;
+                                       }
+                               }
+                       }
+                       else if (Platform.GetRenderer(RefreshView.Content) is ListViewRenderer listViewRenderer)
+                       {
+                               if (GetScrollYOnGenList(listViewRenderer.Control.RealHandle) == 0)
+                               {
+                                       return true;
+                               }
+                       }
+                       else if (Platform.GetRenderer(RefreshView.Content) is WebViewRenderer webviewRenderer)
+                       {
+                               if (GetScrollYOnWebView(webviewRenderer.Control.WebView) == 0)
+                               {
+                                       return true;
+                               }
+                       }
+
+                       return false;
+               }
+
+               int GetScrollYOnGenList(IntPtr handle)
+               {
+                       var interop = typeof(EvasObject).Assembly.GetType("Interop");
+                       var elementary = interop?.GetNestedType("Elementary", BindingFlags.NonPublic | BindingFlags.Static) ?? null;
+
+                       if (elementary != null)
+                       {
+                               object[] parameters = new object[] { handle, -1, -1, -1, -1 };
+                               elementary.GetMethod("elm_scroller_region_get", BindingFlags.NonPublic | BindingFlags.Static)?.Invoke(null, parameters);
+                               return (int)parameters[2];
+                       }
+                       return -1;
+               }
+
+               int GetScrollYOnWebView(TWebView webview)
+               {
+                       var property = webview.GetType().GetProperty("ScrollPosition");
+                       if (property != null)
+                       {
+                               var point = (ElmSharp.Point)property.GetValue(webview);
+                               return point.Y;
+                       }
+                       return -1;
+               }
+
+               void OnMoved(GestureLayer.MomentumData moment)
+               {
+                       if (RefreshState == RefreshState.Idle)
+                       {
+                               if (IsEdgeScrolling())
+                               {
+                                       UpdateRefreshLayout();
+                               }
+                       }
+
+                       if (RefreshState == RefreshState.Drag)
+                       {
+                               var dy = moment.Y2 - moment.Y1;
+                               _refreshLayout?.SetDistance(Forms.ConvertToScaledDP(dy));
+                       }
+               }
+
+               void OnEnd(GestureLayer.MomentumData moment)
+               {
+                       if (RefreshState == RefreshState.Drag && _refreshLayout != null && _refreshLayoutRenderer != null)
+                       {
+                               if (_refreshLayout.ShouldRefresh())
+                               {
+                                       _refreshLayout.Start();
+                                       RefreshState = RefreshState.Loading;
+                                       RefreshView.SetValueFromRenderer(RefreshView.IsRefreshingProperty, true);
+                               }
+                               else
+                               {
+                                       _ = ResetRefreshAsync();
+                               }
+                       }
+               }
+
+               async Task ResetRefreshAsync()
+               {
+                       var refreshLayout = _refreshLayout;
+                       var refreshIconRenderer = _refreshLayoutRenderer;
+                       _refreshLayout = null;
+                       _refreshLayoutRenderer = null;
+                       await refreshLayout.ResetRefreshIconAsync();
+                       refreshIconRenderer?.Dispose();
+                       RefreshState = RefreshState.Idle;
+               }
+
+               void UpdateRefreshColor()
+               {
+                       if (_refreshLayout != null)
+                       {
+                               _refreshLayout.RefreshIconColor = RefreshView.RefreshColor;
+                       }
+               }
+
+               async void UpdateIsRefreshing(bool init)
+               {
+                       if (init)
+                               return;
+
+                       if (!RefreshView.IsRefreshing && RefreshState == RefreshState.Loading)
+                       {
+                               var refreshLayout = _refreshLayout;
+                               var refreshIconRenderer = _refreshLayoutRenderer;
+                               _refreshLayout = null;
+                               _refreshLayoutRenderer = null;
+                               await refreshLayout?.StopAsync();
+                               refreshIconRenderer?.Dispose();
+
+                               RefreshState = RefreshState.Idle;
+                       }
+               }
+       }
+}
diff --git a/Xamarin.Forms.Platform.Tizen/Resource/refresh_48dp.png b/Xamarin.Forms.Platform.Tizen/Resource/refresh_48dp.png
new file mode 100644 (file)
index 0000000..2f76cd4
Binary files /dev/null and b/Xamarin.Forms.Platform.Tizen/Resource/refresh_48dp.png differ
index 0d10a7f..3b10009 100644 (file)
@@ -101,6 +101,7 @@ namespace Xamarin.Forms.Platform.Tizen
                        Registered.Register(typeof(StructuredItemsView), typeof(StructuredItemsViewRenderer));
                        Registered.Register(typeof(CarouselView), typeof(CarouselViewRenderer));
                        Registered.Register(typeof(SwipeView), typeof(SwipeViewRenderer));
+                       Registered.Register(typeof(RefreshView), typeof(RefreshViewRenderer));
 
                        //ImageSourceHandlers
                        Registered.Register(typeof(FileImageSource), typeof(FileImageSourceHandler));
@@ -136,4 +137,4 @@ namespace Xamarin.Forms.Platform.Tizen
                        }
                }
        }
-}
\ No newline at end of file
+}
index 6eb5f61..c51527b 100644 (file)
@@ -16,6 +16,7 @@
     <EmbeddedResource Include="Resource\arrow_left.png" />
     <EmbeddedResource Include="Resource\dots_horizontal.png" />
     <EmbeddedResource Include="Resource\menu.png" />
+    <EmbeddedResource Include="Resource\refresh_48dp.png" />
   </ItemGroup>
 
   <ItemGroup>