package com.addisonelliott.segmentedbutton;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Path.Direction;
import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader.TileMode;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.RippleDrawable;
import android.graphics.drawable.VectorDrawable;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import androidx.annotation.ColorInt;
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import codetail.graphics.drawables.DrawableHotspotTouch;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@SuppressWarnings("unused")
@SuppressLint("RtlHardcoded")
public class SegmentedButton extends View
{
    // region Variables & Constants
    private static final String TAG = "SegmentedButton";

    // Bitmap used for creating bitmaps from the background & selected background drawables
    private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;

    // Intrinsic size (width & height) to use for creating a Bitmap from a ColorDrawable
    // A ColorDrawable has no intrinsic size on its own, so this size is used instead
    private static final int COLORDRAWABLE_SIZE = 2;

    @IntDef(flag = true, value = {
        Gravity.LEFT,
        Gravity.RIGHT,
        Gravity.TOP,
        Gravity.BOTTOM,
    })
    @Retention(RetentionPolicy.SOURCE)
    private @interface GravityOptions {}

    // General purpose rectangle to prevent memory allocation in onDraw
    private RectF rectF;
    // General purpose path to prevent memory allocation in onDraw
    private Path path;

    // Text paint variable contains paint info for unselected and selected text
    private TextPaint textPaint;
    // Static layout used for positioning and drawing unselected and selected text
    private StaticLayout textStaticLayout;
    // Maximum text width assuming all text is on one line, this is used in onMeasure to calculate the desired width
    private int textMaxWidth;

    // Position (X/Y) of the text and drawable
    private PointF textPosition, drawablePosition;

    // Clip path used to round background drawable edges to create rounded button group
    private Path backgroundClipPath;
    // Radius of the segmented button group used for creating background clip path
    private int backgroundRadius;
    // The button directly to the left and right of this current button
    // Preferably segmented buttons shouldn't NEED to know about the buttons beside it, but there is a special case
    // where its required
    // In addition, this is used to determine whether this button is the left-most or right-most in the group so the
    // correct side can be rounded out (if a background radius is specified)
    private SegmentedButton leftButton, rightButton;

    // Paint objects used for drawing the background and selected background drawables with rounded corners if desired
    // The background paint object will only be used if a drawable is present and the background radius is greater
    // than 0 (meaning there is rounded corners). Similarly, for the selected background, if a drawable is present
    // and the background radius is greater than 0 OR there is a selected button radius.
    // Paint objects will contain a BitmapShader that is linked to a Bitmap created from the respective drawables
    //
    // Note: The BitmapShader approach is used rather than Canvas.clipPath because antialiasing is supported in the
    // former but not the latter
    private Paint backgroundPaint;
    private Paint selectedBackgroundPaint;

    // Radius of the selected button used for creating a rounded selected button
    private int selectedButtonRadius;
    // Corner radii for the selected button, this contains 8x values all set to selectedButtonRadius
    // This is used to prevent allocation in the onDraw method
    private float[] selectedButtonRadii;

    // Paint information for how the border should be drawn for the selected button, null indicates no border
    private Paint selectedButtonBorderPaint;

    // Horizontal relative clip position from 0.0f to 1.0f.
    // Value is scaled by the width of this view to get the actual clip X coordinate
    private float relativeClipPosition;
    // Whether or not the clipping is occurring from the left (true) or right (false). In simpler terms, if true,
    // then the start clipping relative position is 0.0f, otherwise, if clipping from the right, the position is 1.0f
    private boolean isClippingLeft;

    // Drawable for the background, this will be a ColorDrawable in the case a solid color is given
    private Drawable backgroundDrawable;
    // Drawable for the background when selected, this will be a ColorDrawable in the case a solid color is given
    private Drawable selectedBackgroundDrawable;

    // Should button have rounded corners regardless of position
    private boolean rounded;

    // Color of the ripple to display over the button (default value is gray)
    private int rippleColor;

    // RippleDrawable is used for drawing ripple animation when tapping buttons on Lollipop and above devices (API 21+)
    private RippleDrawable rippleDrawableLollipop;
    // Backport for RippleDrawable for API 16-20 devices
    private codetail.graphics.drawables.RippleDrawable rippleDrawable;

    // Color filters used for tinting the button drawable in normal and when button is selected, will be null for no
    // tint
    private PorterDuffColorFilter drawableColorFilter, selectedDrawableColorFilter;

    // Drawable to draw for the button. Can be drawn beside text or without text at all
    private Drawable drawable;
    // Padding for the drawable in pixels, this will only be applied between the drawable and text (default value is 0)
    private int drawablePadding;
    // Whether or not there is a tint color for the drawable when unselected and/or selected
    private boolean hasDrawableTint, hasSelectedDrawableTint;
    // Tint color for the drawable when unselected and selected
    private int drawableTint, selectedDrawableTint;
    // Whether or not a width or height was specified for the drawable
    private boolean hasDrawableWidth, hasDrawableHeight;
    // Width and height for the drawable, in pixels
    private int drawableWidth, drawableHeight;
    // Determines where to draw the drawable in relation to the text, can be one of GravityOptions types
    private int drawableGravity;

    // Whether or not we have text, false indicates text should be empty
    private boolean hasText;
    // Text to display for button (default value is an empty string meaning no text will be shown)
    private String text;
    // Whether or not we have a selected text color
    private boolean hasSelectedTextColor;
    // Text color and selected text color (default value is gray for unselected, white for selected text colors)
    private int textColor, selectedTextColor;
    // Font size of the text in pixels (default value is 14sp)
    private float textSize;
    // Typeface for displaying the text and selected text, created from the fontFamily & textStyle attributes. Default value for selected is the text typeface.
    private Typeface textTypeface, selectedTextTypeface;

    // Internal listener that is called when the visibility of this button is changed
    private OnVisibilityChangedListener onVisibilityChangedListener;

    // endregion

    // region Constructor

    public SegmentedButton(Context context)
    {
        super(context);

        init(context, null);
    }

    public SegmentedButton(Context context, AttributeSet attrs)
    {
        super(context, attrs);

        init(context, attrs);
    }

    public SegmentedButton(Context context, AttributeSet attrs, int defStyleAttr)
    {
        super(context, attrs, defStyleAttr);

        init(context, attrs);
    }

    private void init(Context context, @Nullable AttributeSet attrs)
    {
        // Retrieve custom attributes
        getAttributes(context, attrs);

        initText();
        initDrawable();

        // Setup default values for clip position
        // By default, set to clip from left and have none of the selected view shown
        relativeClipPosition = 0.0f;
        isClippingLeft = true;

        // Setup background clip path parameters
        // This should be changed before onDraw is ever called but they are initialized to be safe
        backgroundRadius = 0;
        leftButton = null;
        rightButton = null;

        // Create general purpose rectangle, prevents memory allocation during onDraw
        rectF = new RectF();

        // Create general purpose path, prevents memory allocation during onDraw
        path = new Path();

        // Required in order for this button to 'consume' the ripple touch event
        setClickable(true);
    }

    private void getAttributes(Context context, @Nullable AttributeSet attrs)
    {
        // According to docs for obtainStyledAttributes, attrs can be null and I assume that each value will be set
        // to the default
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SegmentedButton);

        // Load background if available, this can be a drawable or a color
        // In the instance of a color, a ColorDrawable is created and used instead
        // Note: Not well documented but getDrawable will return a ColorDrawable if a color is specified
        if (ta.hasValue(R.styleable.SegmentedButton_android_background))
            backgroundDrawable = ta.getDrawable(R.styleable.SegmentedButton_android_background);

        // Load background on selection if available, can be drawable or color
        if (ta.hasValue(R.styleable.SegmentedButton_selectedBackground))
            selectedBackgroundDrawable = ta.getDrawable(R.styleable.SegmentedButton_selectedBackground);

        rounded = ta.getBoolean(R.styleable.SegmentedButton_rounded, false);

        // Parse ripple color value and update the ripple
        setRipple(ta.getColor(R.styleable.SegmentedButton_rippleColor, Color.GRAY));

        // Load drawable if available, otherwise variable will be null
        if (ta.hasValue(R.styleable.SegmentedButton_drawable))
        {
            int drawableResId = ta.getResourceId(R.styleable.SegmentedButton_drawable, -1);
            drawable = readCompatDrawable(context, drawableResId);
        }
        drawablePadding = ta.getDimensionPixelSize(R.styleable.SegmentedButton_drawablePadding, 0);
        hasDrawableTint = ta.hasValue(R.styleable.SegmentedButton_drawableTint);
        drawableTint = ta.getColor(R.styleable.SegmentedButton_drawableTint, -1);
        hasSelectedDrawableTint = ta.hasValue(R.styleable.SegmentedButton_selectedDrawableTint);
        selectedDrawableTint = ta.getColor(R.styleable.SegmentedButton_selectedDrawableTint, -1);
        hasDrawableWidth = ta.hasValue(R.styleable.SegmentedButton_drawableWidth);
        hasDrawableHeight = ta.hasValue(R.styleable.SegmentedButton_drawableHeight);
        drawableWidth = ta.getDimensionPixelSize(R.styleable.SegmentedButton_drawableWidth, -1);
        drawableHeight = ta.getDimensionPixelSize(R.styleable.SegmentedButton_drawableHeight, -1);
        drawableGravity = ta.getInteger(R.styleable.SegmentedButton_drawableGravity, Gravity.LEFT);

        hasText = ta.hasValue(R.styleable.SegmentedButton_text);
        text = ta.getString(R.styleable.SegmentedButton_text);
        textColor = ta.getColor(R.styleable.SegmentedButton_textColor, Color.GRAY);
        hasSelectedTextColor = ta.hasValue(R.styleable.SegmentedButton_selectedTextColor);
        selectedTextColor = ta.getColor(R.styleable.SegmentedButton_selectedTextColor, Color.WHITE);

        // Convert 14sp to pixels for default value on text size
        final float px14sp = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 14.0f,
            context.getResources().getDisplayMetrics());
        textSize = ta.getDimension(R.styleable.SegmentedButton_textSize, px14sp);

        final boolean hasFontFamily = ta.hasValue(R.styleable.SegmentedButton_android_fontFamily);
        final int textStyle = ta.getInt(R.styleable.SegmentedButton_textStyle, Typeface.NORMAL);
        final int selectedTextStyle = ta.getInt(R.styleable.SegmentedButton_selectedTextStyle, textStyle);

        // If a font family is present then load typeface with text style from that
        if (hasFontFamily)
        {
            // Note: TypedArray.getFont is used for Android O & above while ResourcesCompat.getFont is used for below
            // Experienced an odd bug in the design viewer of Android Studio where it would not work with only using
            // the ResourcesCompat.getFont function. Unsure of the reason but this fixes it
            if (VERSION.SDK_INT >= VERSION_CODES.O)
            {
                textTypeface = Typeface.create(ta.getFont(R.styleable.SegmentedButton_android_fontFamily), textStyle);
                selectedTextTypeface = Typeface.create(ta.getFont(R.styleable.SegmentedButton_android_fontFamily), selectedTextStyle);
            }
            else
            {
                final int fontFamily = ta.getResourceId(R.styleable.SegmentedButton_android_fontFamily, 0);

                if (fontFamily > 0)
                {
                    textTypeface = Typeface.create(ResourcesCompat.getFont(context, fontFamily), textStyle);
                    selectedTextTypeface = Typeface.create(ResourcesCompat.getFont(context, fontFamily), selectedTextStyle);
                }
                else
                {
                    // On lower API Android versions, fontFamily returns 0 for default fonts such as "sans-serif" and
                    // "monospace". Thus, we get the font as a string and then try to load that way
                    textTypeface = Typeface.create(ta.getString(R.styleable.SegmentedButton_android_fontFamily),
                        textStyle);
                    selectedTextTypeface = Typeface.create(ta.getString(R.styleable.SegmentedButton_android_fontFamily),
                        selectedTextStyle);
                }
            }
        }
        else
        {
            textTypeface = Typeface.create((Typeface)null, textStyle);
            selectedTextTypeface = Typeface.create((Typeface)null, selectedTextStyle);
        }

        ta.recycle();
    }

    private void initText()
    {
        // Text position is calculated regardless of if text exists
        // Not worth extra effort of not setting two float values
        textPosition = new PointF();

        // If there is no text then do not bother
        if (!hasText)
        {
            textStaticLayout = null;
            return;
        }

        // Create text paint that will be used to draw the text on the canvas
        textPaint = new TextPaint();
        textPaint.setAntiAlias(true);
        textPaint.setTextSize(textSize);
        textPaint.setColor(textColor);
        textPaint.setTypeface(textTypeface);

        // Initial kickstart to setup the text layout by assuming the text will be all in one line
        textMaxWidth = (int)textPaint.measureText(text);
        if (Build.VERSION.SDK_INT >= VERSION_CODES.M)
        {
            textStaticLayout = StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, textMaxWidth).build();
        }
        else
        {
            textStaticLayout = new StaticLayout(text, textPaint, textMaxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0,
                false);
        }
    }

    private Drawable readCompatDrawable(Context context, int drawableResId)
    {
        Drawable drawable = AppCompatResources.getDrawable(context, drawableResId);

        // API 28 has a bug with vector drawables where the selected tint color is always applied to the drawable
        // To prevent this, the vector drawable is converted to a bitmap
        if ((VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && drawable instanceof VectorDrawable)
            || drawable instanceof VectorDrawableCompat)
        {
            Bitmap bitmap = getBitmapFromVectorDrawable(drawable);
            return new BitmapDrawable(context.getResources(), bitmap);
        }
        else
            return drawable;
    }


    private void initDrawable()
    {
        // Drawable position is calculated regardless of if drawable exists
        // Not worth extra effort of not setting two float values
        drawablePosition = new PointF();

        // If there is no drawable then do not bother
        if (drawable == null)
            return;

        // If drawable has a tint color, then create a color filter that will be applied to it
        if (hasDrawableTint)
            drawableColorFilter = new PorterDuffColorFilter(drawableTint, PorterDuff.Mode.SRC_IN);

        // If selected drawable has a tint color, then create a color filter that will be applied to it
        if (hasSelectedDrawableTint)
            selectedDrawableColorFilter = new PorterDuffColorFilter(selectedDrawableTint, PorterDuff.Mode.SRC_IN);
    }

    // endregion

    // region Layout & Measure

    @SuppressLint("DrawAllocation")
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        // Measured width & height
        int width, height;

        // Calculate drawable width, 0 if null, drawableWidth if set, otherwise intrinsic width
        final int drawableWidth = drawable != null ? hasDrawableWidth ? this.drawableWidth
            : drawable.getIntrinsicWidth() : 0;
        // For the text width, assume that it is in a single line with no wrapping which would be textMaxWidth
        // This variable is used to calculate the desired width and the desire is for it all to be in a single line
        final int textWidth = hasText ? textMaxWidth : 0;

        // Desired width will always have left & right padding regardless of horizontal/vertical gravity for the
        // drawable and text.
        int desiredWidth = getPaddingLeft() + getPaddingRight();

        if (Gravity.isHorizontal(drawableGravity))
        {
            // When drawable and text are inline horizontally, then the total desired width is:
            //     padding left + text width (assume one line) + drawable padding + drawable width + padding right
            desiredWidth += textWidth + drawablePadding + drawableWidth;
        }
        else
        {
            // When drawable and text are on top of each other, the total desired width is:
            //     padding left + max(text width, drawable width) + padding right
            desiredWidth += Math.max(textWidth, drawableWidth);
        }

        // Resolve width with measure spec and desired width
        // Three options:
        //      - MeasureSpec.EXACTLY: Set width to exactly specified size
        //      - MeasureSpec.AT_MOST: Set width to desired size but dont exceed specified size
        //      - MeasureSpec.UNSPECIFIED: Set width to desired size
        width = resolveSize(desiredWidth, widthMeasureSpec);

        // With width calculated, recalculate the text parameters to get new height (wrapping may occur)
        measureTextWidth(width, drawableWidth);

        // Repeat measuring process for height now
        // Note that the height is the static layout height which may or may not be multi-lined
        // Calculate drawable height, 0 if null, drawableHeight if set, otherwise intrinsic height
        final int drawableHeight = drawable != null ? hasDrawableHeight ? this.drawableHeight
            : drawable.getIntrinsicHeight() : 0;
        final int textHeight = hasText ? textStaticLayout.getHeight() : 0;

        int desiredHeight = getPaddingTop() + getPaddingBottom();

        if (Gravity.isHorizontal(drawableGravity))
        {
            // When drawable and text are horizontal, the total desired height is:
            //     padding left + max(text width, drawable width) + padding right
            desiredHeight += Math.max(textHeight, drawableHeight);
        }
        else
        {
            // When drawable and text are vertical, then the total desired height is:
            //     padding left + text width (assume one line) + drawable padding + drawable width + padding right
            desiredHeight += textHeight + drawablePadding + drawableHeight;
        }

        // Resolve height with measure spec and desired height
        // Three options:
        //      - MeasureSpec.EXACTLY: Set height to exactly specified size
        //      - MeasureSpec.AT_MOST: Set height to desired size but dont exceed specified size
        //      - MeasureSpec.UNSPECIFIED: Set height to desired size
        height = resolveSize(desiredHeight, heightMeasureSpec);

        // Required to be called to notify the View of the width & height decided
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh)
    {
        super.onSizeChanged(w, h, oldw, oldh);

        // Calculate new positions and bounds for text & drawable
        updateSize();

        // Recalculate the background clip path since width & height have changed
        setupBackgroundClipPath();
    }

    /**
     * Create new static text layout with new measured text width based off the total width of the button and the
     * drawable width.
     *
     * This does nothing if the button has no text to display
     *
     * @param width         size, in pixels, of the button
     * @param drawableWidth size, in pixels, of the drawable
     */
    private void measureTextWidth(int width, int drawableWidth)
    {
        // If there is no text, then we don't need to do anything
        if (!hasText)
            return;

        // Set drawable width to be the drawable width if the drawable has horizontal gravity, otherwise the drawable
        // width doesnt matter
        // Text width is equal to the total width minus padding and drawable width
        // But, if the maximum text width is smaller, just use that and we will manually pad it later
        int newDrawableWidth = Gravity.isHorizontal(drawableGravity) ? drawableWidth : 0;
        int textWidth = Math.min(width - getPaddingLeft() - getPaddingRight() - newDrawableWidth, textMaxWidth);

        // Odd case where there is not enough space for the padding and drawable width so we just return
        if (textWidth < 0)
            return;

        // Create new static layout with width
        // Old way of creating static layout was deprecated but I dont think there is any speed difference between
        // the two
        if (Build.VERSION.SDK_INT >= VERSION_CODES.M)
        {
            textStaticLayout = StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, textWidth).build();
        }
        else
        {
            textStaticLayout = new StaticLayout(text, textPaint, textWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0,
                false);
        }
    }

    /**
     * Calculate new bounds for all elements in the button
     *
     * This will be called on onSizeChanged, which is called less frequently than onMeasure which will speed things up
     */
    private void updateSize()
    {
        final int width = getWidth(), height = getHeight();
        final int textWidth = hasText ? textStaticLayout.getWidth() : 0;
        final int textHeight = hasText ? textStaticLayout.getHeight() : 0;
        final int drawableWidth = drawable != null ? hasDrawableWidth ? this.drawableWidth
            : drawable.getIntrinsicWidth() : 0;
        final int drawableHeight = drawable != null ? hasDrawableHeight ? this.drawableHeight
            : drawable.getIntrinsicHeight() : 0;

        // Calculates the X/Y positions of the text and drawable now that the measured size is known
        if (Gravity.isHorizontal(drawableGravity))
        {
            // Calculate Y position for horizontal gravity, i.e. center the drawable and/or text if necessary
            textPosition.y = getPaddingTop()
                + (height - getPaddingTop() - getPaddingBottom() - textHeight) / 2.0f;
            drawablePosition.y = getPaddingTop()
                + (height - getPaddingTop() - getPaddingBottom() - drawableHeight) / 2.0f;

            // Calculate the starting X position with horizontal gravity
            // startPosition is half of the remaining space to center the drawable and text
            final float startPosition = (width - textWidth - drawableWidth - drawablePadding) / 2.0f;

            // Position the drawable & text based on the gravity
            if (drawableGravity == Gravity.LEFT)
            {
                textPosition.x = startPosition + drawableWidth + drawablePadding;
                drawablePosition.x = startPosition;
            }
            else if (drawableGravity == Gravity.RIGHT)
            {
                textPosition.x = startPosition;
                drawablePosition.x = startPosition + textWidth + drawablePadding;
            }
        }
        else
        {
            // Calculate X position for vertical gravity, i.e. center the drawable and/or text horizontally if necessary
            textPosition.x = getPaddingLeft()
                + (width - getPaddingLeft() - getPaddingRight() - textWidth) / 2.0f;
            drawablePosition.x = getPaddingLeft()
                + (width - getPaddingLeft() - getPaddingRight() - drawableWidth) / 2.0f;

            // Calculate the starting Y position with vertical gravity
            // startPosition is half of the remaining space to center the drawable and text
            final float startPosition = (height - textHeight - drawableHeight - drawablePadding) / 2.0f;

            // Position the drawable & text based on the gravity
            if (drawableGravity == Gravity.TOP)
            {
                textPosition.y = startPosition + drawableHeight + drawablePadding;
                drawablePosition.y = startPosition;
            }
            else if (drawableGravity == Gravity.BOTTOM)
            {
                textPosition.y = startPosition;
                drawablePosition.y = startPosition + textHeight + drawablePadding;
            }
        }

        // Set bounds of drawable if it exists
        if (drawable != null)
        {
            drawable.setBounds((int)drawablePosition.x, (int)drawablePosition.y,
                (int)drawablePosition.x + drawableWidth, (int)drawablePosition.y + drawableHeight);
        }

        // Set bounds of background drawable if it exists
        if (backgroundDrawable != null)
            backgroundDrawable.setBounds(0, 0, width, height);

        // Set bounds of selected background drawable if it exists
        if (selectedBackgroundDrawable != null)
            selectedBackgroundDrawable.setBounds(0, 0, width, height);

        // Set bounds of ripple drawable if it exists
        if (rippleDrawableLollipop != null)
            rippleDrawableLollipop.setBounds(0, 0, width, height);

        // Set bounds of ripple drawable if it exists
        if (rippleDrawable != null)
            rippleDrawable.setBounds(0, 0, width, height);
    }

    // endregion

    // region Drawing

    @Override
    protected void onDraw(Canvas canvas)
    {
        super.onDraw(canvas);

        final int width = getWidth();
        final int height = getHeight();

        // Draw background (unselected)
        if (backgroundDrawable != null)
        {
            // Draw the background with rounded corners if the background clip path and background paint objet are
            // non-null. The background clip path will be present if the background has rounded corners. See
            // setupBackgroundClipPath for more details. Ideally the backgroundPaint object will always be present
            // when backgroundClipPath is present but there are select cases when the bitmap cannot be generated from
            // the drawable because of unknown bounds on program start.
            //
            // Otherwise, the background is drawn normally via the drawable with no rounded corners
            if (backgroundClipPath != null && backgroundPaint != null)
                canvas.drawPath(backgroundClipPath, backgroundPaint);
            else
                backgroundDrawable.draw(canvas);
        }

        // Draw text (unselected)
        if (hasText)
        {
            canvas.save();
            canvas.translate(textPosition.x, textPosition.y);
            textPaint.setColor(textColor);
            textPaint.setTypeface(textTypeface);
            textStaticLayout.draw(canvas);
            canvas.restore();
        }

        // Draw drawable (unselected)
        if (drawable != null)
        {
            drawable.setColorFilter(drawableColorFilter);
            drawable.draw(canvas);
        }

        // Begin drawing selected button view
        canvas.save();

        // Clip canvas for drawing selected button items
        // The relativeClipPosition and isClippingLeft is used to clip part of the selected button view to allow for
        // smooth animation between one button to the next
        //
        // If isClippingLeft is true, then the left side of the selected button is being clipped (i.e. shown) and the
        // right side is hidden. If isClippingLeft is false, then the right side of the selected button is being
        // clipped and the left side is hidden.
        //
        // The amount of the left or right side being shown is based on the relativeClippingPosition, a value from
        // 0.0f to 1.0f representing the relative position on the button.
        if (isClippingLeft)
        {
            // If clipping the left, then relativeClipPosition * width represents the right side of the selected
            // button that is shown/clipped.
            //
            // The left side of the clip rectangle is set to be the relative clip position minus 1.0f times the width
            // of the button directly to the left of this button. This will be a negative value (relativeClipPosition
            // ranges from 0.0f to 1.0f, so subtracting 1.0f will make it range from -1.0f to 0.0f) and is scaled by
            // the button width directly to the left of this button. The width of this button may not be the same as
            // the one to the left so this is necessary.
            //
            // The reason the left side is set to a negative value as opposed to just 0.0f is because it is necessary
            // for a smooth animation when the selected button has rounded corners (i.e. selectedButtonRadius > 0).
            // Without the negative left clip side, the rounded corners will not smoothly transition from the button
            // to the left to this button.
            //
            // For the left-most button, the left button width is set to be the width of this button because it
            // doesn't matter.
            final float leftButtonWidth = isLeftButton() ? width : leftButton.getWidth();
            rectF.set((relativeClipPosition - 1.0f) * leftButtonWidth, 0.0f, relativeClipPosition * width, height);
        }
        else
        {
            // Otherwise, if clipping the right, then the relativeClipPosition * width represents the left side of
            // the selected button that is shown/clipped.
            //
            // The right side of the clip rectangle is set to be the width plus the relativeClipPosition times the
            // width of the button directly to the right of this button. Note that the width of the button to the
            // right may not be the same as the width of this button.
            //
            // The reason the right side is set to a value greater than the width as opposed to just the width itself
            // is because it is necessary for a smooth animation when the selected button has rounded corners (i.e.
            // selectedButtonRadius > 0). Without the correct right clip side, the rounded corners will not smoothly
            // transition from the button to the right to this button.
            final float rightButtonWidth = isRightButton() ? width : rightButton.getWidth();
            rectF.set(relativeClipPosition * width, 0.0f, width + relativeClipPosition * rightButtonWidth, height);
        }

        // Clip canvas for drawing the selected button view
        // Allows for smooth animation between one button to the next
        canvas.clipRect(rectF);

        // Draw background (selected)
        //
        // Draw the selected background with rounded corners in two cases:
        //      1. Selected button has rounded corners (i.e. selectedButtonRadius > 0)
        //      2. Background has a radius (i.e. backgroundRadius > 0)
        // In these two cases, the background is drawn using a BitmapShader contained in the background paint object/
        // Otherwise, the background is drawn normally via the drawable with no rounded corners.
        if (selectedButtonRadius > 0 && selectedBackgroundPaint != null)
        {
            path.reset();
            path.addRoundRect(rectF, selectedButtonRadii, Direction.CW);

            canvas.drawPath(path, selectedBackgroundPaint);
        }
        else if (backgroundClipPath != null && selectedBackgroundPaint != null)
        {
            canvas.drawPath(backgroundClipPath, selectedBackgroundPaint);
        }
        else if (selectedBackgroundDrawable != null)
        {
            selectedBackgroundDrawable.draw(canvas);
        }

        // Draw text (selected)
        if (hasText)
        {
            canvas.save();
            canvas.translate(textPosition.x, textPosition.y);
            // If a selected text color was specified, then use that, otherwise we want to default to the original
            // text color
            textPaint.setColor(hasSelectedTextColor ? selectedTextColor : textColor);
            textPaint.setTypeface(selectedTextTypeface);
            textStaticLayout.draw(canvas);
            canvas.restore();
        }

        // Draw drawable (selected)
        if (drawable != null)
        {
            // If a selected drawable tint was used, then use that, but if it wasn't specified we want to stick with
            // the normal tint color.
            drawable.setColorFilter(hasSelectedDrawableTint ? selectedDrawableColorFilter : drawableColorFilter);
            drawable.draw(canvas);
        }

        // Draw a border around the selected button
        if (selectedButtonBorderPaint != null)
        {
            // Get the border width from the paint information and divide by 2
            // Remember that rectF is the rectangle that was setup for the appropriate clip path above
            // Note that this rectangle should NOT be touched after the clip path is set otherwise the border drawn
            // will be incorrect.
            //
            // The rectangle is inset by half of the border width because the border width is centered about the
            // rectangle bounds resulting in half of the border being cut off since it is outside the clip path. In
            // addition, the inset is reduced by half a pixel (0.5f) to ensure there is no antialiasing bleed through
            // around the edge of the border.
            final float halfBorderWidth = selectedButtonBorderPaint.getStrokeWidth() / 2.0f;
            rectF.inset(halfBorderWidth - 0.5f, halfBorderWidth - 0.5f);

            // Note: A path is used here rather than canvas.drawRoundRect because there was odd behavior on API 19
            // and particular devices where the border radius did not match the background radius.
            path.reset();
            path.addRoundRect(rectF, selectedButtonRadii, Direction.CW);

            canvas.drawPath(path, selectedButtonBorderPaint);
        }

        canvas.restore();

        canvas.save();

        // Clip to the background clip path if available
        // This is used so the ripple effect will stop at the rounded corners of the background
        if (backgroundClipPath != null)
        {
            canvas.clipPath(backgroundClipPath);
        }

        // Draw ripple drawable to show ripple effect on click
        if (rippleDrawableLollipop != null)
        {
            rippleDrawableLollipop.draw(canvas);
        }

        // Draw ripple drawable to show ripple effect on click
        if (rippleDrawable != null)
        {
            rippleDrawable.draw(canvas);
        }

        canvas.restore();
    }

    /**
     * Horizontally clips selected button view from the left side (0.0f) to relativePosition
     *
     * For example, a relativePosition of 1.0f would mean the entire selected button view would be available and no
     * clipping would occur.
     *
     * However, a relative position of 0.0f would mean the entire selected button view is clipped and the normal
     * button view is entirely visible.
     *
     * This can be thought of as the selected button view being clipped from 0.0f on the left to the relativePosition
     * with 1.0f being all the way on the right.
     *
     * @param relativePosition Position from 0.0f to 1.0f that represents where to end clipping. A value of 0.0f
     *                         would represent no clipping and 1.0f would represent clipping the entire view
     */
    void clipLeft(@FloatRange(from = 0.0, to = 1.0) float relativePosition)
    {
        // Clipping from the left side, set to true
        isClippingLeft = true;

        // Update relative clip position
        relativeClipPosition = relativePosition;

        // Redraw
        invalidate();
    }

    /**
     * Horizontally clips selected button view from the right side (1.0f) to relativePosition
     *
     * For example, a relativePosition of 0.0f would mean the entire selected button view would be available and no
     * clipping would occur.
     *
     * However, a relative position of 1.0f would mean the entire selected button view is clipped and the normal
     * button view is entirely visible.
     *
     * This can be thought of as the selected button view being clipped from 0.0f on the left to the relativePosition
     * with 1.0f being all the way on the right.
     *
     * @param relativePosition Position from 0.0f to 1.0f that represents where to end clipping. A value of 1.0f
     *                         would represent no clipping and 0.0f would represent clipping the entire view
     */
    void clipRight(@FloatRange(from = 0.0, to = 1.0) float relativePosition)
    {
        // Clipping from the right side, set to false
        isClippingLeft = false;

        // Update relative clip position
        relativeClipPosition = relativePosition;

        // Redraw
        invalidate();
    }

    // endregion

    // region Ripple-related

    /**
     * Updates hotspot for drawable
     *
     * This function is called by the base View class when the user taps on a location. The base View class handles
     * this automatically for the background drawable. The primary advantage of this function and a hotspot in
     * general is for the ripple effect to show where the ripple show originate from.
     *
     * Updates the hotspot for the ripple drawable manually since the base View class does not know about the ripple
     * drawable.
     *
     * @param x X coordinate of the new hotspot
     * @param y Y coordinate of the new hotspot
     */
    @SuppressLint("NewApi")
    @Override
    public void drawableHotspotChanged(final float x, final float y)
    {
        super.drawableHotspotChanged(x, y);

        // Update the hotspot for the ripple drawable
        if (rippleDrawableLollipop != null)
            rippleDrawableLollipop.setHotspot(x, y);
    }

    /**
     * Updates state for drawable
     *
     * This function is called by the base View class when the state of the View changes. The state of the View is
     * how state-lists and the ripple drawable work, by monitoring the state for a change in the pressed state and
     * having different drawable states or actions when the state changes.
     *
     * The base View class handles updating the state for the background drawable but the ripple drawable state is
     * updated manually here since the base View class does not know about the ripple drawable.
     */
    @Override
    protected void drawableStateChanged()
    {
        super.drawableStateChanged();

        // Update the state for the ripple drawable
        if (rippleDrawableLollipop != null)
            rippleDrawableLollipop.setState(getDrawableState());

        // Update the state for the ripple drawable
        if (rippleDrawable != null)
            rippleDrawable.setState(getDrawableState());
    }

    /**
     * Validate Drawables and whether or not they are allowed to animate
     *
     * By returning true for a Drawable, this will allow animations to be scheduled for that Drawable, which is
     * relevant for the ripple drawables in this class.
     *
     * @param who Drawable to verify. Return true if this class is displaying the drawable.
     * @return Returns true if the drawable is being displayed in this view, else false and it is not allowed to
     * animate
     */
    @Override
    protected boolean verifyDrawable(@NonNull final Drawable who)
    {
        // Very obscure and difficult to find but it is noted in the source code docstring for this function
        // Return true if the drawable is the ripple drawable (backport or regular)
        // Normally the super class handles this automatically for the background drawable but the ripple drawable is
        // not the background in this instance
        return who == rippleDrawableLollipop || who == rippleDrawable || super.verifyDrawable(who);
    }

    // endregion

    // region Getters & Setters

    /**
     * Set the background radius of the corners of the parent button group in order to round edges
     *
     * If isLeftButton() is true, this radius will be used to clip the bottom-left and top-left corners.
     * If isRightButton() is true, this radius will be used to clip the bottom-right and top-right corners.
     * If both are true, then all corners will be rounded with the radius.
     * If none are set, no corners are rounded and this parameter is not used.
     *
     * Note: You must manually call setupBackgroundClipPath after all changes to background radius, left button,
     * right button, rounded & width/height are completed.
     *
     * @param backgroundRadius radius of corners of parent button group in pixels
     */
    void setBackgroundRadius(int backgroundRadius)
    {
        this.backgroundRadius = backgroundRadius;
    }

    /**
     * Returns whether this button is the left-most button in the group
     *
     * This is determined based on whether the rightButton variable is null
     */
    public boolean isLeftButton()
    {
        return leftButton == null;
    }

    /**
     * Returns whether this button is the right-most button in the group
     *
     * This is determined based on whether the rightButton variable is null
     */
    public boolean isRightButton()
    {
        return rightButton == null;
    }

    /**
     * Sets the button directly to the left of this button. Set to null to indicate that this is the left-most button
     * in the group
     *
     * Note: You must manually call setupBackgroundClipPath after all changes to background radius,
     * leftButton, rightButton, rounded & width/height are completed.
     */
    @SuppressWarnings("SameParameterValue")
    void setLeftButton(SegmentedButton leftButton)
    {
        this.leftButton = leftButton;
    }

    /**
     * Sets the button directly to the right of this button. Set to null to indicate that this is the right-most button
     * in the group
     *
     * Note: You must manually call setupBackgroundClipPath after all changes to background radius, leftButton,
     * rightButton, rounded & width/height are completed.
     */
    @SuppressWarnings("SameParameterValue")
    void setRightButton(SegmentedButton rightButton)
    {
        this.rightButton = rightButton;
    }

    /**
     * Returns whether this button is rounded
     */
    public boolean isRounded()
    {
        return rounded;
    }

    /**
     * Sets whether button is rounded regardless of its position in group
     *
     * Note: You must manually call setupBackgroundClipPath after all changes to background radius, leftButton,
     * rightButton, rounded & width/height are completed.
     */
    public void setRounded(boolean rounded)
    {
        this.rounded = rounded;
    }

    /**
     * Set the radius of the selected button
     *
     * This will round out the selected button or the part shown in this button during animation based on this radius
     * value.
     *
     * @param selectedButtonRadius radius of corners for selected button in pixels
     */
    void setSelectedButtonRadius(int selectedButtonRadius)
    {
        this.selectedButtonRadius = selectedButtonRadius;
    }

    /**
     * Setup the background clip path in order to round the edges of this button
     *
     * If isLeftButton() is true, this radius will be used to clip the bottom-left and top-left corners.
     * If isRightButton() is true, this radius will be used to clip the bottom-right and top-right corners.
     * If both are true or isRounded() is true, then all corners will be rounded with the radius.
     * If none are set, no corners are rounded and this parameter is not used.
     *
     * This function should be called when the size of the button changes, if the background radius changes and/or if
     * the isLeftButton() or isRightButton() boolean values change.
     *
     * Note that this function internally calls setupBackgroundBitmaps() because a change in the clip path will
     * require updating the bitmaps
     */
    void setupBackgroundClipPath()
    {
        // If there is no background radius then skip
        if (backgroundRadius == 0)
        {
            backgroundClipPath = null;

            // Update background bitmaps
            setupBackgroundBitmaps();
            return;
        }

        // Set rectangle to take up entire view, used to create clip path
        rectF.set(0, 0, getWidth(), getHeight());

        // Background radius, shorthand variable to make code cleaner
        final float br = backgroundRadius;

        if (isRounded() || (isLeftButton() && isRightButton()))
        {
            // Add radius on all sides, left & right
            backgroundClipPath = new Path();
            backgroundClipPath.addRoundRect(rectF,
                new float[] {br, br, br, br, br, br, br, br}, Direction.CW);
        }
        else if (isLeftButton())
        {
            // Add radius on left side only
            backgroundClipPath = new Path();
            backgroundClipPath.addRoundRect(rectF, new float[] {br, br, 0, 0, 0, 0, br, br}, Direction.CW);
        }
        else if (isRightButton())
        {
            // Add radius on right side only
            backgroundClipPath = new Path();
            backgroundClipPath.addRoundRect(rectF, new float[] {0, 0, br, br, br, br, 0, 0}, Direction.CW);
        }
        else
        {
            backgroundClipPath = null;
        }

        // Canvas.clipPath, used in onDraw for drawing the background clip path (rounding the edges for left-most and
        // right-most buttons) is not supported with hardware acceleration until API 18
        // Thus, switch to software acceleration if the background clip path is not null (meaning the edges are
        // rounded) and the current version is less than 18
        if (backgroundClipPath != null && Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2)
            setLayerType(LAYER_TYPE_SOFTWARE, null);

        // Update background bitmaps
        setupBackgroundBitmaps();
    }

    /**
     * Setup the background paint objects so that the background & selected background drawable can be rendered with
     * rounded corners using a bitmap shader
     *
     * This function is called by setupBackgroundClipPath since the background clip determines whether the button has
     * rounded corners. In addition, this function should be called when the drawable or selected drawable changes,
     * the selected button radius changes, or the size of either drawable changes.
     */
    void setupBackgroundBitmaps()
    {
        Bitmap bitmap;

        // Setup background paint object to render background using a bitmap shader approach under three conditions:
        //      1. Background has rounded corners
        //      2. There is a background drawable
        //      3. Able to successfully create bitmap from drawable
        if (backgroundClipPath != null && backgroundDrawable != null
            && (bitmap = getBitmapFromDrawable(backgroundDrawable)) != null)
        {
            final BitmapShader backgroundBitmapShader = new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP);

            backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            backgroundPaint.setShader(backgroundBitmapShader);
        }
        else
            backgroundPaint = null;

        // Setup selected background paint object to render background using a bitmap shader approach under three
        // conditions:
        //      1. Background has rounded corners OR selected button has rounded corners
        //      2. There is a background drawable
        //      3. Able to successfully create bitmap from drawable
        if ((backgroundClipPath != null || selectedButtonRadius > 0) && selectedBackgroundDrawable != null
            && (bitmap = getBitmapFromDrawable(selectedBackgroundDrawable)) != null)
        {
            final BitmapShader selectedBackgroundBitmapShader = new BitmapShader(bitmap, TileMode.CLAMP,
                TileMode.CLAMP);

            selectedBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            selectedBackgroundPaint.setShader(selectedBackgroundBitmapShader);
        }
        else
            selectedBackgroundPaint = null;
    }

    /**
     * Setup the selected button clip path to round the corners of the selected button
     *
     * This function should be called if the selected button radius is changed
     */
    void setupSelectedButtonClipPath()
    {
        // Setup selected button radii
        // Object allocated here rather than in onDraw to increase performance
        selectedButtonRadii = new float[] {
            selectedButtonRadius, selectedButtonRadius, selectedButtonRadius,
            selectedButtonRadius, selectedButtonRadius, selectedButtonRadius, selectedButtonRadius,
            selectedButtonRadius
        };

        if (selectedButtonRadius > 0)
        {
            // Canvas.clipPath, used in onDraw for drawing the selected button clip path is not supported with
            // hardware acceleration until API 18. Thus, this switches to software acceleration if current Android
            // API version is less than 18.
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2)
            {
                setLayerType(LAYER_TYPE_SOFTWARE, null);
            }
        }

        // Update background bitmaps
        setupBackgroundBitmaps();

        invalidate();
    }

    /**
     * Set the border for the selected button
     *
     * @param width     Width of the border in pixels (default value is 0px or no border)
     * @param color     Color of the border (default color is black)
     * @param dashWidth Width of the dash for border, in pixels. Value of 0px means solid line (default is 0px)
     * @param dashGap   Width of the gap for border, in pixels.
     */
    void setSelectedButtonBorder(int width, @ColorInt int color, int dashWidth, int dashGap)
    {
        if (width > 0)
        {
            // Allocate Paint object for drawing border here
            // Used in onDraw to draw the border around the selected button
            selectedButtonBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            selectedButtonBorderPaint.setStyle(Paint.Style.STROKE);
            selectedButtonBorderPaint.setStrokeWidth(width);
            selectedButtonBorderPaint.setColor(color);

            if (dashWidth > 0.0f)
            {
                selectedButtonBorderPaint.setPathEffect(new DashPathEffect(new float[] {dashWidth, dashGap}, 0));
            }
        }
        else
        {
            // If the width is 0, then disable drawing border
            selectedButtonBorderPaint = null;
        }

        invalidate();
    }

    @Override
    public void setVisibility(final int visibility)
    {
        super.setVisibility(visibility);

        // Notify the parent button group of change
        if (onVisibilityChangedListener != null)
        {
            onVisibilityChangedListener.onVisibilityChanged(this, visibility);
        }
    }

    /**
     * Sets the background of the button to be the drawable background if it does not have a background already and
     * the drawable is not null
     *
     * If either one of those conditions are met, then the background is not changed
     *
     * This is a package-private function used by SegmentedButtonGroup to pass its 'global' background down to the
     * buttons
     *
     * @param drawable Drawable to set as the background
     */
    void setDefaultBackground(@Nullable Drawable drawable)
    {
        if (backgroundDrawable == null && drawable != null)
        {
            // Make sure to clone the drawable so that we can set the bounds on it
            backgroundDrawable = drawable.getConstantState().newDrawable();
        }
    }

    /**
     * Sets the selected background of the button to be the drawable background if it does not have a background
     * already and the drawable is not null
     *
     * If either one of those conditions are met, then the background is not changed
     *
     * This is a package-private function used by SegmentedButtonGroup to pass its 'global' background down to the
     * buttons
     *
     * @param drawable Drawable to set as the background
     */
    void setDefaultSelectedBackground(@Nullable Drawable drawable)
    {
        if (selectedBackgroundDrawable == null && drawable != null)
        {
            // Make sure to clone the drawable so that we can set the bounds on it
            selectedBackgroundDrawable = drawable.getConstantState().newDrawable();
        }
    }

    /**
     * Returns the background drawable that is shown when the button is not selected
     *
     * In the case a solid color background is used, this will be a ColorDrawable
     *
     * @return the current background drawable when the button is not selected
     */
    public Drawable getBackground()
    {
        return backgroundDrawable;
    }

    /**
     * Set the background displayed when not selected to a given drawable
     *
     * @param drawable drawable to set the background to
     */
    @Override
    public void setBackground(final Drawable drawable)
    {
        backgroundDrawable = drawable;
        backgroundDrawable.setBounds(0, 0, getWidth(), getHeight());

        // Setup the background bitmaps again since background drawable has changed
        setupBackgroundBitmaps();

        invalidate();
    }

    /**
     * Set the background displayed when not selected to a given color
     *
     * This will create a ColorDrawable or modify the current background if it is a ColorDrawable
     *
     * @param color color to set the background to
     */
    public void setBackground(@ColorInt int color)
    {
        if (backgroundDrawable instanceof ColorDrawable)
        {
            // If the current drawable is a ColorDrawable, just change the color
            ((ColorDrawable)backgroundDrawable.mutate()).setColor(color);
        }
        else
        {
            backgroundDrawable = new ColorDrawable(color);
            backgroundDrawable.setBounds(0, 0, getWidth(), getHeight());
        }

        // Setup the background bitmaps again since background drawable has changed
        setupBackgroundBitmaps();

        invalidate();
    }

    /**
     * Convenience function for setting the background color when not selected
     *
     * This function already exists in the base View class so it is overridden to prevent confusion as to why
     * setBackground works but not setBackgroundColor.
     *
     * @param color color to set the background to
     */
    @Override
    public void setBackgroundColor(@ColorInt int color)
    {
        setBackground(color);
    }

    /**
     * Returns the background drawable that is shown when the button is selected
     *
     * In the case a solid color background is used, this will be a ColorDrawable
     *
     * @return the current background drawable when the button is selected
     */
    public Drawable getSelectedBackground()
    {
        return selectedBackgroundDrawable;
    }

    /**
     * Set the background displayed when selected to a given drawable
     *
     * @param drawable drawable to set the background to
     */
    public void setSelectedBackground(final Drawable drawable)
    {
        selectedBackgroundDrawable = drawable;
        selectedBackgroundDrawable.setBounds(0, 0, getWidth(), getHeight());

        // Setup the background bitmaps again since background drawable has changed
        setupBackgroundBitmaps();

        invalidate();
    }

    /**
     * Set the background displayed when selected to a given color
     *
     * This will create a ColorDrawable or modify the current background if it is a ColorDrawable
     *
     * @param color color to set the background to
     */
    public void setSelectedBackground(@ColorInt int color)
    {
        if (selectedBackgroundDrawable instanceof ColorDrawable)
        {
            // If the current drawable is a ColorDrawable, just change the color
            ((ColorDrawable)selectedBackgroundDrawable.mutate()).setColor(color);
        }
        else
        {
            selectedBackgroundDrawable = new ColorDrawable(color);
            selectedBackgroundDrawable.setBounds(0, 0, getWidth(), getHeight());
        }

        // Setup the background bitmaps again since background drawable has changed
        setupBackgroundBitmaps();

        invalidate();
    }

    /**
     * Convenience function for setting the background color when selected
     *
     * @param color color to set the background to
     */
    public void setSelectedBackgroundColor(@ColorInt int color)
    {
        setSelectedBackground(color);
    }

    /**
     * Returns the ripple color used for displaying the ripple effect on button press
     *
     * The ripple color is a tint color applied on top of the button when it is pressed
     */
    public int getRippleColor()
    {
        return rippleColor;
    }

    /**
     * Set ripple effect to be either enabled or disabled on button press
     *
     * If enabled, then the ripple color used will be the last ripple color set for this button or the default value
     * of gray
     *
     * Note: This function is package-private because enabling or disabling the ripple effect on a per-button basis
     * sounds like a terrible idea
     *
     * @param enabled whether or not to enable the ripple effect for this button
     */
    void setRipple(boolean enabled)
    {
        if (enabled)
        {
            // Recreate the ripple drawable and setup with the ripple color
            setRipple(rippleColor);
        }
        else
        {
            // Set both ripple drawables to null so that we do not draw the ripple
            rippleDrawableLollipop = null;
            rippleDrawable = null;
        }
    }

    /**
     * Set ripple color used for ripple effect on button press
     *
     * @param color color to set for the ripple effect for this button
     */
    public void setRipple(@ColorInt int color)
    {
        rippleColor = color;

        if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP)
        {
            rippleDrawableLollipop = new RippleDrawable(ColorStateList.valueOf(rippleColor), null, null);
            // setCallback on Drawable allows animations to be scheduled and the drawable to invalidate the view on
            // animation
            rippleDrawableLollipop.setCallback(this);
            rippleDrawableLollipop.setBounds(0, 0, getWidth(), getHeight());

            // Disable/nullify the pre-lollipop RippleDrawable backport
            rippleDrawable = null;
        }
        else
        {
            rippleDrawable = new codetail.graphics.drawables.RippleDrawable(ColorStateList.valueOf(rippleColor), null,
                null);
            // setCallback on Drawable allows animations to be scheduled and the drawable to invalidate the view on
            // animation
            rippleDrawable.setCallback(this);
            rippleDrawable.setBounds(0, 0, getWidth(), getHeight());

            setOnTouchListener(new DrawableHotspotTouch(rippleDrawable));

            // Disable/nullify the lollipop RippleDrawable
            rippleDrawableLollipop = null;
        }

        invalidate();
    }

    /**
     * Returns the drawable for the button or null if no drawable is set
     */
    public Drawable getDrawable()
    {
        return drawable;
    }

    /**
     * Set the drawable for the button
     *
     * If drawable is null, then the drawable is removed from the button
     *
     * @param drawable Drawable to set for the button
     */
    public void setDrawable(final @Nullable Drawable drawable)
    {
        this.drawable = drawable;

        // Request a layout, drawable may have different size and things need to be rearranged
        requestLayout();

        // Calculate new positions and bounds for text & drawable
        // This may be redundant if the case that onSizeChanged gets called but there are cases where the size doesnt
        // change but the positions still need to be recalculated
        updateSize();
    }

    /**
     * Returns the drawable padding
     *
     * The drawable padding is the space between the drawable and text, in pixels. If the drawable or the text is not
     * present, then the padding is ignored.
     */
    public int getDrawablePadding()
    {
        return drawablePadding;
    }

    /**
     * Set the drawable padding
     *
     * The drawable padding is the space between the drawable and text, in pixels. If the drawable or the text is not
     * present, then the padding is ignored.
     *
     * @param padding padding in pixels to set for the drawable
     */
    public void setDrawablePadding(final int padding)
    {
        drawablePadding = padding;

        requestLayout();

        // Calculate new positions and bounds for text & drawable
        // This may be redundant if the case that onSizeChanged gets called but there are cases where the size doesnt
        // change but the positions still need to be recalculated
        updateSize();
    }

    /**
     * Whether or not there is a tint color applied to the drawable when the button is not selected
     */
    public boolean hasDrawableTint()
    {
        return hasDrawableTint;
    }

    /**
     * Tint color applied to the drawable when the button is not selected
     *
     * If hasDrawableTint is false, then this value will be undefined
     */
    public int getDrawableTint()
    {
        return drawableTint;
    }

    /**
     * Set the drawable tint color to the specified color
     *
     * This drawable tint color will be the color applied to the drawable when the button is not selected
     *
     * @param tint color for the drawable tint
     */
    public void setDrawableTint(final @ColorInt int tint)
    {
        hasDrawableTint = true;
        drawableTint = tint;

        // Create color filter for the tint color
        drawableColorFilter = new PorterDuffColorFilter(tint, PorterDuff.Mode.SRC_IN);

        invalidate();
    }

    /**
     * Remove drawable tint color so that the normal drawable is shown when the button is not selected
     */
    public void removeDrawableTint()
    {
        hasDrawableTint = false;
        drawableColorFilter = null;

        invalidate();
    }

    /**
     * Whether or not there is a tint color applied to the drawable when the button is selected
     */
    public boolean hasSelectedDrawableTint()
    {
        return hasSelectedDrawableTint;
    }

    /**
     * Tint color applied to the drawable when the button is selected
     *
     * If hasSelectedDrawableTint is false, then this value will be undefined
     */
    public int getSelectedDrawableTint()
    {
        return selectedDrawableTint;
    }

    /**
     * Set the drawable tint color to the specified color
     *
     * This drawable tint color will be the color applied to the drawable when the button is selected
     *
     * @param tint color for the drawable tint
     */
    public void setSelectedDrawableTint(final @ColorInt int tint)
    {
        hasSelectedDrawableTint = true;
        selectedDrawableTint = tint;

        // Create color filter for the tint color
        selectedDrawableColorFilter = new PorterDuffColorFilter(tint, PorterDuff.Mode.SRC_IN);

        invalidate();
    }

    /**
     * Remove drawable tint color so that the normal drawable is shown when the button is selected
     */
    public void removeSelectedDrawableTint()
    {
        hasSelectedDrawableTint = false;
        selectedDrawableColorFilter = null;

        invalidate();
    }

    /**
     * Whether or not the drawable has a width that was explicitly given
     */
    public boolean hasDrawableWidth()
    {
        return hasDrawableWidth;
    }

    /**
     * Returns the drawable width in pixels
     *
     * If hasDrawableWidth is false, then this value will be undefined
     */
    public int getDrawableWidth()
    {
        return drawableWidth;
    }

    /**
     * Set the drawable width in pixels
     *
     * If the width is -1, then the drawable width will be removed and the intrinsic drawable width will be used
     * instead
     *
     * @param width size in pixels of the width of the drawable
     */
    public void setDrawableWidth(final int width)
    {
        hasDrawableWidth = (width != -1);
        drawableWidth = width;

        // Request relayout because the drawable width is different now
        requestLayout();

        // Calculate new positions and bounds for text & drawable
        // This may be redundant if the case that onSizeChanged gets called but there are cases where the size doesnt
        // change but the positions still need to be recalculated
        updateSize();
    }

    /**
     * Whether or not the drawable has a height that was explicitly given
     */
    public boolean hasDrawableHeight()
    {
        return hasDrawableHeight;
    }

    /**
     * Returns the drawable height in pixels
     *
     * If hasDrawableHeight is false, then this value will be undefined
     */
    public int getDrawableHeight()
    {
        return drawableHeight;
    }

    /**
     * Set the drawable height in pixels
     *
     * If the height is -1, then the drawable height will be removed and the intrinsic drawable height will be used
     * instead
     *
     * @param height size in pixels of the height of the drawable
     */
    public void setDrawableHeight(final int height)
    {
        hasDrawableHeight = (height != -1);
        drawableHeight = height;

        // Request relayout because the drawable width is different now
        requestLayout();

        // Calculate new positions and bounds for text & drawable
        // This may be redundant if the case that onSizeChanged gets called but there are cases where the size doesnt
        // change but the positions still need to be recalculated
        updateSize();
    }

    /**
     * Returns the gravity for the drawable
     *
     * The drawable can be placed to the left, top, right, or bottom of the text via this parameter
     */
    public int getDrawableGravity()
    {
        return drawableGravity;
    }

    /**
     * Set the drawable gravity
     *
     * Can be one of the following values:
     * - Gravity.LEFT
     * - Gravity.TOP
     * - Gravity.RIGHT
     * - Gravity.BOTTOM
     *
     * The drawable gravity indicates the location of the drawable in relation to the text. If no text is being
     * displayed, this property will be ignored.
     *
     * @param gravity new drawable gravity
     */
    public void setDrawableGravity(final @GravityOptions int gravity)
    {
        drawableGravity = gravity;

        // Request relayout because the drawable width is different now
        requestLayout();

        // Calculate new positions and bounds for text & drawable
        // This may be redundant if the case that onSizeChanged gets called but there are cases where the size doesnt
        // change but the positions still need to be recalculated
        updateSize();
    }

    /**
     * Return the text currently being displayed
     *
     * If no text is being shown, this will be either null or an empty string
     */
    public String getText()
    {
        return text;
    }

    /**
     * Set the text to a new string
     *
     * If the string is null or an empty string, then the text will be hidden
     *
     * @param text new string to set text to in the button
     */
    public void setText(final @Nullable String text)
    {
        this.hasText = (text != null && !text.isEmpty());
        this.text = text;

        initText();
        requestLayout();

        // Calculate new positions and bounds for text & drawable
        // This may be redundant in the case that onSizeChanged gets called but there are cases where the size doesnt
        // change but the positions still need to be recalculated
        updateSize();
    }

    /**
     * Returns the text color when the button is not selected
     */
    public int getTextColor()
    {
        return textColor;
    }

    /**
     * Set the text color when the button is unselected
     *
     * @param color text color
     */
    public void setTextColor(final @ColorInt int color)
    {
        textColor = color;

        invalidate();
    }

    /**
     * Whether or not there is a text color when this button is selected
     *
     * If this is false, then the text color will be the same as when the button is unselected
     */
    public boolean hasSelectedTextColor()
    {
        return hasSelectedTextColor;
    }

    /**
     * Returns the text color when the button is selected
     *
     * If hasSelectedTextColor is false, then this returned value is undefined
     */
    public int getSelectedTextColor()
    {
        return selectedTextColor;
    }

    /**
     * Set the text color when the button is selected
     *
     * @param color text color
     */
    public void setSelectedTextColor(final @ColorInt int color)
    {
        hasSelectedTextColor = true;
        selectedTextColor = color;

        invalidate();
    }

    /**
     * Remove the text color when the button is selected
     *
     * The text color of the button when selected will be the normal text color of the button
     */
    public void removeSelectedTextColor()
    {
        hasSelectedTextColor = false;

        invalidate();
    }

    /**
     * Return the size of the text in pixels
     */
    public float getTextSize()
    {
        return textSize;
    }

    /**
     * Set the size of the text in pixels
     *
     * @param size new size in pixels of the text
     */
    public void setTextSize(final float size)
    {
        textSize = size;
        if (!hasText)
            return;

        textPaint.setTextSize(size);

        initText();
        requestLayout();

        // Calculate new positions and bounds for text & drawable
        // This may be redundant if the case that onSizeChanged gets called but there are cases where the size doesnt
        // change but the positions still need to be recalculated
        updateSize();
    }

    /**
     * Return the current typeface used for drawing text
     */
    public Typeface getTextTypeface()
    {
        return textTypeface;
    }

    /**
     * Set a new typeface to use for drawing text
     *
     * @param typeface new typeface for text
     */
    public void setTextTypeface(final Typeface typeface)
    {
        textTypeface = typeface;
        refreshTypeface();
    }

    /**
     * Return the current selected typeface used for drawing text
     */
    public Typeface getSelectedTextTypeface()
    {
        return selectedTextTypeface;
    }

    /**
     * Set a new typeface to use for drawing text when the button is selected
     *
     * @param typeface new typeface for selected text
     */
    public void setSelectedTextTypeface(final Typeface typeface)
    {
        selectedTextTypeface = typeface;
        refreshTypeface();
    }

    private void refreshTypeface() {
        initText();
        requestLayout();

        // Calculate new positions and bounds for text & drawable
        // This may be redundant if the case that onSizeChanged gets called but there are cases where the size doesn't
        // change but the positions still need to be recalculated
        updateSize();
    }

    /**
     * This sets a listener that will be called when the visibility of the current button is changed.
     *
     * This listener is meant for internal use by SegmentedButtonGroup ONLY
     *
     * DO NOT USE
     */
    void _setOnVisibilityChangedListener(OnVisibilityChangedListener listener)
    {
        this.onVisibilityChangedListener = listener;
    }

    // endregion

    // region Helper functions

    /**
     * Create a bitmap from a specified vector drawable
     *
     * @param vectorDrawable vector drawable to convert to a bitmap
     */
    public static Bitmap getBitmapFromVectorDrawable(Drawable vectorDrawable)
    {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
        {
            vectorDrawable = (DrawableCompat.wrap(vectorDrawable)).mutate();
        }

        Bitmap bitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(),
            vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        vectorDrawable.draw(canvas);

        return bitmap;
    }

    /**
     * Create a bitmap from a specified drawable
     *
     * Note that if the size of the drawable changes, then the bitmap will need to be changed.
     *
     * @param drawable drawable to convert to a bitmap
     */
    private static Bitmap getBitmapFromDrawable(@Nullable Drawable drawable)
    {
        if (drawable == null)
            return null;

        // Return bitmap if drawable is BitmapDrawable
        if (drawable instanceof BitmapDrawable)
            return ((BitmapDrawable)drawable).getBitmap();

        try
        {
            Bitmap bitmap;

            if (drawable instanceof ColorDrawable)
            {
                // Create a bitmap of fixed size for ColorDrawable since it inherently has no size
                // Ideally, this size can be small because the bitmap can be stretched to fit any width/height
                // without loss of quality
                bitmap = Bitmap.createBitmap(COLORDRAWABLE_SIZE, COLORDRAWABLE_SIZE, BITMAP_CONFIG);
            }
            else if (drawable instanceof GradientDrawable)
            {
                // GradientDrawable ALSO doesn't have a inherent size
                // However, the size of the bitmap used to represent the GradientDrawable should be the size of the
                // bounds.
                // A small fixed size here would result in a pixelated bitmap being drawn
                // Return null if bounds are 0, this occurs if function is called before button is laid out
                final Rect bounds = drawable.getBounds();

                if (bounds.width() > 0 && bounds.height() > 0)
                    bitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), BITMAP_CONFIG);
                else
                    return null;
            }
            else
            {
                // Otherwise, create bitmap based on intrinsic size of the drawable
                bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),
                    BITMAP_CONFIG);
            }

            // Create canvas using bitmap
            Canvas canvas = new Canvas(bitmap);

            // Draw the drawable on the canvas
            // Save the bounds before hand and reset the drawable bounds afterwards
            final Rect bounds = new Rect(drawable.getBounds());
            drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
            drawable.draw(canvas);
            drawable.setBounds(bounds);

            return bitmap;
        }
        catch (Exception e)
        {
            // There was an unexpected problem, print out the stack track
            e.printStackTrace();
            return null;
        }
    }

    // endregion

    // region Listeners

    /**
     * Interface definition for a callback that will be invoked when the visibility of the button changes
     *
     * This is an internal listener meant to be used ONLY by the SegmentedButtonGroup
     */
    public interface OnVisibilityChangedListener
    {
        void onVisibilityChanged(SegmentedButton button, int visibility);
    }

    // endregion
}