As we were looking for a Horizontal list that will match the needs for our app, we reached the conclusion that none of the available options would satisfy our needs so we decided to build it and make it available for the community as well.
In this post we will discuss only the Xamarin.iOS part and for Xamarin.Android please check my friend’s, Mihai, post.
Preview
How to
We saw how it will look like, now let’s discuss how to get it done. My main purpose was to wrap the presented effects within a custom UICollectionView and leave the rest as customisable as possible. Let’s explain a bit how it works:
1. We set the horizontal scroll through a custom implementation of UICollectionViewFlowLayout
public class CustomFlowLayout : UICollectionViewFlowLayout
{
public CustomFlowLayout(nfloat width, nfloat height)
{
ItemSize = new CGSize(width, height);
ScrollDirection = UICollectionViewScrollDirection.Horizontal;
}
}
2. In order to get the elevation effect and force cells to overlap we override the LayoutAttibutesForElementsInRect inside our CustomFlowLayout:
public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect(CGRect rect)
{
var attributesArray = base.LayoutAttributesForElementsInRect(rect);
var numberOfItems = CollectionView.NumberOfItemsInSection(0);
foreach (var attributes in attributesArray)
{
var xPosition = attributes.Center.X;
var yPosition = attributes.Center.Y;
if (attributes.IndexPath.Row == 0)
attributes.ZIndex = int.MaxValue; // Put the first cell on top of the stack
else
{
xPosition -= 20 * attributes.IndexPath.Row;
attributes.ZIndex = numberOfItems - attributes.IndexPath.Row; //Other cells below the first one
}
attributes.Center = new CGPoint(xPosition, yPosition);
}
return attributesArray;
}
3. With elevation and orientation we move further on to create a custom implementation of UICollectionView and use the UICollectionView.Scrolled event in order to wire everything together:
private void XCarouselView_Scrolled(object sender, EventArgs e)
{
var visibleRect = new CGRect(ContentOffset, Bounds.Size);
var visiblePoint = new CGPoint(visibleRect.GetMidX(), visibleRect.GetMidY());
var visibleIndexPath = IndexPathForItemAtPoint(visiblePoint);
if (visibleIndexPath != null)
{
Animate(0.3, 0.0, UIViewAnimationOptions.AllowUserInteraction, () =>
{
if (CellForItem(visibleIndexPath) != null)
{
var middleCell = CellForItem(visibleIndexPath);
middleCell.Transform = CGAffineTransform.MakeScale(new nfloat(1.3), new nfloat(1.3));
}
foreach (var cell in VisibleCells)
{
var path = IndexPathForCell(cell);
var difference = path.LongRow - visibleIndexPath.LongRow;
if (difference == 0)
{
cell.Layer.ZPosition = int.MaxValue;
foreach (var subView in cell.ContentView.Subviews)
{
if (subView.GetType() == typeof(UIImageView))
{
if (baseCellImage == null)
baseCellImage = (subView as UIImageView).Image;
(subView as UIImageView).Image = baseCellImage;
}
}
}
if (difference == 1 || difference == -1)
UpdateCellLayer(cell, new nfloat(1.2), new nfloat(1.2), int.MinValue + 2, new nfloat(0.3));
else if (difference == 2 || difference == -2)
UpdateCellLayer(cell, new nfloat(1.1), new nfloat(1.1), int.MinValue + 1, new nfloat(0.5));
else if (difference != 0)
UpdateCellLayer(cell, 1, 1, int.MinValue, new nfloat(0.7));
}
}, null);
}
}
Now let’s discuss a bit about what is happening above:
- Why Animate? In order to make transition between states much smoother.
- Why CGAffineTransform.MakeScale? As the elevation was not enough, we have to scale the item as well.
- UpdateCellLayer and baseCellImage? The cells around the central one will get the fade effect, but because the images are not a regular shape and that alfa would result into an unwanted behaviour (read here what happens), we found that the only way is to edit the UIImage itself. See below what’s happening within the UpdateCellLayer:
private void UpdateCellLayer(UICollectionViewCell cell, nfloat sx, nfloat sy, int zPosition, nfloat alpha)
{
cell.Transform = CGAffineTransform.MakeScale(sx, sy);
cell.Layer.ZPosition = zPosition;
foreach (var subView in cell.ContentView.Subviews)
{
if (subView.GetType() == typeof(UIImageView))
{
var newImage = ChangeImageColor(baseCellImage, alpha, FadeColor);
(subView as UIImageView).Image = newImage;
}
}
}
private UIImage ChangeImageColor(UIImage image, nfloat alpha, UIColor color)
{
var alphaColor = color.ColorWithAlpha(alpha);
UIGraphics.BeginImageContextWithOptions(image.Size, false, UIScreen.MainScreen.Scale);
var context = UIGraphics.GetCurrentContext();
alphaColor.SetFill();
context.TranslateCTM(0, image.Size.Height);
context.ScaleCTM(new nfloat(1.0), new nfloat(-1.0));
context.SetBlendMode(CGBlendMode.Lighten);
var rect = new CGRect(0, 0, image.Size.Width, image.Size.Height);
context.DrawImage(rect, image.CGImage);
context.SetBlendMode(CGBlendMode.SourceAtop);
context.AddRect(rect);
context.DrawPath(CGPathDrawingMode.Fill);
image = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();
return image;
}
4. Last, but not least, we have to create a new UICollectionView (change control’s class type to your custom one) and initialise datasource and the prerequisites.
customCollectionView.RegisterNibForCell(CustomCollectionViewCell.Nib, CustomCollectionViewCell.Key);
customCollectionView.DataSource = new CustomDataSource();
customCollectionView.InitPrerequisites(UIColor.White, 20, 50, 100);
This was the short story of how we achieved the effects from the preview. If you’re interested of details check our samples or implementation from our repo.
I’ll come with an update when this will be available on the NuGet.
Have fun coding!