2

I want to add a gradient to both parts of the menu of an Xamarin app. Place where I want the gradient are marked with "here" - I'm just after ToolBar for now...

enter image description here

In order to attempt this, I have created the following class

public class GradientHeaderNavigationPage : NavigationPage
{
    public GradientHeaderNavigationPage() { }
    public GradientHeaderNavigationPage(Page root) : base(root) { }

    public static readonly BindableProperty LeftColorProperty =
        BindableProperty.Create(propertyName: nameof(LeftColor),
           returnType: typeof(Color),
           declaringType: typeof(GradientHeaderNavigationPage),
           defaultValue: Color.FromHex("#92CD8C")); //Color.Accent);

    public static readonly BindableProperty RightColorProperty =
        BindableProperty.Create(propertyName: nameof(RightColor),
            returnType: typeof(Color),
            declaringType: typeof(GradientHeaderNavigationPage),
            defaultValue: Color.FromHex("#17AEC6")); //Color.Accent);

    public Color LeftColor
    {
        get { return (Color)GetValue(LeftColorProperty); }
        set { SetValue(LeftColorProperty, value); }
    }

    public Color RightColor
    {
        get { return (Color)GetValue(RightColorProperty); }
        set { SetValue(RightColorProperty, value); }
    }
}

Then for Android (we can worry about iOS later) I have added the following custom renderer

[assembly: ExportRenderer(typeof(GradientHeaderNavigationPage), typeof(GradientHeaderNavigationRenderer))]
namespace Drax.Droid.Renderers
{
    public class GradientHeaderNavigationRenderer : NavigationRenderer
    {
        public GradientHeaderNavigationRenderer(Context context) : base(context) { }

        protected override void OnElementChanged(ElementChangedEventArgs<NavigationPage> e)
        {
            base.OnElementChanged(e);

            if (e.OldElement != null || Element == null)
                return;

            var control = (GradientHeaderNavigationPage)Element;
            var context = (MainActivity)Context;

            context.ActionBar.SetBackgroundDrawable(
                new GradientDrawable(GradientDrawable.Orientation.RightLeft, 
                new int[] { control.RightColor.ToAndroid(), control.LeftColor.ToAndroid() }));
        }
    }
}

and added to the /Resources/drawable folder the file gradient_header.xml

<?xml version="1.0" encoding="utf-8" ?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" 
       android:shape="rectangle" >
    <gradient
        android:angle="180"
        android:startColor="#92CD8C"
        android:endColor="#17AEC6"
        android:type="linear" />
</shape>

and in /Resources/layout/ToolBar.axml I have added android:background="@drawable/header_gradient"

<android.support.v7.widget.Toolbar
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/header_gradient"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    android:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

Now, in my App.cs file (I am using Prism for MVVM) I have

[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace Drax
{
    public partial class App : PrismApplication
    {
        public App(IPlatformInitializer initializer) : base(initializer) { }

        protected override async void OnInitialized()
        {
            try
            {
                InitializeComponent();
                var result = await NavigationService.NavigateAsync(
                    new Uri("/DraxMasterDetailPage/NavigationPage/MapPage"));

                if (!result.Success)
                    SetMainPageFromException(result.Exception);
            }
            catch (Exception e)
            {
                Console.WriteLine($"Android DEBUG:: {e.Message}");
            }
        }

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {
            RegisterServices(containerRegistry);
            RegisterViews(containerRegistry);
        }

        private void RegisterServices(IContainerRegistry containerRegistry)
        {
            containerRegistry.Register<ILoggerFacade, Services.DebugLogger>();
        }

        private void RegisterViews(IContainerRegistry containerRegistry)
        {
            containerRegistry.RegisterForNavigation<NavigationPage>();
            containerRegistry.RegisterForNavigation<DraxMasterDetailPage>();
            containerRegistry.RegisterForNavigation<MapPage>();
        }

        private void SetMainPageFromException(Exception ex)
        {
            var layout = new StackLayout
            {
                Padding = new Thickness(40)
            };
            layout.Children.Add(new Label
            {
                Text = ex?.GetType()?.Name ?? "Unknown Error encountered",
                FontAttributes = FontAttributes.Bold,
                HorizontalOptions = LayoutOptions.Center
            });

            layout.Children.Add(new ScrollView
            {
                Content = new Label
                {
                    Text = $"{ex}",
                    LineBreakMode = LineBreakMode.WordWrap
                }
            });

            MainPage = new ContentPage
            {
                Content = layout
            };
        }
    }
}

with my DraxMasterDetailPage as

<MasterDetailPage x:Class="Drax.Views.DraxMasterDetailPage" 
                  xmlns="http://xamarin.com/schemas/2014/forms"
                  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                  xmlns:Controls="clr-namespace:Drax.Controls">
    <MasterDetailPage.Master>
        <NavigationPage Title="Drax">
            <x:Arguments>
                <ContentPage Title="Menu">
                    <StackLayout Padding="40">

                    </StackLayout>
                </ContentPage>
            </x:Arguments>
        </NavigationPage>
    </MasterDetailPage.Master>
</MasterDetailPage>

I have tried to change NavigationPage to GradientHeaderNavigationPage but this throws a runtime exception of

System.NullReferenceException: Object reference not set to an instance of an object.

on this line in the Adroid renderer

context.ActionBar.SetBackgroundDrawable(
    new GradientDrawable(GradientDrawable.Orientation.RightLeft, 
    new int[] { control.RightColor.ToAndroid(), control.LeftColor.ToAndroid() }));

ActionBar is null.

How can I amend this code to show the gradient ToolBar?

MoonKnight
  • 23,214
  • 40
  • 145
  • 277

2 Answers2

1

You have changed the toolbar's background through the gradient_header.xml and ToolBar.axml. It means the whole app's toolbar has been changed. You can comment out the Naviagtion Renderer to see the effect.

If you want to adjust it using the code, you should manipulate the ContentPage's custom renderer instead of the NavigationPage's.

Firstly, define some methods to find the toolbar using code in MainActivity:

private static MainActivity instance;

public static View RootFindViewById<T>(int id) where T : View
{
    return instance.FindViewById<T>(id);
}

public MainActivity()
{
    instance = this;
}

Then you could change the toolbar's background dynamically:

[assembly: ExportRenderer(typeof(ContentPage), typeof(CustomPageRenderer))]
namespace PrismDemo.Droid
{
    public class CustomPageRenderer : PageRenderer
    {
        public CustomPageRenderer(Context context) : base(context)
        {
        }

        protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
        {
            base.OnElementChanged(e);

            var toolbar = MainActivity.RootFindViewById<Toolbar>(Resource.Id.toolbar);
            if (toolbar == null) return;

            toolbar.SetBackground(new GradientDrawable(GradientDrawable.Orientation.RightLeft,
                new int[] { Color.Red.ToAndroid(), Color.Blue.ToAndroid() }));
        }
    }
}

Here, I used hard code for setting the color and apply the custom renderer for all of the content pages. You could adjust it to your specified page and define the color bindable property there.

Alternatively, I think you can utilize the MessagingCenter to trigger the toolbar adjusting code.

Ax1le
  • 6,563
  • 2
  • 14
  • 61
  • Thanks very much for your time. I will be trying this out later and get back to you. Thanks again... – MoonKnight Jun 04 '19 at 09:46
  • `var toolbar = MainActivity.RootFindViewById(Resource.Id.toolbar);` returns null? – MoonKnight Jun 04 '19 at 11:56
  • @MoonKnight Where did you call this method? You have to create a custom renderer for page instead of navigation page. And for the page which isn't wrapped by a navigation page, I forget to add a return. See my updates. – Ax1le Jun 04 '19 at 12:01
  • Ah, I see, a Page renderer. I am new to this, I apologise. How can I apply this renderer to all pages via the Xamarin Shared project? Would you be so kind as to look at my GitHub or provide a more complete explanation? – MoonKnight Jun 04 '19 at 12:04
  • @MoonKnight You can specify your requirement. I will look into it tomorrow. – Ax1le Jun 04 '19 at 12:08
  • 1
    @MoonKnight Here is my sample: https://github.com/landl0526/PrismDemo.git – Ax1le Jun 05 '19 at 09:46
  • This is above and beyond, thank you very much for your time and effort here - I have awarded a bounty in appreciation. – MoonKnight Jun 05 '19 at 12:54
  • @MoonKnight It's my pleasure. – Ax1le Jun 05 '19 at 13:04
  • Lu I have asked another question concerning the system tool bar. I apologies for harassing you, but I am new to this and am a bit confused. https://stackoverflow.com/questions/56462992/how-to-gradient-fill-the-system-status-bar-using-xamarin if you could help that would be great. I will of course provide rep in due course. Would you consider looking at my project, I have added you to the repo, I totally understand if you cannot. Thanks again... – MoonKnight Jun 05 '19 at 15:00
  • @MoonKnight I'm sorry I can't participate in your github account. But I will look into your new issues and post an answer if I have some thoughts about it. – Ax1le Jun 06 '19 at 08:45
  • Understood - thank you very much for your time and help so far. – MoonKnight Jun 06 '19 at 08:55
1

For me this worked for toolbar:

        var toolbar = FindViewById<Android.Support.V7.Widget.Toolbar>(Resource.Id.toolbar);

        if (toolbar == null)
            return;

However, it is inside OnLayout. I can't remember for sure at the moment, but it is likely that you can find toolbar properly only there.

Ivan Ičin
  • 9,672
  • 5
  • 36
  • 57