Pylomorphous Buttons In Android
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.



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.
}