0
votes

Does anyone have a complete example of a Xamarin Forms android button custom renderer? Or is that even possible?

I am basically looking to make round corners button.

I tried starting from this example: https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/custom-renderer/entry/

Using http://taoffi.isosoft.org/post/2016/03/26/Xamarin-forms-so-you-lost-your-rounded-buttons

But I get loads of errors at compilation:

using System;
using System.Collections.Generic;
using System.Linq;
//using UXDivers.Artina.Shared;
using Xamarin.Forms;
using Android.Graphics.Drawables;
using System.ComponentModel;
using Xamarin.Forms.Platform.Android;
using Android.Graphics;
using Android.Views;
using System.Runtime.Remoting.Contexts;
using CustomRenderer.Android;

[assembly: ExportRenderer(typeof(Button), typeof(CustomButtonCompatRenderer))]

namespace CustomRenderer.Android
{
    public class CustomButtonCompatRenderer : ButtonRenderer
    {
        public CustomButtonCompatRenderer(Context context) : base(context)
        {
            SetWillNotDraw(false);
        }


        private GradientDrawable _normal,
                                        _pressed;


        // resolves: button text alignment lost after click or IsEnabled change
        //public override void ChildDrawableStateChanged(Android.Views.View child)
        //{
        //  base.ChildDrawableStateChanged(child);
        //  Control.Text = Control.Text; 
        //}

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

            if (Control != null)
            {
                SetAlignment();

                var density = Math.Max(1, Resources.DisplayMetrics.Density);
                var button = e.NewElement;
                var mode = MeasureSpec.GetMode((int)button.BorderRadius);
                var borderRadius = button.BorderRadius * density;
                var borderWidth = button.BorderWidth * density;

                // Create a drawable for the button's normal state
                _normal = new Android.Graphics.Drawables.GradientDrawable();

                if (button.BackgroundColor.R == -1.0 && button.BackgroundColor.G == -1.0 && button.BackgroundColor.B == -1.0)
                    _normal.SetColor(Android.Graphics.Color.ParseColor("#ff2c2e2f"));
                else
                    _normal.SetColor(button.BackgroundColor.ToAndroid());

                _normal.SetStroke((int)borderWidth, button.BorderColor.ToAndroid());
                _normal.SetCornerRadius(borderRadius);

                // Create a drawable for the button's pressed state
                _pressed = new Android.Graphics.Drawables.GradientDrawable();
                var highlight = Context.ObtainStyledAttributes(new int[]
                                    {
                                        Android.Resource.Attribute.ColorAccent  //  .ColorActivatedHighlight
                                    }).GetColor(0, Android.Graphics.Color.Gray);

                _pressed.SetColor(highlight);
                _pressed.SetStroke((int)borderWidth, button.BorderColor.ToAndroid());
                _pressed.SetCornerRadius(borderRadius);

                // Add the drawables to a state list and assign the state list to the button
                var sld = new StateListDrawable();
                sld.AddState(new int[] { Android.Resource.Attribute.StatePressed }, _pressed);
                sld.AddState(new int[] { }, _normal);
                Control.SetBackground(sld);     //.SetBackgroundDrawable(sld); // deprecated
            }
        }

        private void SetAlignment()
        {
            var element = this.Element as Button;

            if (element == null || this.Control == null)
            {
                return;
            }

            this.Control.Gravity = GravityFlags.CenterHorizontal | GravityFlags.CenterVertical;
            //element.VerticalAlignment.ToDroidVerticalGravity() |  
            //element.HorizontalAlignment.ToDroidHorizontalGravity();  
        }

        void DrawCustom(Button targetButton)
        {
            if (Control == null || targetButton == null)
                return;

        }
    }
}

Errors:

CustomButtonCompatRenderer.cs(20,62,20,66): error CS1729: 'ButtonRenderer' does not contain a constructor that takes 1 arguments
CustomButtonCompatRenderer.cs(52,39,52,47): error CS0234: The type or namespace name 'Graphics' does not exist in the namespace 'CustomRenderer.Android' (are you missing an assembly reference?)
CustomButtonCompatRenderer.cs(55,38,55,54): error CS0234: The type or namespace name 'Graphics' does not exist in the namespace 'CustomRenderer.Android' (are you missing an assembly reference?)
CustomButtonCompatRenderer.cs(63,40,63,48): error CS0234: The type or namespace name 'Graphics' does not exist in the namespace 'CustomRenderer.Android' (are you missing an assembly reference?)
CustomButtonCompatRenderer.cs(66,68,66,79): error CS0117: 'Resource.Attribute' does not contain a definition for 'ColorAccent'
CustomButtonCompatRenderer.cs(67,52,67,68): error CS0234: The type or namespace name 'Graphics' does not exist in the namespace 'CustomRenderer.Android' (are you missing an assembly reference?)
CustomButtonCompatRenderer.cs(75,69,75,81): error CS0117: 'Resource.Attribute' does not contain a definition for 'StatePressed'

My environment:

  • Visual studio for mac: 7.3.3 (build 12)

  • Android SDK tools 26.1.1

  • Android SDK build-tools 26.0.3

  • Android SDK build-tools 25.0.3

  • Android Target version: Android 7.1 (API 25)

  • Minimum Android version: Android 4.0.3 (API 15)

PS: I am new to Xamarin and C#

1

1 Answers

2
votes

Here it is. A ripple-enabled button with CornerRadius and Padding.

You'll have to add the usings, the namespace and the ExportRenderer attribute.

MainActivity.Activity is a property of type Context that I set when the app initializes. Just add this line: Activity = this; in the OnCreate method, before Forms.Init.

public class ButtonRenderer : Xamarin.Forms.Platform.Android.ButtonRenderer
{
    // Your Button class
    Button button;

    float radius = 0;
    //--------------------------------------------------------------------------------//
    public ButtonRenderer() : base(MainActivity.Activity) { }

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

        button = Element as Button;

        if (e.OldElement == null)
        {
            if (IsPostLollipopAndroid) Control.Elevation = 2;

            radius = (float)button.CornerRadius;

            if (button.Padding != null)
            {
                Control.SetPadding(ToPx((int)button.Padding.Left), ToPx((int)button.Padding.Top), ToPx((int)button.Padding.Right), ToPx((int)button.Padding.Bottom));
            }

            SetColors();

            Control.SetMinHeight(0);
            Control.SetMinimumHeight(0);
            Control.SetMinWidth(0);
            Control.SetMinimumWidth(0);
        }
    }
    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        base.OnElementPropertyChanged(sender, e);

        switch (e.PropertyName)
        {
            case nameof(VisualElement.BackgroundColor):
            case nameof(Button.PressedColor):
                {
                    SetColors();

                    break;
                }
            case nameof(Button.Padding):
                {
                    if (button.Padding != null)
                    {
                        Control.SetPadding((int)button.Padding.Left, (int)button.Padding.Top, (int)button.Padding.Right, (int)button.Padding.Bottom);
                    }

                    break;
                }
        }
    }

    public override bool DispatchTouchEvent(MotionEvent e)
    {
        if (!Element.InputTransparent) return base.DispatchTouchEvent(e);
        else return true;
    }

    void SetColors()
    {
        radius = (float)button.CornerRadius;

        StateListDrawable list = new StateListDrawable();

        if (IsPreLollipopAndroid)
        {
            GradientDrawable pressedDrawable;

            if (Element.BackgroundColor != Color.Transparent)
            {
                Android.Graphics.Color pressedColor = ChangeColorBrightness(Element.BackgroundColor, (button.PressedColor == ButtonPressedColor.Dark) ? 1f - (float).15 : 1f + (float).15).ToAndroid();
                pressedDrawable = new GradientDrawable(GradientDrawable.Orientation.LeftRight, new int[] { pressedColor, pressedColor });

                pressedDrawable.SetCornerRadius(ToPxFloat(radius));
            }
            else
            {
                Android.Graphics.Color pressedColor = (button.PressedColor == ButtonPressedColor.Dark) ? Color.Black.MultiplyAlpha(.15).ToAndroid() : Color.White.MultiplyAlpha(.15).ToAndroid();
                pressedDrawable = new GradientDrawable(GradientDrawable.Orientation.LeftRight, new int[] { pressedColor, pressedColor });

                pressedDrawable.SetCornerRadius(ToPxFloat(radius));
            }

            list.AddState(new int[] { Android.Resource.Attribute.StatePressed }, pressedDrawable);

            //--------------------------------------------------------------------------------//

            Android.Graphics.Color color = Element.BackgroundColor.ToAndroid();
            var normalDrawable = new GradientDrawable(GradientDrawable.Orientation.LeftRight, new int[] { color, color });

            normalDrawable.SetCornerRadius(ToPxFloat(radius));

            list.AddState(new int[] { -Android.Resource.Attribute.StatePressed }, normalDrawable);

            Control.Background = list;
        }
        else
        {
            GradientDrawable normalDrawable = new GradientDrawable();
            normalDrawable.SetCornerRadius(ToPxFloat(radius));
            normalDrawable.SetColor(Element.BackgroundColor.ToAndroid());

            float[] outerRadii = Enumerable.Repeat(ToPxFloat(radius), 8).ToArray();

            RoundRectShape r = new RoundRectShape(outerRadii, null, null);

            ShapeDrawable shapeDrawable = new ShapeDrawable(r);
            shapeDrawable.Paint.Color = Color.White.ToAndroid();

            var pressedColor = (button.PressedColor == ButtonPressedColor.Dark) ? Color.Black.MultiplyAlpha(.15).ToAndroid() : Color.White.MultiplyAlpha(.15).ToAndroid();
            var ripple = new RippleDrawable(ColorStateList.ValueOf(pressedColor), normalDrawable, shapeDrawable);

            Control.Background = ripple;
        }
    }

    public static int ToPx(double dp)
    {
        return (int)Android.Util.TypedValue.ApplyDimension(Android.Util.ComplexUnitType.Dip, (float)dp, Droid.MainActivity.Activity.Resources.DisplayMetrics);
    }

    public static float ToPxFloat(double dp)
    {
        return Android.Util.TypedValue.ApplyDimension(Android.Util.ComplexUnitType.Dip, (float)dp, Droid.MainActivity.Activity.Resources.DisplayMetrics);
    }

    public static bool IsPreLollipopAndroid => Android.OS.Build.VERSION.SdkInt < Android.OS.BuildVersionCodes.Lollipop;
    public static bool IsPostLollipopAndroid => Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Lollipop;

    public static Color ChangeColorBrightness(Color color, float factor)
    {
        return Color.FromRgba(color.R * factor, color.G * factor, color.B * factor, color.A);
    }
}

The Button class has Padding, CornerRadius and PressedColor (an enum with Dark and Light fields).

Hope it helps!