Alright now when it comes to the default Xamarin Forms ScrollView, its pretty much generic and limited with the simple common attributes and behaviours, and it does not deliver any specific “cool” features with it.
What’s so cool about it?
So I thought of creating my own Custom ScrollView for Xamarin Forms using Custom Renderers, which would include the following awesome features,
Bouncy Effect – Yeah you gotta admit, the bounce effect of a native scrollview is pretty fun to play with, in a User’s perspective. So I thought of enabling this feature in my custom ScrollView even when the Child Element doesn’t exceeds the ScrollView boundaries… 😉
(PS: this effect is interpreted in native Android and iOS differently)
Disabling and Enabling Horizontal and Vertical Scroll Indicators – Now sometimes these scroll bar indicators are useful but there are times which we want to hide them, as it might look ugly on the UI in certain cases. So yeah let’s have some control over it shall we? 😀
Background Image – Of course who wouldn’t like a background Image on a Scroll view eh! 😉 Well to be specific we are going to add a Fixed Background Image for the ScrollView. And note that this background Image would be fixed, and will not be scrolling with the Content of the ScrollView. (I will do another post to enable that feature).
Yes behold, me, myself and I presenting the “BloopyScrollView” why the name “BloopyScrollView”? I don’t even know. lol 😛
Implementation
Alright let’s go ahead and create our Custom ScrollView Control in the PCL project. Along with the following properties, so that we could have direct control over the above said behaviours.
Alright now expect this to be longer, since I have added the properties as Bindable Properties, so you could use them in any MVVM scenario with ease. 😀
namespace WhateverYourNamespace
{
public class BloopyScrollView : ScrollView
{
public static readonly BindableProperty IsHorizontalScrollbarEnabledProperty =
BindableProperty.Create(
nameof(IsHorizontalScrollbarEnabled),
typeof(bool),
typeof(BloopyScrollView),
false,
BindingMode.Default,
null);
/// <summary>
/// Gets or sets the Horizontal scrollbar visibility
/// </summary>
public bool IsHorizontalScrollbarEnabled
{
get { return (bool)GetValue(IsHorizontalScrollbarEnabledProperty); }
set { SetValue(IsHorizontalScrollbarEnabledProperty, value); }
}
public static readonly BindableProperty IsVerticalScrollbarEnabledProperty =
BindableProperty.Create(
nameof(IsVerticalScrollbarEnabled),
typeof(bool),
typeof(BloopyScrollView),
false,
BindingMode.Default,
null);
/// <summary>
/// Gets or sets the Vertical scrollbar visibility
/// </summary>
public bool IsVerticalScrollbarEnabled
{
get { return (bool)GetValue(IsVerticalScrollbarEnabledProperty); }
set { SetValue(IsVerticalScrollbarEnabledProperty, value); }
}
public static readonly BindableProperty IsNativeBouncyEffectEnabledProperty =
BindableProperty.Create(
nameof(IsNativeBouncyEffectEnabled),
typeof(bool),
typeof(BloopyScrollView),
true,
BindingMode.Default,
null);
/// <summary>
/// Gets or sets the Native Bouncy effect status
/// </summary>
public bool IsNativeBouncyEffectEnabled
{
get { return (bool)GetValue(IsNativeBouncyEffectEnabledProperty); }
set { SetValue(IsNativeBouncyEffectEnabledProperty, value); }
}
public static readonly BindableProperty BackgroundImageProperty =
BindableProperty.Create(
nameof(BackgroundImage),
typeof(ImageSource),
typeof(BloopyScrollView),
null,
BindingMode.Default,
null);
/// <summary>
/// Gets or sets the Background Image of the ScrollView
/// </summary>
public ImageSource BackgroundImage
{
get { return (ImageSource)GetValue(BackgroundImageProperty); }
set { SetValue(BackgroundImageProperty, value); }
}
}
}
There we go IsHorizontalScrollbarEnabled, IsVerticalScrollbarEnabled to disable/enable Horizonal and Vertical Scrollbars.
IsNativeBouncyEffectEnabled to control the Native Bouncy effect.
BackgroundImage to set the ImageSource for the ScrollView’s background Image. And make sure to provide a proper image for this hence we will be resizing the given image in our native renderer level to fit to the background of the ScrollView.(You will see in the next steps below)
Alright let’s head over to creating the Custom Renderers associated with our BloopyScrollView.
Something to keep in mind…
So if you’re a frequent reader of my blog, you may remember sometime ago I created an Extention class for handling Xamarin Forms Images in Native code level: https://theconfuzedsourcecode.wordpress.com/2016/12/12/an-awesome-image-helper-to-convert-xamarin-forms-imagesource-to-ios-uiimage-or-android-bitmap/
Why I’m bringing this up, is because we are going to be needing it for this project. You ask why? Because we need to convert the above BackgroundImage, which is of type Xamarin Forms ImageSource.
So we need to convert that ImageSource to native UIImage or Bitmap image respectively in our Custom renderer levels. 😉
So go ahead and grab that code real quick and add it to your Native Projects. 😀
iOS Implementation
Now let’s create the Custom Renderer for the Control in Xamarin.iOS project.
[assembly: ExportRenderer(typeof(BloopyScrollView), typeof(BloopyScrollViewRenderer))]
namespace WhateverYourNamespace.iOS
{
public class BloopyScrollViewRenderer : ScrollViewRenderer
{
private UIImage _uiImageImageBackground;
protected override async void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
this.ShowsVerticalScrollIndicator = ((BloopyScrollView)e.NewElement).IsVerticalScrollbarEnabled;
this.ShowsHorizontalScrollIndicator = ((BloopyScrollView)e.NewElement).IsHorizontalScrollbarEnabled;
if (e.NewElement != null)
{
if (((BloopyScrollView)e.NewElement).IsNativeBouncyEffectEnabled)
{
this.Bounces = true;
this.AlwaysBounceVertical = true;
}
if (((BloopyScrollView)e.NewElement).BackgroundImage != null)
{
// retrieving the UIImage Image from the ImageSource by converting
_uiImageImageBackground = await IosImageHelper.GetUIImageFromImageSourceAsync(((BloopyScrollView)e.NewElement).BackgroundImage);
}
((BloopyScrollView)e.NewElement).PropertyChanged += OnPropertyChanged;
}
}
private void OnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
if (propertyChangedEventArgs.PropertyName == BloopyScrollView.HeightProperty.PropertyName)
{
// check if the Width and Height are assigned
if (((BloopyScrollView)sender).Width > 0 & ((BloopyScrollView)sender).Height > 0)
{
// resize the UIImage to fit the current UIScrollView's width and height
_uiImageImageBackground = ResizeUIImage(_uiImageImageBackground, (float)((BloopyScrollView)sender).Width, (float)((BloopyScrollView)sender).Height);
// Set the background Image
this.BackgroundColor = UIColor.FromPatternImage(_uiImageImageBackground);
}
}
}
// We need to override this to have the background image to be fixed
public override void Draw(CGRect rect)
{
base.Draw(rect);
}
// Resize the UIImage
public UIImage ResizeUIImage(UIImage sourceImage, float widthToScale, float heightToScale)
{
var sourceSize = sourceImage.Size;
var maxResizeFactor = Math.Max(widthToScale / sourceSize.Width, heightToScale / sourceSize.Height);
if (maxResizeFactor > 1) return sourceImage;
var width = maxResizeFactor * sourceSize.Width;
var height = maxResizeFactor * sourceSize.Height;
UIGraphics.BeginImageContext(new CGSize(width, height));
sourceImage.Draw(new CGRect(0, 0, width, height));
var resultImage = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();
return resultImage;
}
}
}
Inside the method we are assigning the relevant properties of our BloopyScrollView to the native control properties.
The UIScrollView which is associated with the Xamarin Forms ScrollView has the following native properties:
- ShowsVerticalScrollIndicator: Make the vertical scrollbar visible or hidden
- ShowsHorizontalScrollIndicator: Make the horizontal scrollbar visible or hidden
- Bounces: Always enable the native bounce effect on iOS UIScrollView
- BackgroundColor: Allows to set the background color for the UIScrollView or set an Image as a pattern
Also you may have noted that we are converting our Image Source BackgroundImage to a UIImage using our extension.
And then when the Height and Width are set to the Control, we are resizing the Image to fit those properties and setting that as the Background of the UIScrollView through the UIColor.FromPatternImage() which allows us to set the image as a pattern throughout the canvas of the UIScrollView.
the strange tale of getting the UIScrollView’s Fixed background in Xamarin… :O
Notice that we are overriding the Draw(CGRect rect) method, this is to have the UIScroll Background Image to be fixed within the boundaries, and not to be spanned across the Content area.
Because usually if we set the BackgroundColor property, it will span across the Content area, but strangely if we override the Draw() method, BackgroundColor would only be contained within UIScrollView’s boundaries, without spanning across the Content area. This is something I figured out while playing around with the above implementation. 😀
Alright let’s jump into Android… 😀
Android Implementation
Now let’s create the Custom Renderer for the Control in Xamarin.Android project.
[assembly: ExportRenderer(typeof(BloopyScrollView), typeof(BloopyScrollViewRenderer))]
namespace WhateverYourNamespace.Droid
{
public class BloopyScrollViewRenderer : ScrollViewRenderer
{
private Bitmap _bitmapImageBackground;
protected override async void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
this.VerticalScrollBarEnabled = ((BloopyScrollView)e.NewElement).IsVerticalScrollbarEnabled;
this.HorizontalScrollBarEnabled = ((BloopyScrollView)e.NewElement).IsHorizontalScrollbarEnabled;
if (((BloopyScrollView)e.NewElement).IsNativeBouncyEffectEnabled)
{
this.OverScrollMode = OverScrollMode.Always;
}
if (((BloopyScrollView) e.NewElement).BackgroundImage != null)
{
// retrieving the Bitmap Image from the ImageSource by converting
_bitmapImageBackground = await AndroidImageHelper.GetBitmapFromImageSourceAsync(((BloopyScrollView)e.NewElement).BackgroundImage, this.Context);
// resize the Bitmap to fit the current ScrollView's width and height
var _resizedBitmapImageBackground = new BitmapDrawable(ResizeBitmap(_bitmapImageBackground, this.Width, this.Height));
// Set the background Image
this.Background = _resizedBitmapImageBackground;
}
}
// Resize the Bitmap
private Bitmap ResizeBitmap(Bitmap originalImage, int widthToScae, int heightToScale)
{
Bitmap resizedBitmap = Bitmap.CreateBitmap(widthToScae, heightToScale, Bitmap.Config.Argb8888);
float originalWidth = originalImage.Width;
float originalHeight = originalImage.Height;
Canvas canvas = new Canvas(resizedBitmap);
float scale = this.Width / originalWidth;
float xTranslation = 0.0f;
float yTranslation = (this.Height - originalHeight * scale) / 2.0f;
Matrix transformation = new Matrix();
transformation.PostTranslate(xTranslation, yTranslation);
transformation.PreScale(scale, scale);
Paint paint = new Paint();
paint.FilterBitmap = true;
canvas.DrawBitmap(originalImage, transformation, paint);
return resizedBitmap;
}
}
}
The Android ScrollView which is associated with the Xamarin Forms ScrollView has the following native properties:
- VerticalScrollBarEnabled: Make the vertical scrollbar visible or hidden
- HorizontalScrollBarEnabled: Make the horizontal scrollbar visible or hidden
- OverScrollMode: Always enable the native bounce effect on Android ScrollView
- Background: Allows to set the background drawable for the ScrollView
As you may have noticed we are converting our Xamarin Forms Image Source BackgroundImage to a Bitmap image using our extension.
Then we are resizing out Bitmap image according to the Width and Height of the ScrollView to fit to the full background to be wrapped around a BitmapDrawable and set to the Background of ScrollView.
There you go! 😀
Let’s use it… 😉
Alright now that’s done, let’s consume this in our PCL project.
<StackLayout Padding="10,0,10,0">
<Label Text="Welcome to Xamarin Forms!"
VerticalOptions="Center"
HorizontalOptions="Center" />
<local:BloopyScrollView
IsNativeBouncyEffectEnabled="True"
IsVerticalScrollbarEnabled="False"
IsHorizontalScrollbarEnabled="False">
<local:BloopyScrollView.BackgroundImage>
<FileImageSource File="xamarinBackgroundImage.png"/>
</local:BloopyScrollView.BackgroundImage>
<Label
FontSize="22"
HeightRequest="400"
Text=
"Whatever your text content to be displayed." />
</local:BloopyScrollView>
</StackLayout>
As you can see I have inserted my BloopyScrollView in a StackLayout and as the content of the ScrollView I have added Label. Well you can add any content you want or set any Height or Width as you wish.
Notice that I have set IsNativeBouncyEffectEnabled to be True as I want to see the native Bouncy effect. Then I have disabled the Vertical and Horizontal Scrollbars from the properties we added earlier. Then finally I have added the BackgroundImage and set the FileImageSource to the ImageSource type, where as I have placed the image in the native Resource folder, as you would do with any defualt Xamarin Forms Image. 😉
Now let’s see the results… 😀
Yaay! 😀
As we expected the Vertical and Horizontal Scrollbars are disabled and our ScrollView has full native bouncy effect accordingly.
Also you can see the Background Image nicely resized itself and fit to the background of the BloopyScrollView. 😀
Happy dance! lol
Recap Stuff…
Now there’s some stuff I wanted to recap, that is you may have noticed that when I was resizing the Image, I needed the Control’s Height and Width, and where I have acquired those properties are in two different places on each Android and iOS renderers.
To be specific I have accessed the Control’s Width and Height on Android right from the OnElementChanged method, but on iOS renderer I have accessed those values from the OnPropertyChanged method’s Height property event.
This is because of the differences of the Rendering cycling of Android and iOS, whereas on Android right at the firing of the Custom Renderer it assigns itself Width and Height. But on iOS we have to access them indirectly by waiting till those properties are set, by listening to the OnPropertyChanged event.
Get it on Github! 😀 XFImprovedScrollView
Cheers everyone!
Pass this on to another developer to make them smile! 😀
– Udara Alwis