1
votes

I am trying to achieve a button with shadow in android. As you can see in this image I have 3 types of buttons.

The left one is the default button using this: <Button FontSize="14" Text="Button" /> The shadow is defined and properly animates when pressed.

The middle one is this: <Button FontSize="14" Text="Button" BackgroundColor="Red" TextColor="White" /> Changing the background color makes the button more taller and removes the shadow.

The right one I used a custom renderer, and I used a custom ViewOutlineProvider and assign it to the Control's View.OutlineProvider property. The markup is this: <ctrl:MaterialButton FontSize="14" Text="Discard Changes" BackgroundColor="#6200EE" HeightRequest="36" TextColor="White" />

This is the custom renderer class:

[assembly: ExportRenderer(typeof(MaterialButton), typeof(MaterialButtonRenderer))]
namespace XF.Material.Droid.Renderers
{

    public class MaterialButtonRenderer : Xamarin.Forms.Platform.Android.ButtonRenderer
    {
        private MaterialButton _materialButton;

        public MaterialButtonRenderer(Context context) : base(context) { }

        protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Button> e)
        {
            base.OnElementChanged(e);

            if (e?.NewElement != null)
            {
                _materialButton = this.Element as MaterialButton;
                this.Control.Text = this.Control.Text.ToUpper();
                this.Control.OutlineProvider = new MaterialButtonOutlineProvider(_materialButton.CornerRadius);
            }
        }

        private class MaterialButtonOutlineProvider : ViewOutlineProvider
        {
            private readonly int _cornerRadius;

            public MaterialButtonOutlineProvider(int cornerRadius)
            {
                _cornerRadius = cornerRadius;
            }

            public override void GetOutline(Android.Views.View view, Outline outline)
            {
                var cornerRadius = MaterialExtensions.ConvertDpToPx(_cornerRadius);
                outline.SetRoundRect(0, 0, view.Width, view.Height, cornerRadius);
            }
        }
    }
}

I also tried to set the View.Elevation and View.TranslationZ property in the renderer, but still no shadow. If you look closely the corners of the right button, you can see an outline but it appears to have been cut.

2

2 Answers

1
votes

After a day of figuring out how to solve this, I am finally able to show the shadow.

By default Android provides this default button drawable:

<inset xmlns:android="http://schemas.android.com/apk/res/android"
   android:insetLeft="@dimen/abc_button_inset_horizontal_material"
   android:insetTop="@dimen/abc_button_inset_vertical_material"
   android:insetRight="@dimen/abc_button_inset_horizontal_material"
   android:insetBottom="@dimen/abc_button_inset_vertical_material">
   <shape android:shape="rectangle">
        <corners android:radius="@dimen/abc_control_corner_material" />
        <solid android:color="@android:color/white" />
        <padding android:left="@dimen/abc_button_padding_horizontal_material"
             android:top="@dimen/abc_button_padding_vertical_material"
             android:right="@dimen/abc_button_padding_horizontal_material"
             android:bottom="@dimen/abc_button_padding_vertical_material" />
    </shape>
</inset>

In Xamarin Forms when setting a custom background (i.e. <Button BackgroundColor="Red" Text="Ok"/>) it creates a drawable with no padding and insets, thus hiding any outline or shadows of the button. That is why a button with a background color defined looks bigger when compared to a default button with no background color. You can set the HeightRequest of the button to make it smaller but the shadow will still not appear.

As a work around for devices >= API 21, I created two drawable XML files under the Resources/drawable-v21 folder namely drawable_ripple_dark and drawable_ripple_light. These will be used for buttons with dark or light background colors, respectively:

<?xml version="1.0" encoding="utf-8" ?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
        android:color="@color/ripple_material_dark"> <!-- or android:color="@color/ripple_material_light" -->
  <item android:id="@+id/inset_drawable">
    <inset android:insetLeft="@dimen/abc_button_inset_horizontal_material"
           android:insetTop="@dimen/abc_button_inset_vertical_material"
           android:insetRight="@dimen/abc_button_inset_horizontal_material"
           android:insetBottom="@dimen/abc_button_inset_vertical_material">
      <shape android:shape="rectangle">
        <corners android:radius="@dimen/abc_control_corner_material" />
        <solid android:color="@android:colorAccent" />
        <padding android:left="@dimen/abc_button_padding_horizontal_material"
                 android:top="@dimen/abc_button_padding_vertical_material"
                 android:right="@dimen/abc_button_padding_horizontal_material"
                 android:bottom="@dimen/abc_button_padding_vertical_material" />
      </shape>
    </inset>
  </item>
</ripple>

This will be used as a "template" drawable in my custom renderer class:

[assembly: ExportRenderer(typeof(MaterialButton), typeof(MaterialButtonRenderer))]
namespace XF.Material.Droid.Renderers
{
    public class MaterialButtonRenderer : Xamarin.Forms.Platform.Android.AppCompat.ButtonRenderer
    {
        private MaterialButton _materialButton;

        public MaterialButtonRenderer(Context context) : base(context) { }

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

            if (e?.NewElement != null)
            {
                _materialButton = this.Element as MaterialButton;

                this.Control.SetAllCaps(true);

                if (Material.IsLollipop)
                {
                    this.Control.Background = this.CreateRippleDrawable();
                }
            }
        }

        private Drawable CreateRippleDrawable()
        {
            var normalColor = _materialButton.BackgroundColor.ToAndroid();
            var cornerRadius = _materialButton.CornerRadius.ConvertDpToPx();
            var borderWidth = (int)MaterialExtensions.ConvertDpToPx((int)_materialButton.BorderWidth);
            var borderColor = _materialButton.BorderColor.ToAndroid();
            var rippleDrawable = (normalColor.IsColorDark() ? ContextCompat.GetDrawable(this.Context, Resource.Drawable.drawable_ripple_dark) as RippleDrawable : ContextCompat.GetDrawable(this.Context, Resource.Drawable.drawable_ripple_light) as RippleDrawable).GetConstantState().NewDrawable().Mutate() as RippleDrawable; //Copies the drawable
            var insetDrawable = rippleDrawable.FindDrawableByLayerId(Resource.Id.inset_drawable) as InsetDrawable;

            var gradientDrawable = insetDrawable.Drawable as GradientDrawable;
            gradientDrawable.SetCornerRadius(cornerRadius);
            gradientDrawable.SetColor(normalColor);
            gradientDrawable.SetStroke(borderWidth, borderColor);

            return rippleDrawable;
        }
    }
}

Now I am able to show the shadow. It also elevates when touched.

0
votes

Do you have any examples of how to integrate your solution?

I put your code and it generates some details since the variable MaterialButton is not known in this part

[assembly: ExportRenderer (typeof (MaterialButton), typeof (MaterialButtonRenderer))]

neither is this variable

private MaterialButton _materialButton;

Did you import any nugget or what did you do other than this?

Greetings.