[Guide] Create a Gradient Background without Images

RaphaelSchindlerRaphaelSchindler USMember ✭✭✭

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

  • RaphaelSchindlerRaphaelSchindler USMember ✭✭✭

    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 in View.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.

    public static string Width;
    public static string Height;
    public static bool OnAppStart;
    
    public App()
    {
        if(Device.OS == TargetPlatform.iOS)
        {
            OnAppStart = true;
        }
        CreateStyles();
        //The first page gets rendered correctly with NavigationBar Height so we can grad the View.Bounds from it
        MainPage = new NavigationPage(new DashboardView { StartColor = StartColor, EndColor = EndColor });
    }
    

    then I changed the GradientContentPageRenderer like this

    [assembly: ExportRenderer(typeof(GradientContentPage), typeof(GradientContentPageRenderer))]
    
    namespace YourApp.iOS
    {
        public class GradientContentPageRenderer: PageRenderer
        {
            public override void ViewWillAppear(bool animated)
            {
                base.ViewWillAppear(animated);
    
                if(App.OnAppStart)
                {
                    App.Width = View.Bounds.Width.ToString(CultureInfo.InvariantCulture);
                    App.Height = View.Bounds.Height.ToString(CultureInfo.InvariantCulture);
                    App.OnAppStart = false;
                }
    
                var gradientLayer = new CAGradientLayer
                {
                    //This line had to be changed to be able to draw the background correctly
                    Frame = new CGRect{Width = nfloat.Parse(App.Width), Height = nfloat.Parse(Height)},
                    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);
                }
            }
        }
    }
    
  • MichaelRumplerMichaelRumpler ATMember ✭✭✭✭✭

    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 your StringToColor 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.

  • RaphaelSchindlerRaphaelSchindler USMember ✭✭✭
    edited July 2015

    Hi,

    Yes they should be applicable to Layout or Views but then you have to hook up in different Events. Mostly OnElementChanged, I guess. Haven't tried that now.

    On iOS you can handle the rotation for the gradient with StartPoint and EndPoint when you create the CAGradientLayer. On Android you have the GradientDrawable.Orientation in which are a few options.

    No, the NavigationBar is not included. It just the background for the ContentPage. However, I guess you can change the NavigationBar Background inside of the CustomRenderers. 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 :smile: 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 the Build-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 a Regex 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 :smile:

    This is the correct code for the iOS renderer.

    var gradientLayer = new CAGradientLayer
    {
        Frame = UIScreen.MainScreen.Bounds,
        Colors = new [] { App.StartColor.ToCGColor(), App.EndColor.ToCGColor()},
        StartPoint = new CGPoint(0, 0),
        EndPoint = new CGPoint(1, 1)
     };
    
  • GeorgeCookGeorgeCook PEUniversity ✭✭✭
    edited July 2015

    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:

    public EditVideoChatOnlyOneWarningView ()
        {
            _backgroundView = new NControlView {
                DrawingFunction = (canvas, rect) => {
                    var fillBrush = new LinearGradientBrush (NGraphics.Models.Point.Zero, NGraphics.Models.Point.OneY, new NGraphics.Models.Color ("#985ADA"),
                                        new NGraphics.Models.Color ("#ffffff"));
                    canvas.FillRectangle (new NGraphics.Models.Rect (0, 0, rect.Width, rect.Height), fillBrush);
                },
            };
    
    //other controls.. etc
    

  • EmanueleSabettaEmanueleSabetta ITBeta ✭✭✭
    edited July 2015

    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:

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="600" width="600" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" viewBox="0 0 600 600">
     <defs>
      <linearGradient id="linearGradient4297" y2="600" gradientUnits="userSpaceOnUse" x2="300" x1="300">
       <stop stop-color="#e66aff" offset="0"/>
       <stop stop-color="#fef8ff" offset="1"/>
      </linearGradient>
     </defs>
     <rect style="color:#000000" height="600" width="600" y="0" x="0" fill="url(#linearGradient4297)"/>
    </svg>
    

    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.

  • MichaelRumplerMichaelRumpler ATMember ✭✭✭✭✭

    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, but View.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.

  • RaphaelSchindlerRaphaelSchindler USMember ✭✭✭

    @MichaelRumpler Thanks for that info. I'm gonna try it out and post my findings.

  • ThibaultDThibaultD SEMember ✭✭✭

    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.

  • TornikeGomareliTornikeGomareli USMember
    edited January 2018

    It's an old thread but please check out this library, I've rewritten it from swift gradient library.

    PastelXamarinIos <- Search it on github

  • DeepakDeepsDeepakDeeps INMember ✭✭✭

    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.

  • NMackayNMackay GBInsider, University mod

    You can achieve this with Skiasharp

    using SkiaSharp;
    using SkiaSharp.Views.Forms;
    using Xamarin.Forms;
    
    namespace HamburgerMenu.CustomControls
    {
        public partial class GradientView : ContentView
        {
            public string StartColor { get; set; } = "#ffffff";
            public string EndColor { get; set; } = "#000000"; 
            public bool Horizontal { get; set; } = false;
    
            public GradientView()
            {
                SKCanvasView canvasView = new SKCanvasView();
                canvasView.PaintSurface += OnCanvasViewPaintSurface;
                Content = canvasView;
            }
    
            void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
            {
                SKImageInfo info = args.Info;
                SKSurface surface = args.Surface;
                SKCanvas canvas = surface.Canvas;
    
                canvas.Clear();
    
                SKColor.TryParse(StartColor, out var stColor);
                SKColor.TryParse(EndColor, out var edColor);
    
                var colors = new SKColor[] { stColor, edColor };
                SKPoint startPoint = new SKPoint(0, 0);
                SKPoint endPoint = Horizontal ? new SKPoint(info.Width, 0) : new SKPoint(0, info.Height);
    
                var shader = SKShader.CreateLinearGradient(startPoint, endPoint, colors, null, SKShaderTileMode.Clamp);
    
                SKPaint paint = new SKPaint
                {
                    Style = SKPaintStyle.Fill,
                    Shader = shader
                };
    
                canvas.DrawRect(new SKRect(0, 0, info.Width, info.Height), paint);
            }
        }
    }
    

    Then just stretch the canvas over your grid etc

            <Grid BackgroundColor="Transparent" RowSpacing="0">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="*" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                        </Grid.RowDefinitions>
                        <customControls:GradientView StartColor="#FFAFF9" EndColor="#FF23E5" Horizontal="True" Grid.Row="0"
                                                     Grid.RowSpan="3" />
                        <Label Grid.Row="1" Text="My Company" HorizontalOptions="End" Margin="0,0,10,0"
                               FontAttributes="Bold" FontSize="Large" TextColor="White" VerticalTextAlignment="End" />
                        <Label Grid.Row="2" Text="User" HorizontalOptions="End" Margin="0,0,10,0"
                               FontSize="Medium" TextColor="White" VerticalTextAlignment="Start" />
                    </Grid>
    
    
Sign In or Register to comment.