[NUI] Scrollbar uses View instead Visual since blinking issue (#2735)
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI.Wearable / src / public / CircularScrollbar.cs
1 /*
2  * Copyright(c) 2020 Samsung Electronics Co., Ltd.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  *
16  */
17 using System;
18 using System.ComponentModel;
19 using Tizen.NUI.BaseComponents;
20 using Tizen.NUI.Binding;
21 using Tizen.NUI.Components;
22
23 namespace Tizen.NUI.Wearable
24 {
25     /// <summary>
26     /// The CircualrScrollbar is a wearable NUI component that can be linked to the scrollable objects
27     /// indicating the current scroll position of the scrollable object.<br />
28     /// </summary>
29     [EditorBrowsable(EditorBrowsableState.Never)]
30     public class CircularScrollbar : ScrollbarBase
31     {
32         #region Fields
33
34         /// <summary>Bindable property of Thickness</summary>
35         [EditorBrowsable(EditorBrowsableState.Never)]
36         public static readonly BindableProperty ThicknessProperty = BindableProperty.Create(nameof(Thickness), typeof(float), typeof(CircularScrollbar), default(float), propertyChanged: (bindable, oldValue, newValue) =>
37         {
38             var instance = ((CircularScrollbar)bindable);
39             float value = (float?)newValue ?? 0;
40             instance.CurrentStyle.Thickness = value;
41             instance.UpdateVisualThickness(value);
42         },
43         defaultValueCreator: (bindable) =>
44         {
45             var instance = (CircularScrollbar)bindable;
46             return instance.CurrentStyle.Thickness ?? 0;
47         });
48
49         /// <summary>Bindable property of TrackSweepAngle</summary>
50         [EditorBrowsable(EditorBrowsableState.Never)]
51         public static readonly BindableProperty TrackSweepAngleProperty = BindableProperty.Create(nameof(TrackSweepAngle), typeof(float), typeof(CircularScrollbar), default(float), propertyChanged: (bindable, oldValue, newValue) =>
52         {
53             var instance = ((CircularScrollbar)bindable);
54             float value = (float?)newValue ?? 0;
55             instance.CurrentStyle.TrackSweepAngle = value;
56             instance.UpdateTrackVisualSweepAngle(value);
57         },
58         defaultValueCreator: (bindable) =>
59         {
60             var instance = (CircularScrollbar)bindable;
61             return instance.CurrentStyle.TrackSweepAngle ?? 0;
62         });
63
64         /// <summary>Bindable property of TrackColor</summary>
65         [EditorBrowsable(EditorBrowsableState.Never)]
66         public static readonly BindableProperty TrackColorProperty = BindableProperty.Create(nameof(TrackColor), typeof(Color), typeof(CircularScrollbar), null, propertyChanged: (bindable, oldValue, newValue) =>
67         {
68             var instance = ((CircularScrollbar)bindable);
69             instance.CurrentStyle.TrackColor = (Color)newValue;
70             instance.UpdateTrackVisualColor((Color)newValue);
71         },
72         defaultValueCreator: (bindable) =>
73         {
74             return ((CircularScrollbar)bindable).CurrentStyle.TrackColor;
75         });
76
77         /// <summary>Bindable property of ThumbColor</summary>
78         [EditorBrowsable(EditorBrowsableState.Never)]
79         public static readonly BindableProperty ThumbColorProperty = BindableProperty.Create(nameof(ThumbColor), typeof(Color), typeof(CircularScrollbar), null, propertyChanged: (bindable, oldValue, newValue) =>
80         {
81             var instance = ((CircularScrollbar)bindable);
82             instance.CurrentStyle.ThumbColor = (Color)newValue;
83             instance.UpdateThumbVisualColor((Color)newValue);
84         },
85         defaultValueCreator: (bindable) =>
86         {
87             return ((CircularScrollbar)bindable).CurrentStyle.ThumbColor;
88         });
89
90         private ArcVisual trackVisual;
91         private ArcVisual thumbVisual;
92         private float contentLength;
93         private float visibleLength;
94         private float previousPosition;
95         private float currentPosition;
96         private float directionAlpha;
97         private Size containerSize = new Size(0, 0);
98         private Animation thumbStartAngleAnimation;
99         private Animation thumbSweepAngleAnimation;
100         private bool mScrollEnabled = true;
101
102         #endregion Fields
103
104
105         #region Constructors
106
107         /// <summary>
108         /// Create an empty CircularScrollbar.
109         /// </summary>
110         public CircularScrollbar() : base()
111         {
112         }
113
114         /// <summary>
115         /// Create a CircularScrollbar and initialize with properties.
116         /// </summary>
117         /// <param name="contentLength">The length of the scrollable content area.</param>
118         /// <param name="viewportLength">The length of the viewport representing the amount of visible content.</param>
119         /// <param name="currentPosition">The current position of the viewport in scrollable content area. This is the viewport's top position if the scroller is vertical, otherwise, left.</param>
120         /// <param name="isHorizontal">Whether the direction of scrolling is horizontal or not. It is vertical by default.</param>
121         [EditorBrowsable(EditorBrowsableState.Never)]
122         public CircularScrollbar(float contentLength, float viewportLength, float currentPosition, bool isHorizontal = false) : base()
123         {
124             Initialize(contentLength, viewportLength, currentPosition, isHorizontal);
125         }
126
127         /// <summary>
128         /// Create an empty CircularScrollbar with a CircularScrollbarStyle instance to set style properties.
129         /// </summary>
130         [EditorBrowsable(EditorBrowsableState.Never)]
131         public CircularScrollbar(CircularScrollbarStyle style) : base(style)
132         {
133         }
134
135         /// <summary>
136         /// Static constructor to initialize bindable properties when loading.
137         /// </summary>
138         static CircularScrollbar()
139         {
140         }
141
142         #endregion Constructors
143
144
145         #region Properties
146
147         /// <summary>
148         /// Return a copied Style instance of CircularScrollbar
149         /// </summary>
150         /// <remarks>
151         /// It returns copied Style instance and changing it does not effect to the CircularScrollbar.
152         /// Style setting is possible by using constructor or the function of ApplyStyle(ViewStyle viewStyle)
153         /// </remarks>
154         [EditorBrowsable(EditorBrowsableState.Never)]
155         public CircularScrollbarStyle Style
156         {
157             get
158             {
159                 var result = new CircularScrollbarStyle(ViewStyle as CircularScrollbarStyle);
160                 result.CopyPropertiesFromView(this);
161                 return result;
162             }
163         }
164
165         /// <summary>
166         /// The thickness of the scrollbar and track.
167         /// </summary>
168         [EditorBrowsable(EditorBrowsableState.Never)]
169         public float Thickness
170         {
171             get => (float)GetValue(ThicknessProperty);
172             set => SetValue(ThicknessProperty, value);
173         }
174
175         /// <summary>
176         /// The sweep angle of track area in degrees.
177         /// </summary>
178         /// <remarks>
179         /// Values below 6 degrees are treated as 6 degrees.
180         /// Values exceeding 180 degrees are treated as 180 degrees.
181         /// </remarks>
182         [EditorBrowsable(EditorBrowsableState.Never)]
183         public float TrackSweepAngle
184         {
185             get => (float)GetValue(TrackSweepAngleProperty);
186             set => SetValue(TrackSweepAngleProperty, value);
187         }
188
189         /// <summary>
190         /// The color of the track part.
191         /// </summary>
192         [EditorBrowsable(EditorBrowsableState.Never)]
193         public Color TrackColor
194         {
195             get => (Color)GetValue(TrackColorProperty);
196             set => SetValue(TrackColorProperty, value);
197         }
198
199         /// <summary>
200         /// The color of the thumb part.
201         /// </summary>
202         [EditorBrowsable(EditorBrowsableState.Never)]
203         public Color ThumbColor
204         {
205             get => (Color)GetValue(ThumbColorProperty);
206             set => SetValue(ThumbColorProperty, value);
207         }
208
209         private CircularScrollbarStyle CurrentStyle => ViewStyle as CircularScrollbarStyle;
210
211         #endregion Properties
212
213
214         #region Methods
215
216         /// <inheritdoc/>
217         [EditorBrowsable(EditorBrowsableState.Never)]
218         public override void Initialize(float contentLength, float viewportLenth, float currentPosition, bool isHorizontal = false)
219         {
220             this.contentLength = contentLength > 0.0f ? contentLength : 0.0f;
221             this.visibleLength = viewportLenth;
222             this.currentPosition = currentPosition;
223             this.directionAlpha = isHorizontal ? 270.0f : 0.0f;
224
225             thumbStartAngleAnimation?.Stop();
226             thumbStartAngleAnimation = null;
227
228             thumbSweepAngleAnimation?.Stop();
229             thumbSweepAngleAnimation = null;
230
231
232             float trackSweepAngle = CalculateTrackSweepAngle(TrackSweepAngle);
233             float trackStartAngle = CalculateTrackStartAngle(trackSweepAngle);
234             float thumbSweepAngle = CalculateThumbSweepAngle(TrackSweepAngle);
235             float thumbStartAngle = CalculateThumbStartAngle(currentPosition, trackStartAngle, trackSweepAngle, thumbSweepAngle);
236
237             if (trackVisual == null)
238             {
239                 trackVisual = new ArcVisual
240                 {
241                     SuppressUpdateVisual = true,
242                     Thickness = this.Thickness,
243                     Cap = ArcVisual.CapType.Round,
244                     MixColor = TrackColor,
245                     Size = containerSize - new Size(2, 2),
246                     SizePolicy = VisualTransformPolicyType.Absolute,
247                     SweepAngle = trackSweepAngle,
248                     StartAngle = trackStartAngle,
249                 };
250
251                 AddVisual("Track", trackVisual);
252             }
253             else
254             {
255                 trackVisual.SweepAngle = trackSweepAngle;
256                 trackVisual.StartAngle = trackStartAngle;
257                 trackVisual.UpdateVisual(true);
258             }
259
260             if (thumbVisual == null)
261             {
262                 thumbVisual = new ArcVisual
263                 {
264                     SuppressUpdateVisual = true,
265                     Thickness = trackVisual.Thickness,
266                     Cap = ArcVisual.CapType.Round,
267                     MixColor = ThumbColor,
268                     Size = containerSize - new Size(2, 2),
269                     SizePolicy = VisualTransformPolicyType.Absolute,
270                     SweepAngle = thumbSweepAngle,
271                     StartAngle = thumbStartAngle,
272                     Opacity = CalculateThumbVisibility() ? 1.0f : 0.0f,
273                 };
274
275                 AddVisual("Thumb", thumbVisual);
276             }
277             else
278             {
279                 thumbVisual.SweepAngle = thumbSweepAngle;
280                 thumbVisual.StartAngle = thumbStartAngle;
281                 thumbVisual.UpdateVisual(true);
282             }
283         }
284
285         /// <inheritdoc/>
286         [EditorBrowsable(EditorBrowsableState.Never)]
287         public override void Update(float contentLength, float viewportLength, float position, uint durationMs = 0, AlphaFunction alphaFunction = null)
288         {
289             this.visibleLength = viewportLength;
290             Update(contentLength, position, durationMs, alphaFunction);
291         }
292
293         /// <inheritdoc/>
294         [EditorBrowsable(EditorBrowsableState.Never)]
295         public override void Update(float contentLength, float position, uint durationMs = 0, AlphaFunction alphaFunction = null)
296         {
297             this.previousPosition = this.currentPosition;
298             this.currentPosition = position;
299             this.contentLength = contentLength > 0.0f ? contentLength : 0.0f;
300
301             if (thumbVisual == null)
302             {
303                 return;
304             }
305
306             thumbVisual.SweepAngle = CalculateThumbSweepAngle(TrackSweepAngle);
307             thumbVisual.StartAngle = CalculateThumbStartAngle(position, trackVisual.StartAngle, trackVisual.SweepAngle, thumbVisual.SweepAngle);
308             thumbVisual.Opacity = CalculateThumbVisibility() ? 1.0f : 0.0f;
309
310             if (durationMs == 0)
311             {
312                 thumbVisual.UpdateVisual(true);
313
314                 return;
315             }
316
317             // TODO Support non built-in alpha function for visual trainsition in DALi.
318             AlphaFunction.BuiltinFunctions builtinAlphaFunction = alphaFunction?.GetBuiltinFunction() ?? AlphaFunction.BuiltinFunctions.Default;
319
320             thumbStartAngleAnimation?.Stop();
321             thumbStartAngleAnimation = AnimateVisual(thumbVisual, "startAngle", thumbVisual.StartAngle, 0, (int)durationMs, builtinAlphaFunction);
322             thumbStartAngleAnimation.Play();
323
324             thumbSweepAngleAnimation?.Stop();
325             thumbSweepAngleAnimation = AnimateVisual(thumbVisual, "sweepAngle", thumbVisual.SweepAngle, 0, (int)durationMs, builtinAlphaFunction);
326             thumbSweepAngleAnimation.Play();
327         }
328
329         /// <inheritdoc/>
330         [EditorBrowsable(EditorBrowsableState.Never)]
331         public override void ScrollTo(float position, uint durationMs = 0, AlphaFunction alphaFunction = null)
332         {
333             previousPosition = currentPosition;
334             currentPosition = position;
335
336             if (mScrollEnabled == false)
337             {
338                 return;
339             }
340
341             if (thumbVisual == null)
342             {
343                 return;
344             }
345
346             var oldThumbStartAngle = thumbVisual.StartAngle;
347
348             thumbVisual.StartAngle = CalculateThumbStartAngle(position, trackVisual.StartAngle, trackVisual.SweepAngle, thumbVisual.SweepAngle);
349
350             if (durationMs == 0)
351             {
352                 thumbVisual.UpdateVisual(true);
353
354                 return;
355             }
356
357             // TODO Support non built-in alpha function for visual trainsition in DALi.
358             AlphaFunction.BuiltinFunctions builtinAlphaFunction = alphaFunction?.GetBuiltinFunction() ?? AlphaFunction.BuiltinFunctions.Default;
359
360             thumbStartAngleAnimation?.Stop();
361             thumbStartAngleAnimation = AnimateVisual(thumbVisual, "startAngle", thumbVisual.StartAngle, 0, (int)durationMs, builtinAlphaFunction);
362             thumbStartAngleAnimation.Play();
363         }
364
365         /// <inheritdoc/>
366         [EditorBrowsable(EditorBrowsableState.Never)]
367         public override void OnRelayout(Vector2 size, RelayoutContainer container)
368         {
369             base.OnRelayout(size, container);
370
371             if (size.Width == containerSize?.Width && size.Height == containerSize.Height)
372             {
373                 return;
374             }
375
376             containerSize = new Size(size.Width, size.Height);
377
378             if (trackVisual == null)
379             {
380                 return;
381             }
382
383             trackVisual.Size = containerSize - new Size(2, 2);
384             thumbVisual.Size = containerSize - new Size(2, 2);
385             
386             trackVisual.UpdateVisual(true);
387             thumbVisual.UpdateVisual(true);
388         }
389
390         /// <inheritdoc/>
391         [EditorBrowsable(EditorBrowsableState.Never)]
392         public override void ApplyStyle(ViewStyle viewStyle)
393         {
394             if (viewStyle == null) return;
395             if (viewStyle.WidthResizePolicy == null) viewStyle.WidthResizePolicy = ResizePolicyType.FillToParent;
396             if (viewStyle.HeightResizePolicy == null) viewStyle.HeightResizePolicy = ResizePolicyType.FillToParent;
397
398             base.ApplyStyle(viewStyle);
399         }
400
401         /// <inheritdoc/>
402         [EditorBrowsable(EditorBrowsableState.Never)]
403         protected override ViewStyle CreateViewStyle()
404         {
405             return new CircularScrollbarStyle();
406         }
407
408         private float CalculateTrackStartAngle(float currentTrackSweepAngle)
409         {
410             return ((180.0f - currentTrackSweepAngle) / 2.0f) + directionAlpha;
411         }
412
413         private float CalculateTrackSweepAngle(float inputTrackSweepAngle)
414         {
415             return Math.Min(Math.Max(inputTrackSweepAngle, 3), 180);
416         }
417
418         private float CalculateThumbStartAngle(float position, float trackStartAngle, float trackSweepAngle, float thumbSweepAngle)
419         {
420             float minAngle = trackStartAngle;
421             float maxAngle = trackStartAngle + trackSweepAngle - thumbSweepAngle;
422             float resultAngle = trackStartAngle + (trackSweepAngle * (position < 0.0f ? 0.0f : position) / contentLength);
423
424             return Math.Min(Math.Max(resultAngle, minAngle), maxAngle);
425         }
426
427         private float CalculateThumbSweepAngle(float trackSweepAngle)
428         {
429             return trackSweepAngle * visibleLength / contentLength;
430         }
431
432         private bool CalculateThumbVisibility()
433         {
434             return contentLength > visibleLength;
435         }
436
437         private void UpdateVisualThickness(float thickness)
438         {
439             if (trackVisual == null)
440             {
441                 return;
442             }
443
444             trackVisual.Thickness = thickness;
445             thumbVisual.Thickness = thickness;
446
447             trackVisual.UpdateVisual(true);
448             thumbVisual.UpdateVisual(true);
449         }
450
451         private void UpdateTrackVisualSweepAngle(float trackSweepAngle)
452         {
453             if (trackVisual == null || thumbVisual == null)
454             {
455                 return;
456             }
457
458             trackVisual.SweepAngle = CalculateTrackSweepAngle(trackSweepAngle);
459             trackVisual.StartAngle = CalculateTrackStartAngle(trackVisual.SweepAngle);
460
461             thumbVisual.SweepAngle = CalculateThumbSweepAngle(TrackSweepAngle);
462             thumbVisual.StartAngle = CalculateThumbStartAngle(currentPosition, trackVisual.StartAngle, trackVisual.SweepAngle, thumbVisual.SweepAngle);
463
464             trackVisual.UpdateVisual(true);
465             thumbVisual.UpdateVisual(true);
466         }
467
468         private void UpdateTrackVisualColor(Color trackColor)
469         {
470             if (trackVisual == null)
471             {
472                 return;
473             }
474
475             trackVisual.MixColor = trackColor;
476             trackVisual.UpdateVisual(true);
477         }
478
479         private void UpdateThumbVisualColor(Color thumbColor)
480         {
481             if (thumbVisual == null)
482             {
483                 return;
484             }
485
486             thumbVisual.MixColor = thumbColor;
487             thumbVisual.UpdateVisual(true);
488         }
489
490         /// <inheritdoc/>
491         [EditorBrowsable(EditorBrowsableState.Never)]
492         public override bool ScrollEnabled
493         {
494             get
495             {
496                 return mScrollEnabled;
497             }
498             set
499             {
500                 if (value != mScrollEnabled)
501                 {
502                     mScrollEnabled = value;
503                 }
504             }
505         }
506
507         /// <inheritdoc/>
508         [EditorBrowsable(EditorBrowsableState.Never)]
509         public override Position ScrollPosition
510         {
511             get
512             {
513                 bool isHorizontal = (directionAlpha == 270.0f) ? true : false;
514                 float length = Math.Min(Math.Max(currentPosition, 0.0f), contentLength - visibleLength);
515
516                 return (isHorizontal ? new Position(length, 0.0f) : new Position(0.0f, length));
517             }
518         }
519
520         /// <inheritdoc/>
521         [EditorBrowsable(EditorBrowsableState.Never)]
522         public override Position ScrollCurrentPosition
523         {
524             get
525             {
526                 bool isHorizontal = (directionAlpha == 270.0f) ? true : false;
527                 float length = Math.Min(Math.Max(currentPosition, 0.0f), contentLength - visibleLength);
528
529                 if (thumbStartAngleAnimation != null)
530                 {
531                     float progress = thumbStartAngleAnimation.CurrentProgress;
532                     float previousLength = Math.Min(Math.Max(previousPosition, 0.0f), contentLength - visibleLength);
533
534                     length = ((1.0f - progress) * previousLength) + (progress * length);
535                 }
536
537                 return (isHorizontal ? new Position(length, 0.0f) : new Position(0.0f, length));
538             }
539         }
540
541         #endregion Methods
542     }
543 }