4

I need to implement a button in my app like this

button

I used a SwitchCompat button but the closest I arrived was to this point,

a busy cat

having two main problems:

1 - The width of the button does not adjust correctly when screen sizes change (drawable gets cut off, become too small etc), it is important that the width occupies the parent view correctly ( a small linear layout enclosing it)

2 - I was not able to understand how I could get the letters in the Switch Track

Is it possible to achieve this result with a switch button? How? Should I use another view instead of the switch button? Which one?

I stumbled upon this project but it seems a bit outdated

https://github.com/pellucide/Android-Switch-Demo-pre-4.0/tree/master/ Screenshot

Petermonteer
  • 296
  • 4
  • 17
  • 1
    I don't think you can easily achieve this with default `Switch` component. You probably should go with the custom view approach – MatPag Aug 28 '18 at 19:51
  • @MatPag how should I go about it? I have no idea – Petermonteer Aug 28 '18 at 20:02
  • Creating custom views it's not easy. Requires a bit of skill with the framework at least. You can try to start with something like this: https://github.com/GwonHyeok/StickySwitch as base view and then apply changes to it based on your needs – MatPag Aug 28 '18 at 20:06

2 Answers2

12

For example:

class SwitchCompatEx : SwitchCompat {

    companion object {

        val TRACK_COLOR = 0xFFFFFFFF.toInt()
        val TRACK_STROKE_WIDTH = 2f.dp2Px.toInt()
        val TRACK_STROKE_COLOR = 0xFF00A1FF.toInt()
        val TRACK_LABEL_COLOR = 0xFF00A1FF.toInt()
        val TRACK_LABEL_SIZE = 14f.sp2Px

        val THUMB_COLOR = 0xFF00A1FF.toInt()
        val THUMB_LABEL_COLOR = 0xFFFFFFFF.toInt()
        val THUMB_LABEL_SIZE = 14f.sp2Px

        fun drawLabel(canvas: Canvas,
                      bounds: Rect,
                      paint: Paint,
                      text: CharSequence?) {
            text ?: return

            val tb = RectF();
            tb.right = paint.measureText(text, 0, text.length)
            tb.bottom = paint.descent() - paint.ascent()
            tb.left += bounds.centerX() - tb.centerX()
            tb.top += bounds.centerY() - tb.centerY() - paint.ascent()

            canvas.drawText(text.toString(), tb.left, tb.top, paint)
        }

        private inline val Float.sp2Px
            get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
                    this,
                    Resources.getSystem().displayMetrics)

        private inline val Float.dp2Px
            get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                    this,
                    Resources.getSystem().displayMetrics)
    }

    private val trackLabelPaint = Paint().apply {
        isAntiAlias = true
        textSize = TRACK_LABEL_SIZE
        color = TRACK_LABEL_COLOR
    }

    private val thumbLabelPaint = Paint().apply {
        isAntiAlias = true
        textSize = THUMB_LABEL_SIZE
        color = THUMB_LABEL_COLOR
    }

    private val thumbLabel
        get () = if (isChecked) textOn else textOff

    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    init {
        background = null
        trackDrawable = TrackDrawable()
        thumbDrawable = ThumbDrawable()
    }

    override fun onSizeChanged(w: Int,
                               h: Int,
                               oldw: Int,
                               oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)

        (trackDrawable as GradientDrawable).setSize(w, h)
        (thumbDrawable as GradientDrawable).setSize(w / 2, h)
    }

    inner class TrackDrawable : GradientDrawable() {

        private val textOffBounds = Rect()
        private val textOnBounds = Rect()

        init {
            setColor(TRACK_COLOR)
            setStroke(TRACK_STROKE_WIDTH, TRACK_STROKE_COLOR)
        }

        override fun onBoundsChange(r: Rect) {
            super.onBoundsChange(r)

            cornerRadius = r.height() / 2f

            textOffBounds.set(r)
            textOffBounds.right /= 2

            textOnBounds.set(textOffBounds)
            textOnBounds.offset(textOffBounds.right, 0)
        }

        override fun draw(canvas: Canvas) {
            super.draw(canvas)

            drawLabel(canvas, textOffBounds, trackLabelPaint, textOff)
            drawLabel(canvas, textOnBounds, trackLabelPaint, textOn)
        }
    }

    inner class ThumbDrawable : GradientDrawable() {

        private val thumbLabelBounds = Rect()

        init {
            setColor(THUMB_COLOR)
        }

        override fun onBoundsChange(r: Rect) {
            super.onBoundsChange(r)

            cornerRadius = r.height() / 2f

            thumbLabelBounds.set(r)
        }

        override fun draw(canvas: Canvas) {
            super.draw(canvas)

            drawLabel(canvas, thumbLabelBounds, thumbLabelPaint, thumbLabel)
        }
    }
}

...

<demo.sodemos.SwitchCompatEx
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:minHeight="40dp"
    android:textOff="M"
    android:textOn="F"
    app:switchMinWidth="100dp" />

...

enter image description here

Also check out this Custom view components Tutorial.

Hope this helps

Akaki Kapanadze
  • 2,552
  • 2
  • 11
  • 21
  • Thanks so much for your detailed answer! Will try it and give some feedback! :) – Petermonteer Aug 30 '18 at 08:38
  • I tried and it works perfectly in the emulator with api 17! The problem is, at least with api 24 and 27, it doesn't display correctly, the thumb and the track are not displayed at all ( the only thing that is displayed is the set up texton and textoff). Any suggestions? Thanks :) – Petermonteer Sep 01 '18 at 16:43
  • Well fun and weird fact, the custom view class displays correctly in all api versions AFTER I press a random edit text view in the same activity and the soft keyboard opens´ – Petermonteer Sep 02 '18 at 18:58
  • 1
    Hmm interesting, sounds like there is problem with drawable bounds. I'll check it later. Thanks – Akaki Kapanadze Sep 03 '18 at 05:35
  • 1
    yeah it seems like that's the problem, I tried to debug it but couldnt figure it out... if you set a new height programatically (after the views are all created) it also works. I tried to change the height in the onStart call but it didn't change anything... Thanks for your help once again, and if you figure out what the problem is I'd love to see the explanation! – Petermonteer Sep 03 '18 at 09:44
  • Another text is there. Very small. in the middle of the each thumb(when it's selected): https://ibb.co/drGF0jK – Dr.jacky Feb 11 '21 at 14:29
  • This works well for LTR layouts, but for RTL layouts, you have to reverse `textOn` & `textOff` within `draw()` method – Zain Feb 16 '22 at 00:01
  • For fixing layout/text error add this ad the bottom of SwitchCompatEx (Thanks to Muhammad fot the fix) : override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) invalidate() requestLayout() } – DkP Mar 08 '23 at 16:13
2

After messing around I fixed rendering issue by adding below two lines at the end of onDraw function for each drawable

    invalidate()
    requestLayout()