a44d3679d393f6c38a64f59673c998e1dec27db4
[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             ThemeManager.AddPackageTheme(new DefaultThemeCreator());
141         }
142
143         #endregion Constructors
144
145
146         #region Properties
147
148         /// <summary>
149         /// Return a copied Style instance of CircularScrollbar
150         /// </summary>
151         /// <remarks>
152         /// It returns copied Style instance and changing it does not effect to the CircularScrollbar.
153         /// Style setting is possible by using constructor or the function of ApplyStyle(ViewStyle viewStyle)
154         /// </remarks>
155         [EditorBrowsable(EditorBrowsableState.Never)]
156         public CircularScrollbarStyle Style
157         {
158             get
159             {
160                 var result = new CircularScrollbarStyle(ViewStyle as CircularScrollbarStyle);
161                 result.CopyPropertiesFromView(this);
162                 return result;
163             }
164         }
165
166         /// <summary>
167         /// The thickness of the scrollbar and track.
168         /// </summary>
169         [EditorBrowsable(EditorBrowsableState.Never)]
170         public float Thickness
171         {
172             get => (float)GetValue(ThicknessProperty);
173             set => SetValue(ThicknessProperty, value);
174         }
175
176         /// <summary>
177         /// The sweep angle of track area in degrees.
178         /// </summary>
179         /// <remarks>
180         /// Values below 6 degrees are treated as 6 degrees.
181         /// Values exceeding 180 degrees are treated as 180 degrees.
182         /// </remarks>
183         [EditorBrowsable(EditorBrowsableState.Never)]
184         public float TrackSweepAngle
185         {
186             get => (float)GetValue(TrackSweepAngleProperty);
187             set => SetValue(TrackSweepAngleProperty, value);
188         }
189
190         /// <summary>
191         /// The color of the track part.
192         /// </summary>
193         [EditorBrowsable(EditorBrowsableState.Never)]
194         public Color TrackColor
195         {
196             get => (Color)GetValue(TrackColorProperty);
197             set => SetValue(TrackColorProperty, value);
198         }
199
200         /// <summary>
201         /// The color of the thumb part.
202         /// </summary>
203         [EditorBrowsable(EditorBrowsableState.Never)]
204         public Color ThumbColor
205         {
206             get => (Color)GetValue(ThumbColorProperty);
207             set => SetValue(ThumbColorProperty, value);
208         }
209
210         private CircularScrollbarStyle CurrentStyle => ViewStyle as CircularScrollbarStyle;
211
212         #endregion Properties
213
214
215         #region Methods
216
217         /// <inheritdoc/>
218         [EditorBrowsable(EditorBrowsableState.Never)]
219         public override void Initialize(float contentLength, float viewportLenth, float currentPosition, bool isHorizontal = false)
220         {
221             this.contentLength = contentLength > 0.0f ? contentLength : 0.0f;
222             this.visibleLength = viewportLenth;
223             this.currentPosition = currentPosition;
224             this.directionAlpha = isHorizontal ? 270.0f : 0.0f;
225
226             thumbStartAngleAnimation?.Stop();
227             thumbStartAngleAnimation = null;
228
229             thumbSweepAngleAnimation?.Stop();
230             thumbSweepAngleAnimation = null;
231
232
233             float trackSweepAngle = CalculateTrackSweepAngle(TrackSweepAngle);
234             float trackStartAngle = CalculateTrackStartAngle(trackSweepAngle);
235             float thumbSweepAngle = CalculateThumbSweepAngle(TrackSweepAngle);
236             float thumbStartAngle = CalculateThumbStartAngle(currentPosition, trackStartAngle, trackSweepAngle, thumbSweepAngle);
237
238             if (trackVisual == null)
239             {
240                 trackVisual = new ArcVisual
241                 {
242                     SuppressUpdateVisual = true,
243                     Thickness = this.Thickness,
244                     Cap = ArcVisual.CapType.Round,
245                     MixColor = TrackColor,
246                     Size = containerSize - new Size(2, 2),
247                     SizePolicy = VisualTransformPolicyType.Absolute,
248                     SweepAngle = trackSweepAngle,
249                     StartAngle = trackStartAngle,
250                 };
251
252                 AddVisual("Track", trackVisual);
253             }
254             else
255             {
256                 trackVisual.SweepAngle = trackSweepAngle;
257                 trackVisual.StartAngle = trackStartAngle;
258                 trackVisual.UpdateVisual(true);
259             }
260
261             if (thumbVisual == null)
262             {
263                 thumbVisual = new ArcVisual
264                 {
265                     SuppressUpdateVisual = true,
266                     Thickness = trackVisual.Thickness,
267                     Cap = ArcVisual.CapType.Round,
268                     MixColor = ThumbColor,
269                     Size = containerSize - new Size(2, 2),
270                     SizePolicy = VisualTransformPolicyType.Absolute,
271                     SweepAngle = thumbSweepAngle,
272                     StartAngle = thumbStartAngle,
273                     Opacity = CalculateThumbVisibility() ? 1.0f : 0.0f,
274                 };
275
276                 AddVisual("Thumb", thumbVisual);
277             }
278             else
279             {
280                 thumbVisual.SweepAngle = thumbSweepAngle;
281                 thumbVisual.StartAngle = thumbStartAngle;
282                 thumbVisual.UpdateVisual(true);
283             }
284         }
285
286         /// <inheritdoc/>
287         [EditorBrowsable(EditorBrowsableState.Never)]
288         public override void Update(float contentLength, float position, uint durationMs = 0, AlphaFunction alphaFunction = null)
289         {
290             this.previousPosition = this.currentPosition;
291             this.currentPosition = position;
292             this.contentLength = contentLength > 0.0f ? contentLength : 0.0f;
293
294             if (thumbVisual == null)
295             {
296                 return;
297             }
298
299             thumbVisual.SweepAngle = CalculateThumbSweepAngle(TrackSweepAngle);
300             thumbVisual.StartAngle = CalculateThumbStartAngle(position, trackVisual.StartAngle, trackVisual.SweepAngle, thumbVisual.SweepAngle);
301             thumbVisual.Opacity = CalculateThumbVisibility() ? 1.0f : 0.0f;
302
303             if (durationMs == 0)
304             {
305                 thumbVisual.UpdateVisual(true);
306
307                 return;
308             }
309
310             // TODO Support non built-in alpha function for visual trainsition in DALi.
311             AlphaFunction.BuiltinFunctions builtinAlphaFunction = alphaFunction?.GetBuiltinFunction() ?? AlphaFunction.BuiltinFunctions.Default;
312
313             thumbStartAngleAnimation?.Stop();
314             thumbStartAngleAnimation = AnimateVisual(thumbVisual, "startAngle", thumbVisual.StartAngle, 0, (int)durationMs, builtinAlphaFunction);
315             thumbStartAngleAnimation.Play();
316
317             thumbSweepAngleAnimation?.Stop();
318             thumbSweepAngleAnimation = AnimateVisual(thumbVisual, "sweepAngle", thumbVisual.SweepAngle, 0, (int)durationMs, builtinAlphaFunction);
319             thumbSweepAngleAnimation.Play();
320         }
321
322         /// <inheritdoc/>
323         [EditorBrowsable(EditorBrowsableState.Never)]
324         public override void ScrollTo(float position, uint durationMs = 0, AlphaFunction alphaFunction = null)
325         {
326             previousPosition = currentPosition;
327             currentPosition = position;
328
329             if (mScrollEnabled == false)
330             {
331                 return;
332             }
333
334             if (thumbVisual == null)
335             {
336                 return;
337             }
338
339             var oldThumbStartAngle = thumbVisual.StartAngle;
340
341             thumbVisual.StartAngle = CalculateThumbStartAngle(position, trackVisual.StartAngle, trackVisual.SweepAngle, thumbVisual.SweepAngle);
342
343             if (durationMs == 0)
344             {
345                 thumbVisual.UpdateVisual(true);
346
347                 return;
348             }
349
350             // TODO Support non built-in alpha function for visual trainsition in DALi.
351             AlphaFunction.BuiltinFunctions builtinAlphaFunction = alphaFunction?.GetBuiltinFunction() ?? AlphaFunction.BuiltinFunctions.Default;
352
353             thumbStartAngleAnimation?.Stop();
354             thumbStartAngleAnimation = AnimateVisual(thumbVisual, "startAngle", thumbVisual.StartAngle, 0, (int)durationMs, builtinAlphaFunction);
355             thumbStartAngleAnimation.Play();
356         }
357
358         /// <inheritdoc/>
359         [EditorBrowsable(EditorBrowsableState.Never)]
360         public override void OnRelayout(Vector2 size, RelayoutContainer container)
361         {
362             base.OnRelayout(size, container);
363
364             if (size.Width == containerSize?.Width && size.Height == containerSize.Height)
365             {
366                 return;
367             }
368
369             containerSize = new Size(size.Width, size.Height);
370
371             if (trackVisual == null)
372             {
373                 return;
374             }
375
376             trackVisual.Size = containerSize - new Size(2, 2);
377             thumbVisual.Size = containerSize - new Size(2, 2);
378             
379             trackVisual.UpdateVisual(true);
380             thumbVisual.UpdateVisual(true);
381         }
382
383         /// <inheritdoc/>
384         [EditorBrowsable(EditorBrowsableState.Never)]
385         public override void ApplyStyle(ViewStyle viewStyle)
386         {
387             if (viewStyle == null) return;
388             if (viewStyle.WidthResizePolicy == null) viewStyle.WidthResizePolicy = ResizePolicyType.FillToParent;
389             if (viewStyle.HeightResizePolicy == null) viewStyle.HeightResizePolicy = ResizePolicyType.FillToParent;
390
391             base.ApplyStyle(viewStyle);
392         }
393
394         /// <inheritdoc/>
395         [EditorBrowsable(EditorBrowsableState.Never)]
396         protected override ViewStyle CreateViewStyle()
397         {
398             return new CircularScrollbarStyle();
399         }
400
401         private float CalculateTrackStartAngle(float currentTrackSweepAngle)
402         {
403             return ((180.0f - currentTrackSweepAngle) / 2.0f) + directionAlpha;
404         }
405
406         private float CalculateTrackSweepAngle(float inputTrackSweepAngle)
407         {
408             return Math.Min(Math.Max(inputTrackSweepAngle, 3), 180);
409         }
410
411         private float CalculateThumbStartAngle(float position, float trackStartAngle, float trackSweepAngle, float thumbSweepAngle)
412         {
413             float minAngle = trackStartAngle;
414             float maxAngle = trackStartAngle + trackSweepAngle - thumbSweepAngle;
415             float resultAngle = trackStartAngle + (trackSweepAngle * (position < 0.0f ? 0.0f : position) / contentLength);
416
417             return Math.Min(Math.Max(resultAngle, minAngle), maxAngle);
418         }
419
420         private float CalculateThumbSweepAngle(float trackSweepAngle)
421         {
422             return trackSweepAngle * visibleLength / contentLength;
423         }
424
425         private bool CalculateThumbVisibility()
426         {
427             return contentLength > visibleLength;
428         }
429
430         private void UpdateVisualThickness(float thickness)
431         {
432             if (trackVisual == null)
433             {
434                 return;
435             }
436
437             trackVisual.Thickness = thickness;
438             thumbVisual.Thickness = thickness;
439
440             trackVisual.UpdateVisual(true);
441             thumbVisual.UpdateVisual(true);
442         }
443
444         private void UpdateTrackVisualSweepAngle(float trackSweepAngle)
445         {
446             if (trackVisual == null || thumbVisual == null)
447             {
448                 return;
449             }
450
451             trackVisual.SweepAngle = CalculateTrackSweepAngle(trackSweepAngle);
452             trackVisual.StartAngle = CalculateTrackStartAngle(trackVisual.SweepAngle);
453
454             thumbVisual.SweepAngle = CalculateThumbSweepAngle(TrackSweepAngle);
455             thumbVisual.StartAngle = CalculateThumbStartAngle(currentPosition, trackVisual.StartAngle, trackVisual.SweepAngle, thumbVisual.SweepAngle);
456
457             trackVisual.UpdateVisual(true);
458             thumbVisual.UpdateVisual(true);
459         }
460
461         private void UpdateTrackVisualColor(Color trackColor)
462         {
463             if (trackVisual == null)
464             {
465                 return;
466             }
467
468             trackVisual.MixColor = trackColor;
469             trackVisual.UpdateVisual(true);
470         }
471
472         private void UpdateThumbVisualColor(Color thumbColor)
473         {
474             if (thumbVisual == null)
475             {
476                 return;
477             }
478
479             thumbVisual.MixColor = thumbColor;
480             thumbVisual.UpdateVisual(true);
481         }
482
483         /// <inheritdoc/>
484         [EditorBrowsable(EditorBrowsableState.Never)]
485         public override bool ScrollEnabled
486         {
487             get
488             {
489                 return mScrollEnabled;
490             }
491             set
492             {
493                 if (value != mScrollEnabled)
494                 {
495                     mScrollEnabled = value;
496                 }
497             }
498         }
499
500         /// <inheritdoc/>
501         [EditorBrowsable(EditorBrowsableState.Never)]
502         public override Position ScrollPosition
503         {
504             get
505             {
506                 bool isHorizontal = (directionAlpha == 270.0f) ? true : false;
507                 float length = Math.Min(Math.Max(currentPosition, 0.0f), contentLength - visibleLength);
508
509                 return (isHorizontal ? new Position(length, 0.0f) : new Position(0.0f, length));
510             }
511         }
512
513         /// <inheritdoc/>
514         [EditorBrowsable(EditorBrowsableState.Never)]
515         public override Position ScrollCurrentPosition
516         {
517             get
518             {
519                 bool isHorizontal = (directionAlpha == 270.0f) ? true : false;
520                 float length = Math.Min(Math.Max(currentPosition, 0.0f), contentLength - visibleLength);
521
522                 if (thumbStartAngleAnimation != null)
523                 {
524                     float progress = thumbStartAngleAnimation.CurrentProgress;
525                     float previousLength = Math.Min(Math.Max(previousPosition, 0.0f), contentLength - visibleLength);
526
527                     length = ((1.0f - progress) * previousLength) + (progress * length);
528                 }
529
530                 return (isHorizontal ? new Position(length, 0.0f) : new Position(0.0f, length));
531             }
532         }
533
534         #endregion Methods
535     }
536 }