Support new features of Tizen.CircularUI (#188)
[platform/core/csapi/xsf.git] / src / XSF / Xamarin.Forms.Platform.Tizen / Renderers / RefreshViewRenderer.cs
1 using ElmSharp;
2 using System;
3 using System.Reflection;
4 using System.Threading.Tasks;
5 using TWebView = Tizen.WebView.WebView;
6
7 namespace Xamarin.Forms.Platform.Tizen
8 {
9         class RefreshIcon : ContentView
10         {
11                 public static readonly int IconSize = 48;
12                 static readonly Color DefaultColor = Color.FromHex("#6200EE");
13                 static readonly string IconPath = "XSF.Resources.refresh_48dp.png";
14
15                 bool _isPlaying;
16                 Image _icon;
17
18                 public RefreshIcon()
19                 {
20                         HeightRequest = IconSize;
21                         WidthRequest = IconSize;
22                         var layout = new AbsoluteLayout()
23                         {
24                                 HeightRequest = IconSize,
25                                 WidthRequest = IconSize,
26                         };
27
28                         layout.Children.Add(new BoxView
29                         {
30                                 Color = Color.White,
31                                 CornerRadius = new CornerRadius(IconSize),
32                         }, new Rectangle(0.5, 0.5, IconSize, IconSize), AbsoluteLayoutFlags.PositionProportional);
33
34                         _icon = new Image
35                         {
36                                 Source = ImageSource.FromResource(IconPath, typeof(ShellItemRenderer).Assembly),
37                         };
38
39                         layout.Children.Add(_icon, new Rectangle(0.5, 0.5, IconSize - 8, IconSize - 8), AbsoluteLayoutFlags.PositionProportional);
40                         Content = layout;
41
42                         IconColor = DefaultColor;
43                 }
44
45                 public Color IconColor
46                 {
47                         get
48                         {
49                                 return PlatformConfiguration.TizenSpecific.Image.GetBlendColor(_icon);
50                         }
51                         set
52                         {
53                                 PlatformConfiguration.TizenSpecific.Image.SetBlendColor(_icon, value == Color.Default ? DefaultColor : value);
54                         }
55                 }
56
57                 public double IconRotation
58                 {
59                         get
60                         {
61                                 return _icon.Rotation;
62                         }
63                         set
64                         {
65                                 _icon.Rotation = value;
66                         }
67                 }
68
69                 public void Start()
70                 {
71                         Stop();
72                         _isPlaying = true;
73                         TurnInternal();
74                 }
75
76                 public void Stop()
77                 {
78                         _isPlaying = false;
79                         _icon.AbortAnimation("RotateTo");
80                 }
81
82                 async void TurnInternal()
83                 {
84                         await _icon.RelRotateTo(360, 1000);
85                         if (_isPlaying)
86                                 TurnInternal();
87                 }
88         }
89
90         class RefreshLayout : StackLayout
91         {
92                 static readonly int MaximumDistance = 100;
93
94                 public RefreshLayout()
95                 {
96                         HeightRequest = 200;
97                         HorizontalOptions = LayoutOptions.FillAndExpand;
98
99                         RefreshIcon = new RefreshIcon
100                         {
101                                 HorizontalOptions = LayoutOptions.Center,
102                                 VerticalOptions = LayoutOptions.Center,
103                                 TranslationY = -RefreshIcon.IconSize,
104                                 Opacity = 0.5,
105                         };
106                         Children.Add(RefreshIcon);
107                 }
108
109                 RefreshIcon RefreshIcon { get; set; }
110
111                 public Color RefreshIconColor
112                 {
113                         get => RefreshIcon.IconColor;
114                         set => RefreshIcon.IconColor = value;
115                 }
116
117                 public void SetDistance(double distance)
118                 {
119                         var calculated = -RefreshIcon.IconSize + distance;
120                         if (calculated > MaximumDistance)
121                                 calculated = MaximumDistance;
122                         RefreshIcon.TranslationY = calculated;
123                         RefreshIcon.IconRotation = 180 * (calculated / (float)MaximumDistance);
124                         RefreshIcon.Opacity = 0.5 + (calculated / (float)MaximumDistance);
125                 }
126
127                 public void Start()
128                 {
129                         _ = RefreshIcon.TranslateTo(0, MaximumDistance / 2.0, length:200);
130                         RefreshIcon.Start();
131                 }
132
133                 public bool ShouldRefresh()
134                 {
135                         return RefreshIcon.TranslationY > (MaximumDistance - 30);
136                 }
137
138                 public async Task StopAsync()
139                 {
140                         _ = RefreshIcon.FadeTo(0);
141                         await RefreshIcon.ScaleTo(0.2);
142                         RefreshIcon.Stop();
143                 }
144
145                 public async Task ResetRefreshIconAsync()
146                 {
147                         new Animation((r) =>
148                         {
149                                 RefreshIcon.IconRotation = 180 * (RefreshIcon.TranslationY / (float)MaximumDistance);
150                         }).Commit(RefreshIcon, "reset", length: 250);
151                         _ = RefreshIcon.FadeTo(0.5, length: 250);
152                         await RefreshIcon.TranslateTo(0, -RefreshIcon.IconSize, length: 250);
153                 }
154         }
155
156         enum RefreshState
157         {
158                 Idle,
159                 Drag,
160                 Loading,
161         }
162
163         public class RefreshViewRenderer : LayoutRenderer
164         {
165                 GestureLayer _gestureLayer;
166
167                 RefreshLayout _refreshLayout;
168                 IVisualElementRenderer _refreshLayoutRenderer;
169
170                 public RefreshViewRenderer()
171                 {
172                         RegisterPropertyHandler(RefreshView.RefreshColorProperty, UpdateRefreshColor);
173                         RegisterPropertyHandler(RefreshView.IsRefreshingProperty, UpdateIsRefreshing);
174                 }
175
176                 RefreshView RefreshView => Element as RefreshView;
177                 RefreshState RefreshState { get; set; }
178
179
180                 protected override void OnElementChanged(ElementChangedEventArgs<Layout> e)
181                 {
182                         base.OnElementChanged(e);
183                         Initialize();
184                 }
185
186                 void Initialize()
187                 {
188                         _gestureLayer?.Unrealize();
189                         _gestureLayer = new GestureLayer(NativeView);
190                         _gestureLayer.Attach(NativeView);
191
192                         _gestureLayer.SetMomentumCallback(GestureLayer.GestureState.Move, OnMoved);
193                         _gestureLayer.SetMomentumCallback(GestureLayer.GestureState.End, OnEnd);
194                         _gestureLayer.SetMomentumCallback(GestureLayer.GestureState.Abort, OnEnd);
195                 }
196
197                 void UpdateRefreshLayout()
198                 {
199                         _refreshLayout = new RefreshLayout();
200                         _refreshLayout.RefreshIconColor = RefreshView.RefreshColor;
201                         _refreshLayoutRenderer = Platform.GetOrCreateRenderer(_refreshLayout);
202                         (_refreshLayoutRenderer as LayoutRenderer).RegisterOnLayoutUpdated();
203
204                         Control.Children.Add(_refreshLayoutRenderer.NativeView);
205                         var measured = _refreshLayout.Measure(Element.Width, Element.Height);
206                         var parentBound = NativeView.Geometry;
207                         var bound = new Rect
208                         {
209                                 X = parentBound.X,
210                                 Y = parentBound.Y,
211                                 Width = parentBound.Width,
212                                 Height = Forms.ConvertToScaledPixel(measured.Request.Height)
213                         };
214
215                         _refreshLayoutRenderer.NativeView.Geometry = bound;
216                         RefreshState = RefreshState.Drag;
217                 }
218
219                 bool IsEdgeScrolling()
220                 {
221                         if (RefreshView.Content is ScrollView scrollview)
222                         {
223                                 if (scrollview.ScrollY == 0)
224                                 {
225                                         return true;
226                                 }
227                         }
228                         else if (Platform.GetRenderer(RefreshView.Content) is CarouselViewRenderer carouselViewRenderer)
229                         {
230                                 var collectionView = carouselViewRenderer.NativeView;
231
232                                 var scroller = collectionView.GetType().GetProperty("Scroller", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(collectionView);
233
234                                 if (scroller != null)
235                                 {
236                                         if ((scroller as Scroller)?.CurrentRegion.Y == 0)
237                                         {
238                                                 return true;
239                                         }
240                                 }
241                         }
242                         else if (Platform.GetRenderer(RefreshView.Content) is StructuredItemsViewRenderer itemsViewRenderer)
243                         {
244                                 var collectionView = itemsViewRenderer.NativeView;
245
246                                 var scroller = collectionView.GetType().GetProperty("Scroller", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(collectionView);
247
248                                 if (scroller != null)
249                                 {
250                                         if ((scroller as Scroller)?.CurrentRegion.Y == 0)
251                                         {
252                                                 return true;
253                                         }
254                                 }
255                         }
256                         else if (Platform.GetRenderer(RefreshView.Content) is ListViewRenderer listViewRenderer)
257                         {
258                                 if (GetScrollYOnGenList(listViewRenderer.Control.RealHandle) == 0)
259                                 {
260                                         return true;
261                                 }
262                         }
263                         else if (Platform.GetRenderer(RefreshView.Content) is WebViewRenderer webviewRenderer)
264                         {
265                                 if (GetScrollYOnWebView(webviewRenderer.Control.WebView) == 0)
266                                 {
267                                         return true;
268                                 }
269                         }
270
271                         return false;
272                 }
273
274                 int GetScrollYOnGenList(IntPtr handle)
275                 {
276                         var interop = typeof(EvasObject).Assembly.GetType("Interop");
277                         var elementary = interop?.GetNestedType("Elementary", BindingFlags.NonPublic | BindingFlags.Static) ?? null;
278
279                         if (elementary != null)
280                         {
281                                 object[] parameters = new object[] { handle, -1, -1, -1, -1 };
282                                 elementary.GetMethod("elm_scroller_region_get", BindingFlags.NonPublic | BindingFlags.Static)?.Invoke(null, parameters);
283                                 return (int)parameters[2];
284                         }
285                         return -1;
286                 }
287
288                 int GetScrollYOnWebView(TWebView webview)
289                 {
290                         var property = webview.GetType().GetProperty("ScrollPosition");
291                         if (property != null)
292                         {
293                                 var point = (ElmSharp.Point)property.GetValue(webview);
294                                 return point.Y;
295                         }
296                         return -1;
297                 }
298
299                 void OnMoved(GestureLayer.MomentumData moment)
300                 {
301                         if (RefreshState == RefreshState.Idle)
302                         {
303                                 if (IsEdgeScrolling())
304                                 {
305                                         UpdateRefreshLayout();
306                                 }
307                         }
308
309                         if (RefreshState == RefreshState.Drag)
310                         {
311                                 var dy = moment.Y2 - moment.Y1;
312                                 _refreshLayout?.SetDistance(Forms.ConvertToScaledDP(dy));
313                         }
314                 }
315
316                 void OnEnd(GestureLayer.MomentumData moment)
317                 {
318                         if (RefreshState == RefreshState.Drag && _refreshLayout != null && _refreshLayoutRenderer != null)
319                         {
320                                 if (_refreshLayout.ShouldRefresh())
321                                 {
322                                         _refreshLayout.Start();
323                                         RefreshState = RefreshState.Loading;
324                                         RefreshView.SetValueFromRenderer(RefreshView.IsRefreshingProperty, true);
325                                 }
326                                 else
327                                 {
328                                         _ = ResetRefreshAsync();
329                                 }
330                         }
331                 }
332
333                 async Task ResetRefreshAsync()
334                 {
335                         var refreshLayout = _refreshLayout;
336                         var refreshIconRenderer = _refreshLayoutRenderer;
337                         _refreshLayout = null;
338                         _refreshLayoutRenderer = null;
339                         await refreshLayout.ResetRefreshIconAsync();
340                         refreshIconRenderer?.Dispose();
341                         RefreshState = RefreshState.Idle;
342                 }
343
344                 void UpdateRefreshColor()
345                 {
346                         if (_refreshLayout != null)
347                         {
348                                 _refreshLayout.RefreshIconColor = RefreshView.RefreshColor;
349                         }
350                 }
351
352                 async void UpdateIsRefreshing(bool init)
353                 {
354                         if (init)
355                                 return;
356
357                         if (!RefreshView.IsRefreshing && RefreshState == RefreshState.Loading)
358                         {
359                                 var refreshLayout = _refreshLayout;
360                                 var refreshIconRenderer = _refreshLayoutRenderer;
361                                 _refreshLayout = null;
362                                 _refreshLayoutRenderer = null;
363                                 await refreshLayout?.StopAsync();
364                                 refreshIconRenderer?.Dispose();
365
366                                 RefreshState = RefreshState.Idle;
367                         }
368                 }
369         }
370 }