11 February 2016

Xamarin.Forms Manual Carousel

A custom implementation of a carousel view without platform dependency

At Rocca. we are big fans of the framework provided by the team at Xamarin Inc, which allows us to bring our .NET codebase to multiple platforms greatly enhancing our productivity on mobile projects. So when we heard about Xamarin.Forms offering the ability to build native UI’s from a single codebase, it seemed like a no-brainer that we should make use of it. Hopefully saving ourselves potential headaches for future multi-platform development.

The Problem

What we have been given is a remarkable means of generating multi-platform apps with little to no platform-specific code in relatively short amounts of time. However as with any implementation, Xamarin.Forms is not without its own set of unique challenges, which we soon learned when attempting to implement a manually controlled carousel view.

Many modern websites utilise accordion design patterns to ease the user’s journey through complicated online forms. In a similar fashion, mobile apps often have a carousel effect for the various pages of content. In our case, we are in the process of building a Sport and Gym facility booking system for a client who needs a pleasant and eye catching means of guiding users through a detailed sign-up process.

The Solution

While Xamarin.Forms offers a CarouselPage layout which perfectly replicates the view navigation you may have come to love from the Windows Phone ecosystem, it does have limitations. Specifically the fact that like the Windows Phone layout, the user has complete control over the view. They have the ability to swipe between pages at their leisure which simply isn’t feasible for use with controlled navigation such as you might need for a form.

To remedy this, we set about creating our own Manually controlled carousel layout object by making use of the various layouts provided by the existing framework. What we have come up with is a layout object that can contain various varieties of view elements at whatever size we might need it with a slide transition from page to page.

We achieve the effect by initialising the layout with a Grid layout object with a single column and row, configured to clip its content without its bounds.

_baseLayout = new Grid () {
    ColumnDefinitions = new ColumnDefinitionCollection {
        new ColumnDefinition{ Width = new GridLength (1, GridUnitType.Star) }
    },
    RowDefinitions = new RowDefinitionCollection {
       new RowDefinition { Height = new GridLength (1, GridUnitType.Star) }
    },
    ColumnSpacing = 0,
    RowSpacing = 0,
    IsClippedToBounds = true
};

At this point it is important to populate the page content before calling the Initialise method otherwise we run the risk of the child layout components not being scaled properly to contain the child content.

ContentView page1 = new ContentView {
    BackgroundColor = Color.Red,
    HorizontalOptions = LayoutOptions.FillAndExpand,
    VerticalOptions = LayoutOptions.FillAndExpand,
    Content = new Button {
        Text = "Next Page",
        Command = new Command(() => {
            mcv.AdvancePage(1);
        })
    }
};

mvc.Pages = new List<Layout> {
    page1    
}

It is vital for the layout to function that both the internal current and new page containers are both placed in a Layout that will cause them to fill to the extents of their view container or the contents will not be scaled properly and the page transitions will perform incorrectly. This is why they are both placed in a Grid layout, in the same cell, effectively on top of each other so as to calculate layout information correctly.

Once the pages have been populated the Initialise() method must be called. That creates the two internal layout containers used to house your various pages and assign the first one with the content you assign as the StartPage.

_mainPage = new ContentView () {
    HorizontalOptions = LayoutOptions.FillAndExpand,
    VerticalOptions = LayoutOptions.FillAndExpand,
    Content = Pages [StartPage]
};
_mainPageSV = new ScrollView () {
    Orientation = ScrollOrientation.Vertical,
    HorizontalOptions = LayoutOptions.FillAndExpand,
    VerticalOptions = LayoutOptions.FillAndExpand,
    Content = _mainPage
};
_altPage = new ContentView () {
    HorizontalOptions = LayoutOptions.FillAndExpand,
    VerticalOptions = LayoutOptions.FillAndExpand,
    Content = new ContentView ()
};
_altPageSV = new ScrollView () {
    Orientation = ScrollOrientation.Vertical,
    HorizontalOptions = LayoutOptions.FillAndExpand,
    VerticalOptions = LayoutOptions.FillAndExpand,
    Content = _altPage
};

_baseLayout.Children.Add (_altPageSV, 0, 1, 0, 1);
_baseLayout.Children.Add (_mainPageSV, 0, 1, 0, 1);

After that you’re ready to make use of the layout, place it wherever you’d like, such as a ContentView or NavigationPage. To change pages, you can call the AdvancePage(int) method and pass in the number of pages you’d like to traverse, positive numbers will navigate forwards while negative navigate backwards.

// Navigate forward one page
mcv.AdvancePage(1);
// Navigate back two pages
mcv.AdvancePage(-2);

Navigation works by filling one of two alternating layout objects with the content you wish to display next from the Pages array you populate shortly after initialising the carousel layout. The next page layout is then positioned at the appropriate side of the current page, (either to the left or right) then both of the pages are animated to their new positions. The movements are all performed in a single batch operation as they would overwrite each other if not, resulting in them being displayed incorrectly.

// Prepare references to the pages to prepare for animation.
if (!_usingAltPage) {
    mainpgsv = _mainPageSV;
    mainpg = _mainPage;

    nextpgsv = _altPageSV;
    nextpg = _altPage;
} else {
    mainpgsv = _altPageSV;
    mainpg = _altPage;

    nextpgsv = _mainPageSV;
    nextpg = _mainPage;
}
_usingAltPage = !_usingAltPage;

// Prepare view dimentions each time the page changes so layout alterations are not then fixed.
_dimen = new Rectangle (mainpgsv.X, mainpgsv.Y, mainpgsv.Width, mainpgsv.Height);

// Prepare next page, move into position and scroll appropriately.
var cpageModel = Pages [_currentPage];
nextpg.Content = cpageModel;

// Aniamtion is about to occurr, perform the 'OnAppearing' code
var iNextPgContent = nextpg.Content as IManualCarouselPage;
var iMainPgContent = mainpg.Content as IManualCarouselPage;
Page_Appearing (iNextPgContent);
Page_Disappearing (iMainPgContent);

// Inform the animation framework that we have a batch of aniamtions to perform
mainpgsv.BatchBegin ();
nextpgsv.BatchBegin ();

nextpgsv.ScrollToAsync (0, 0, false);
if (direction >= 0) {
    nextpgsv.Layout (new Rectangle (_dimen.X + _dimen.Width, _dimen.Y, _dimen.Width, _dimen.Height));
} else {
    nextpgsv.Layout (new Rectangle (_dimen.X - _dimen.Width, _dimen.Y, _dimen.Width, _dimen.Height));
}

/* This is very important. Without it, interacting with entry elements in the view would cause 
 * the current page to swap beneath the previous page. This presumably is to do with the fact
 * that animations dont layout the views as android normally would, so they snap back into their
 * calculated positions when something (such as the keybaord) would cause the views to recalcualte. */
_baseLayout.RaiseChild(nextpgsv);

if (direction >= 0) {
    mainpgsv.LayoutTo (new Rectangle (_dimen.X - _dimen.Width, _dimen.Y, _dimen.Width, _dimen.Height), pageAnimationTime, Easing.CubicInOut);
    nextpgsv.LayoutTo (new Rectangle (_dimen.X, _dimen.Y, _dimen.Width, _dimen.Height), pageAnimationTime, Easing.CubicInOut).ContinueWith ((Task arg1) => {
        _animating = false;
        // Aniamtion is ending, alert the events
        Page_Disappeared(iMainPgContent);
        Page_Appeared(iNextPgContent);
    });
} else {
    mainpgsv.LayoutTo (new Rectangle (_dimen.X + _dimen.Width, _dimen.Y, _dimen.Width, _dimen.Height), pageAnimationTime, Easing.CubicInOut);
    nextpgsv.LayoutTo (new Rectangle (_dimen.X, _dimen.Y, _dimen.Width, _dimen.Height), pageAnimationTime, Easing.CubicInOut).ContinueWith ((Task arg1) => {
        _animating = false;
        // Aniamtion is ending, alert the events
        Page_Disappeared(iMainPgContent);
        Page_Appeared(iNextPgContent);
    });
}

// Commit the animations and begin!
mainpgsv.BatchCommit ();
nextpgsv.BatchCommit ();

We have also added in events for when pages are changed to allow you to track and update your content with this in mind.

The Happy Accident

And that is essentially all there is to it, utilising this relatively straight-forward Layout object, we have been able to replicate a carousel view for our forms and other content.

The beauty of it acting as a Layout object rather than a Page view is that it does not need to fill the entire page. For example, a Live tile-like effect could be achieved quite easily with this view, scrolling on a timer rather than by manual control, etc.

Even better than that is that because it makes use of existing layout options provided in the Xamarin.Forms framework, it does not require the implementation of custom renderers for each platform.

Example Videos

Get the Code

To have a go with it, check out our demo project on Github at: https://github.com/roccacreative/XamarinForms-Carousel