0

I have looked at the following:

imagebutton with @null background (transparent)

How to prevent onClick method on transparent portion of a PNG-loaded ImageView

And countless others, so I must be missing it.

I have an ImageButton. The image represented by the button is a PNG with transparency. Everything displays great. However, when I click the button on the transparency, the click event fires. That is not what I want at all.

I am looking for a solution, and it's probably obvious and I am missing it, where the transparency does not count in the hit test of the button.

I want to do this all in code, not in xml.

So far I am initializing my ImageButton like this. This is all in Xamarin.Android, but that shouldn't matter. The syntax will be C# instead of Java.

// make the states:
var states = new StateListDrawable();
states.AddState(new int[] { -Android.Resource.Attribute.StateEnabled }, new BitmapDrawable(Context.Resources, disabledImage));
states.AddState(StateSet.WildCard.ToArray(), drawable);

// setup the button
var button = new Android.Widget.ImageButton(Context);

button.SetBackgroundColor(Android.Graphics.Color.Transparent);
button.SetPadding(0, 0, 0, 0);
button.SetImageDrawable(states);

// I am using this in a custom renderer, so these are my 
// click handlers. I don't think that matters, but maybe it does?
button.SetOnClickListener(ButtonClickListener.Instance.Value);
button.SetOnTouchListener(ButtonTouchListener.Instance.Value);
button.Tag = this;

Again, everything displays perfectly, the only thing is the click is triggered on the transparent part of the image, which is what I don't want.

EDIT

My Click/Touch Listener Code. This is pulled from the Platform Button Render in Xamarin.Forms

https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Platform.Android/Renderers/ButtonRenderer.cs#L251-L284

class ButtonClickListener : Object, IOnClickListener
{
    public static readonly Lazy<ButtonClickListener> Instance = new Lazy<ButtonClickListener>(() => new ButtonClickListener());

    public void OnClick(AView v)
    {
        var renderer = v.Tag as MyButtonRenderer;

        if (renderer != null)
            ((IButtonController)renderer.Element).SendClicked();
    }
}

class ButtonTouchListener : Object, IOnTouchListener
{
    public static readonly Lazy<ButtonTouchListener> Instance = new Lazy<ButtonTouchListener>(() => new ButtonTouchListener());

    public bool OnTouch(AView v, AMotionEvent e)
    {
        var renderer = v.Tag as MyButtonRenderer;

        if (renderer != null)
        {
            var buttonController = renderer.Element as IButtonController;
            if (e.Action == AMotionEventActions.Down)
            {
                buttonController?.SendPressed();
            }
            else if (e.Action == AMotionEventActions.Up)
            {
                buttonController?.SendReleased();
            }
        }
        return false;
    }
}

Things I have tried: in the OnTouchListener

  • Attempt to get renderer.Control.DrawingCache so I could get a Bitmap and test for a transparent pixel. The DrawingCache always returns null. When setting up the button, I also button.DrawingCacheEnabled = true;

  • If the DrawingCache was null in the OnTouchListener attempt to build it and grab it

For Example:

if (cache == null)
{
    renderer.Control.BuildDrawingCache();
    cache = renderer.Control.DrawingCache;
}

Still always null.

  • Attempt to draw the control to a bitmap to test for the transparent pixel. This was a bad idea generally speaking, but I just wanted to see if it worked.

For Example:

Bitmap bitmap = Bitmap.CreateBitmap(renderer.Control.Width, renderer.Control.Height, Bitmap.Config.Argb8888);
Canvas canvas = new Canvas(bitmap);
renderer.Control.Draw(canvas);

In the above cases, once I had a, or thought I had, a Bitmap representation I would check the pixel like this:

int color = bitmap.GetPixel((int)e.GetX(), (int)e.GetY());
if (color == Android.Graphics.Color.Transparent)
    return false;

The above attempts were in service of trying this methodology: https://stackoverflow.com/a/19566795/1060314

Nothing above seemed to work. I may have been on the correct path but just missed something critical along the way.

AJ Venturella
  • 4,742
  • 4
  • 33
  • 62

1 Answers1

0

I ended up subclassing ImageButton, and this did the trick.

using System;
using Android.Content;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.Util;
using Android.Widget;
using AMotionEvent = Android.Views.MotionEvent;

namespace MyProject.Droid.Renderers
{
    public class MDImageButton : Android.Widget.ImageButton
    {
        public MDImageButton(Context context) : this(context, null)
        { }
        public MDImageButton(Context context, IAttributeSet attrs) : this(context, attrs, Android.Resource.Attribute.ImageButtonStyle)
        { }


        public MDImageButton(Context context, IAttributeSet attrs, int defStyleAttr) : this(context, attrs, defStyleAttr, 0)
        { }

        public MDImageButton(Context context, IAttributeSet attrs, int defStyleAttr, int defStyleRes) : base(context, attrs, defStyleAttr, defStyleRes)
        {
            Focusable = true;
        }

        public override bool OnTouchEvent(AMotionEvent e)
        {
            var view = (ImageView)this;
            var currentState = ((StateListDrawable)this.Drawable).Current;

            if (currentState is BitmapDrawable)
            {
                var x = (int)e.GetX();
                var y = (int)e.GetY();

                if (isPixelTransparent(x, y))
                    return false;
                else
                    return base.OnTouchEvent(e);
            }

            return base.OnTouchEvent(e);
        }

        private bool isPixelTransparent(int x, int y)
        {
            var currentState = ((StateListDrawable)this.Drawable).Current;
            Bitmap bmp = ((BitmapDrawable)currentState).Bitmap;

            int color = Android.Graphics.Color.Transparent;

            try
            {
                color = bmp.GetPixel(x, y);
            }
            catch (Exception e)
            {
                // x or y exceed the bitmap's bounds.
                // Reverts the View's internal state from a previously set "pressed" state.
                Pressed = false;
            }

            // Ignores touches on transparent background.
            if (color == Android.Graphics.Color.Transparent)
                return true;
            else
                return false;
        }
    }
}

I found I needed to do it at this level not the Renderer because while:

button.SetOnClickListener(ButtonClickListener.Instance.Value);
button.SetOnTouchListener(ButtonTouchListener.Instance.Value);

does the right thing, the OnClickListener is invoked regardless of what the TouchListener does.

AJ Venturella
  • 4,742
  • 4
  • 33
  • 62