Hey,
today I accomplished the task to create Gradient Background without the use of an Image.
The task was to make it possible for the user to change the Background of the Views on Runtime! First I thought I could achive this with Images but on the long run it would have blown up the App-Size.
This is the best implementation I could come up with and the choosen Colors are persistent with the usage of the Settings Plugin form @JamesMontemagno that you can get here: Nuget
This code works for Android and iOS and you can drop it in if you want
So long Story short here's the code:
CustomRenderer in PCL
:
public class GradientContentPage : ContentPage { public static BindableProperty StartColorProperty = BindableProperty.Create<GradientContentPage, Color>(p => p.StartColor, Color.White); public static BindableProperty EndColorProperty = BindableProperty.Create<GradientContentPage, Color>(p => p.EndColor, Color.Gray); public Color StartColor { get { return (Color) GetValue(StartColorProperty); } set { SetValue(StartColorProperty, value); } } public Color EndColor { get { return (Color) GetValue(EndColorProperty); } set { SetValue(EndColorProperty, value); } } }
To make the Colors available all over my app I create two static variables in App.cs and since I can't put a Color
into the Settings I have to store them as string
So here's the code for my App.cs
public static Color StartColor; public static Color EndColor; public App() { StartColor = StringToColor(Helpers.Settings.StartColor.Split(' , '); EndColor = StringToColor(Helpers.Settings.EndColor.Split(' , '); MainPage = new NavigationPage(new DashboardView{ StartColor = App.StartColor, EndColor = App.EndColor }); }
As you may notice I've setting the Colors for the View on creation. Sadly you have to this, because Views that inherit from ContentPage
can't make us of Implicit Styles
right now because of this Bug 27659
So anyway let's take a look at StringToColor
private static void StringToColor(IList<string> color) { for(var i = 0; i < color.Count(); i++) { //Regex to get the color code color[i] = Regex.Replace(color[i], @"^\d.\d+]", ""); } var a = double.Parse(color[0], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture); var r = double.Parse(color[1], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture); var g = double.Parse(color[2], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture); var b = double.Parse(color[3], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture); return Color.FromRgba(r, g, b, a); }
Ok, I admit I'm not happy with this function and if anyone has a better idea how to achive this, please let me know! Basically this function find the Color-Code.
Alright now let's get to the platform renderers:
CustomRenderer in Android
:
[assembly: ExportRenderer(typeof(GradientContentPage), typeof(GradientContentPageRenderer))] namespace YourApp.Droid { public class GradientContentPageRenderer : PageRenderer { protected override void OnVisibilityChanged(Android.Views.View changedView, ViewStates visibility) { base.OnVisibilityChanged(changedView, visibility); SetBackground(); } protected override void OnElementChanged(ElementChangedEventArgs<Page> page) { base.OnElementChanged(page); SetBackground(); } private void SetBackground() { var startColor = App.StartColor.ToAndroid(); var endColor = App.EndColor.ToAndroid(); var colors = new int[] { startColor, endColor }; Background = new GradientDrawable(GradientDrawable.Orientation.TopBottom, colors); } } }
You have to overwrite both Events to get every Background in the color you wan't. It's a bit odd but this is how Forms on Android reacts and there is no (at least I haven't found one) event that gets fired everytime you switch a Page;
CustomRenderer in iOS
:
[assembly: ExportRenderer(typeof(GradientContentPage), typeof(GradientContentPageRenderer))] namespace YourApp.iOS { public class GradientContentPageRenderer: PageRenderer { public override void ViewWillAppear(bool animated) { base.ViewWillAppear(animated); var gradientLayer = new CAGradientLayer { Frame = View.Bounds, Colors = new [] { App.StartColor.ToCGColor(), App.EndColor.ToCGColor() }, StartPoint = new CGPoint(0, 0), EndPoint = new CGPoint(1, 1) } //This is needed to get every background redrawn if the color changes on runtime if(View.Layer.Sublayers[0].GetType() == typeof(CAGradientLayer)) { View.Layer.ReplaceSublayer(View.Layer.Sublayers[0], gradientLayer); } else { View.Layer.InsertSublayer(gradientLayer, 0); } } } }
For this one I have to thank @JohnBeans for his thread here on the forum: Click me
So for the Color Picking I created a view where the user can select a color and then it gets automatically drawn on runtime with the new color. But posting this would be to much for one thread. If someone is interested I can post it.
Posts
I have to update my post.
Since 1.4.4 -pre2 the rendering has changed on iOS. When
ViewWillAppear
is called after the first page in your App the height of the NavigationBar is not inView.Bounds
anymore. This will lead to a white bar on the end of your background. So I have adjusted my App to counter this.#On App.cs I had to Introduce 3 new variables.
then I changed the GradientContentPageRenderer like this
Thanks for this gradient code. Very informative.
Could this be applied to different elements (layouts, views) too?
Can the angle/direction of the gradient be configured?
If it is applied to a ContentPage, is the navigation bar included? Or can it be in a different way?
A few comments about your code:
If you only need App.Width, App.Height and App.OnAppStart in iOS, then you should keep those values in your iOS project and not outside. Maybe as static variables within your GradientContentPageRenderer.
However I don't see the reason why you read View.Bounds just once. - Apart from converting them to a string and back which you should also avoid. If you need those values somewhere else in your PCL project, then cast the nfloats to float, but do not convert them to strings.
Are you maybe looking for
UIScreen.MainScreen.Bounds
?Why did you add the StartColor and EndColor properties to your GradientContentPage?
I only see that you are using App.StartColor and App.EndColor, but you never use the properties of the GradientContentPage itself. You set them in
new DashboardView{ StartColor = App.StartColor, EndColor = App.EndColor }
but you never use those values.Unless you have a reason to store the colors as floats, you should consider storing them as hex strings instead "#FFFFCC00". Then you can simply use
Color.FromHex(string)
and drop yourStringToColor
with which you are not happy anyway.I try to avoid using
Regex
wherever possible. It can be very expensive.In the company I worked for, we used to validate the email address which the user entered with a very complicated Regex. After it worked for years without change, I stumbled upon a string which broke the validation completely. When the user entered that string, the JavaScript regex parser hang. Then I tried it in a simple C# program and that hang too. We also had some performance and memory issues on the server side which originated in bad usages of Regex.
I guess I sound a bit like a teacher now. Sorry about that. It was not my intention. I just wanted to mention what IMHO could be done better.
Hi,
Yes they should be applicable to
Layout
orViews
but then you have to hook up in different Events. MostlyOnElementChanged
, I guess. Haven't tried that now.On iOS you can handle the rotation for the gradient with
StartPoint
andEndPoint
when you create theCAGradientLayer
. On Android you have theGradientDrawable.Orientation
in which are a few options.No, the
NavigationBar
is not included. It just the background for theContentPage
. However, I guess you can change the NavigationBar Background inside of theCustomRenderers
. Just a thought, haven't tried it.Oh my, didn't knew about
UIScreen.MainScreen.Bounds
. That is working perfectly. Thanks I'll update the code here. Sadly I can't just edit it. Thank you for that.Ha. Got me there
I don't know why I added the properties to the Pages. Thanks for pointing that out!
I actually have a reason. As on the end of my post I stated that I have a
ColorSelectionView
where the User can select the Colors for the background. I'm using own Colors and theBuild-In Colors
. And there is no information what Hex-Code they have. Sadly. But after I'm releasing the App, I'm gonna take me some time to find out which Hex-Codes they have and do it with them. That seems alot easier.I'm aware that
Regex
can be really expensive and this is the only time where I use aRegex
in the App. Thankfully the one I have here is fast enough and the Start time for the app is only 1-2 Seconds even with that function.Actually you really helped me here, thank you. There's something to learn every day
This is the correct code for the iOS renderer.
I find this to be a good way to do gradients.
I actually just made a cross platform gradient with it for a user story right now. It was 3 lines of code.
https://github.com/paulpatarinski/Xamarin.Forms.Plugins/tree/master/SVG
[Edit] an example
This is a fully cross platform view - note how few lines of code are required for the gradient, which is just a content view that can be composed into any other layout:
For gradients I just use an SVG image. A simple rectangle drawn in Inkscape, where I can edit and preview all gradients colors, steps, orientation, etc. This is the svg file content:
Then I use the SVG plugin to display it and resize it to fit any iOS or Android app frame background:
https://github.com/paulpatarinski/Xamarin.Forms.Plugins/tree/master/SVG
In this way artists in my team can change all graphic elements and color themes without my intervention, just editing the SVG files in Inkscape.
I just saw that you should not use
UIScreen.MainScreen.Bounds
anymore. In iOS 9 Apple copied from Microsoft for a change and introduced a split screen mode where two apps can run side by side. In that case the screen bounds are most likely not what you want anymore.I saw voices which said that you should use
View.Window.Bounds
instead, butView.Window
was null in my case.UIApplication.SharedApplication.KeyWindow.Bounds
works for me. From what I learned, this should be the recommended approach, but I don't know iOS well enough to be 100% sure.@MichaelRumpler Thanks for that info. I'm gonna try it out and post my findings.
Using @GeorgeCook 's example with NGraphics 0.7.1, all the NGraphics.Model.AClass should be NGraphics.AClass
Not sure from which version this has changed though.
It's an old thread but please check out this library, I've rewritten it from swift gradient library.
PastelXamarinIos <- Search it on github
I have problem in IOS.
I am using CAGradientLayer,i wrote custom renderer for this, in XAml i have custom width this will change on runtime. when i am changing the width it's not updating.
You can achieve this with Skiasharp
Then just stretch the canvas over your grid etc