MoonKnight
MoonKnight

Reputation: 23831

How to Create a Gradient ToolBar in Android Xamarin App

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?

Upvotes: 2

Views: 513

Answers (2)

Ax1le
Ax1le

Reputation: 6643

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.

Upvotes: 1

Ivan I
Ivan I

Reputation: 9990

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.

Upvotes: 1

Related Questions