using System; using ElmSharp; using EColor = ElmSharp.Color; using ERectangle = ElmSharp.Rectangle; namespace Xamarin.Forms.Platform.Tizen { /// /// Renderer of a CarouselPage widget. /// public class CarouselPageRenderer : VisualElementRenderer, IVisualElementRenderer { /// /// The minimum length of a swipe to be recognized as a page switching command, in screen pixels unit. /// public static readonly double s_minimumSwipeLengthX = 200.0; // Different levels of "difficulty" in making a valid swipe gesture, determined by a maximum absolute value // of an angle between a line formed by the swipe gesture and the horizontal axis, in arc degrees: public static readonly double s_challengeEasyArcDegrees = 25.0; public static readonly double s_challengeComfortableArcDegrees = 20.0; public static readonly double s_challengeStandardArcDegrees = 15.0; public static readonly double s_challengeHardArcDegrees = 10.0; /// /// The maximum allowed angle between a line formed by the swipe gesture and the horizontal axis, in arc degrees. /// The gesture will be recognized as a page switching command if its angle does not exceed this value. /// public static readonly double s_thresholdSwipeArcDegrees = s_challengeComfortableArcDegrees; /// /// The tangent of a maximum allowed angle between the swipe line and the horizontal axis. /// public static readonly double s_thresholdSwipeTangent = Math.Tan(s_thresholdSwipeArcDegrees * (Math.PI / 180.0)); // A master container for the entire widget: protected Box _box; // Used for grabbing gestures over the entire screen, even if Page is smaller than it: protected ERectangle _filler; protected GestureLayer _gestureLayer; protected EvasObject _page; /// /// Invoked whenever the CarouselPage element has been changed in Xamarin. /// /// Event parameters. protected override void OnElementChanged(ElementChangedEventArgs e) { if (NativeView == null) { // Creates an overlaying box which serves as a container // for both page and a gesture handling layer: _box = new Box(Forms.Context.MainWindow) { IsHorizontal = false, }; _box.SetAlignment(-1, -1); _box.SetWeight(1, 1); _box.Show(); // Disallows the Box to lay out its contents. They will be laid out manually, // because the page has to overlay the conformant rectangle. By default // Box will lay its contents in a stack. Applying an empty method disables it: _box.SetLayoutCallback(() => { ResizeContentsToFullScreen(); }); // Creates a Rectangle used for ensuring that the gestures will get recognized: _filler = new ERectangle(Forms.Context.MainWindow) { Color = EColor.Transparent, }; _filler.SetAlignment(-1, -1); _filler.SetWeight(1, 1); _filler.Show(); _box.PackEnd(_filler); // Creates a GestureLayer used for swipe gestures recognition and attaches it to the Box: _gestureLayer = new GestureLayer(_box); _gestureLayer.Attach(_box); AddLineGestureHandler(); SetNativeControl(_box); } if (e.OldElement != null) { Element.CurrentPageChanged -= OnCurrentPageChanged; } if (e.NewElement != null) { Element.CurrentPageChanged += OnCurrentPageChanged; } // If pages have been added to the Xamarin widget and the user has not explicitly // marked one of them to be displayed, displays the first one: if (_page == null && Element.Children.Count > 0) { DisplayPage(Element.Children[0]); } base.OnElementChanged(e); } /// /// Called just before the associated element is deleted. /// /// True if the memory release was requested on demand. protected override void Dispose(bool disposing) { if (_box != null) { Element.CurrentPageChanged -= OnCurrentPageChanged; // Unpacks the page from the box to prevent it from being disposed of prematurely: _box.UnPack(_page); _box.Unrealize(); _box = null; } base.Dispose(disposing); } /// /// Handles the process of switching between the displayed pages. /// /// An object originating the request /// Additional arguments to the event handler void OnCurrentPageChanged(object sender, EventArgs ea) { if (_page != null) { _page.Hide(); _box.UnPack(_page); } DisplayPage(Element.CurrentPage); ResizeContentsToFullScreen(); } /// /// Gets the index of the currently displayed page in Element.Children collection. /// /// An int value representing the index of the page currently displayed, /// or -1 if no page is being displayed currently. int GetCurrentPageIndex() { int index = -1; for (int k = 0; k < Element.Children.Count; ++k) { if (Element.Children[k] == Element.CurrentPage) { index = k; break; } } return index; } /// /// Resizes the widget's contents to utilize all the available screen space. /// void ResizeContentsToFullScreen() { // Box's geometry should match Forms.Context.MainWindow's geometry // minus the space occupied by the top toolbar. // Applies Box's geometry to both displayed page and conformant rectangle: _filler.Geometry = _page.Geometry = _box.Geometry; } /// /// Adds the feature of recognizing swipes to the GestureLayer. /// void AddLineGestureHandler() { _gestureLayer.SetLineCallback(GestureLayer.GestureState.End, (line) => { double horizontalDistance = line.X2 - line.X1; double verticalDistance = line.Y2 - line.Y1; // Determines whether the movement is long enough to be considered a swipe: bool isLongEnough = (Math.Abs(horizontalDistance) >= s_minimumSwipeLengthX); // Determines whether the movement is horizontal enough to be considered as a swipe: // The swipe arc's tangent value (v/h) needs to be lesser than or equal to the threshold value. // This approach allows for getting rid of computationally heavier atan2() function. double angleTangent = Math.Abs(verticalDistance) / horizontalDistance; bool isDirectionForward = (angleTangent < 0); // Determines whether the movement has been recognized as a valid swipe: bool isSwipeMatching = (isLongEnough && (Math.Abs(angleTangent) <= s_thresholdSwipeTangent)); if (isSwipeMatching) { // TODO: Unsure whether changes made via ItemsSource/ItemTemplate properties will be handled correctly this way. // If not, it should be implemented in another method. if (isDirectionForward) { // Tries to switch the page to the next one: int currentPageIndex = GetCurrentPageIndex(); if (currentPageIndex < Element.Children.Count - 1) { // Sets the current page to the next one: Element.CurrentPage = Element.Children[currentPageIndex + 1]; } else { // Reacts to the case of forward-swiping when the last page is already being displayed: Log.Debug("CarouselPage: Displaying the last page already - can not revolve further."); // Note (TODO): Once we have a more sophisticated renderer able to e.g. display the animation // of revolving Pages or at least indicate current overall position, some visual feedback // should be provided here for the user who has haplessly tried to access a nonexistent page. } } else { // Tries to switch the page to the previous one: int currentPageIndex = GetCurrentPageIndex(); if (currentPageIndex > 0) { // Sets the current page to the previous one: Element.CurrentPage = Element.Children[currentPageIndex - 1]; } else { // Reacts to the case of backward-swiping when the first page is already being displayed: Log.Debug("CarouselPage: The first page is already being displayed - can not revolve further."); // Note (TODO): (The same as in case of scrolling forwards) } } } }); } void DisplayPage(ContentPage p) { _page = Platform.GetOrCreateRenderer(p).NativeView; _page.SetAlignment(-1, -1); _page.SetWeight(1, 1); _page.Show(); _box.PackEnd(_page); } } }