Published Spring, 2019

Buttons in Android are restricted to rectangular shapes. While button images may be round or some other shape, the touch area is always a rectangle of some kind (Note: A square is just a rectangle where all four sides are of equal length). So if you want a button with a non-rectangular shape, you have to accept that the touch area will extend beyond the edges of the visible button. Even Android’s Floating Action Button, which appears to be round, has a square touch area. There is, however, a simple way to restrict the touch area of a button to just the area inside the image. The trick is to override the onTouchEvent method and only process events located in areas which aren’t invisible. To accomplish this, you must first setDrawingCacheEnabled to true in the constructor. Then, in the onTouchEvent method, get the drawing cache for the button view and extract the pixel at the touch location. If the color of the pixel is Color.TRANSPARENT, then don’t process the touch event. Otherwise, proceed as normal. For an example of a use case for this type of button, consider the two button images below.

Both the red and blue buttons have non-rectangular shapes. Also, their image areas are not contiguous. In fact, if you overlay the images in an app, the touch area of each button includes a circle inside the main touch area of the other button.

If you were to use these two images for traditional ImageButtons in Android, you’d only get touch events from the button which happened to be placed on top. By using a PolymorphousButton, you can have each button’s touch area correspond to its associated color in an intuitive way. A use case for such a button might be a game board with a complex, non-rectangular shape. For instance, a bulls eye.

You can find the code for this PolymorphousButton along with a working example using the button images above on my Github page here: Polymorphous Button

class PolymorphousButton : AppCompatImageButton {
  constructor(context: Context) : super(context) {
    // You need to set DrawingCacheEnabled to true
    isDrawingCacheEnabled = true
  }

  constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
    // You need to set DrawingCacheEnabled to true
    isDrawingCacheEnabled = true
  }
  constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
    // You need to set DrawingCacheEnabled to true
    isDrawingCacheEnabled = true
  }

  override fun onTouchEvent(event: MotionEvent): Boolean {

    // Process ACTION_UP and ACTION_DOWN events
    when (event.action) {
      MotionEvent.ACTION_DOWN -> {

// For the ACTION_DOWN event, we want to check the color at the touch location
        val touchLocationColor: Int

        // Get the location of the touch event and cast to an Int
        val xTouchLocation = event.x.toInt()
        val yTouchLocation = event.y.toInt()

        try {

// Get the pixel at the touch location, which means getting the
// pixel color.
          touchLocationColor = drawingCache.getPixel(xTouchLocation, yTouchLocation)
        } catch (e: IllegalArgumentException) {

        // The IllegalArgumentException error will get thrown only if the
        // location of the touch event was outside the bounds of the view.
        // And in that case, it obviously should be bypassed.

          return false
        }

// Return true and accept the touch event if the color of the pixel at the
// touch location was TRANSPARENT. Otherwise, don't process the event.

          return touchLocationColor != Color.TRANSPARENT
        }

        MotionEvent.ACTION_UP -> {

// Presumably, by this point, you have determined that the touch event was
// inside the view bounds and not TRANSPARENT. So you can process the click.
        performClick()
        return true
        }
      }
      return false // Return false for other touch events
  }

// Depending on your Lint configuration, Android may show an error telling
// you that you need to override the performClick method. This is for
// accessibility. Google wants you to provide an implementation for users who
// don't use the touch screen. But in this case, since the override is specific
// to the touch screen, you can override it with a no-op implementation.

}