Search code examples
androidkotlinuser-interfaceview

What is the best way to create a view in android that looks like a progress bar but the progress can be scatered depending on where you want it to be


I have an api that returns this type of data :

"timeRanges": [
                {
                    "start": 25201,
                    "end": 32399
                },
                {
                    "start": 68401,
                    "end": 82799
                },
                {
                    "start": 111601,
                    "end": 118799
                },

this data represent time intervals in seconds and i need to represent it in this way:

enter image description here

My question is: how can i create that view and make each green line clickable

i tried to use RangeSlider but it doesn't support adding multiple ranges

and i tried to use horizontal stacked bar graph but the results wasn't like what i need


Solution

  • i wrote a custom view that draws the bar and the ranges inside it

    package com.example.material3app
    
    import android.content.Context
    import android.graphics.*
    import android.graphics.drawable.Drawable
    import android.util.AttributeSet
    import android.view.MotionEvent
    import android.view.View
    import androidx.core.content.ContextCompat
    
    
    /**
     * Created by Khmaies Hassen on 15,March,2023
     */
    class BarView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
        private val paint = Paint()
        private var barHeight = 20
        private var valueFrom = 0
        private var valueTo = 100
        private val ranges = mutableListOf<Range>()
        private var backgroundColor = Color.WHITE
        private var cornerRadius = 10f
        private var selectedRange: Range? = null
        private var onRangeSelectedListener: OnRangeSelectedListener? = null
        private var trackDrawable : Drawable? = null
    
        init {
            paint.style = Paint.Style.FILL
    
            if (trackDrawable == null) {
                trackDrawable = ContextCompat.getDrawable(
                    getContext(),
                    R.drawable.multislider_track_material
                )
            }
        }
    
        fun setBarHeight(height: Int) {
            barHeight = height
        }
    
        fun getRanges(): List<Range> {
            return ranges
        }
        fun setValueFrom(value: Int) {
            valueFrom = value
        }
    
        fun setValueTo(value: Int) {
            valueTo = value
        }
    
        fun addRange(start: Int, end: Int, color: Int) {
            ranges.add(Range(start, end, color))
        }
    
        override fun setBackgroundColor(color: Int) {
            backgroundColor = color
        }
    
        fun setTrackBackground(track: Drawable?) {
            trackDrawable = track
        }
    
        fun setCornerRadius(radius: Float) {
            cornerRadius = radius
        }
    
        fun setOnRangeSelectedListener(listener: OnRangeSelectedListener) {
            onRangeSelectedListener = listener
        }
    
        override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
            val width = MeasureSpec.getSize(widthMeasureSpec)
            val height =  barHeight.toFloat().toInt()
            setMeasuredDimension(width, height)
        }
    
        override fun onDraw(canvas: Canvas) {
            super.onDraw(canvas)
    
    // Draw the track
            trackDrawable?.let {
                it.setBounds( 0, 0, width, height)
                it.draw(canvas)
            }
    
            for (range in ranges) {
                paint.color = range.color
    
                // Calculate range left and right coordinates, clamping them to the view bounds
                val rangeLeft = ((range.start - valueFrom).toFloat() / (valueTo - valueFrom) * width).clamp(0f, width.toFloat())
                val rangeRight = ((range.end - valueFrom).toFloat() / (valueTo - valueFrom) * width).clamp(0f, width.toFloat())
    
                // Draw rounded rectangle for range
                range.rangeRect.set(rangeLeft, 0f, rangeRight, height.toFloat())
                canvas.drawRoundRect(range.rangeRect, cornerRadius, cornerRadius, paint)
            }
    
        }
    
        override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
            super.onSizeChanged(w, h, oldw, oldh)
            trackDrawable?.setBounds(0, 0, w, h)
        }
    
        override fun setBackground(background: Drawable?) {
            super.setBackground(background)
            trackDrawable = background
        }
    
        override fun onTouchEvent(event: MotionEvent): Boolean {
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    for (range in ranges) {
                        if(range.rangeRect.contains(event.x,event.y)) {
                            onRangeSelectedListener?.onRangeSelected(range.start, range.end)
                            selectedRange = range
                            invalidate()
                            return true
                        }
                    }
                }
            }
            return super.onTouchEvent(event)
        }
    
        inner class Range(val start: Int, val end: Int, val color: Int) {
            val rangeRect = RectF()
        }
    
        interface OnRangeSelectedListener {
            fun onRangeSelected(start: Int, end: Int)
        }
    }
    
    fun Float.clamp(minValue: Float, maxValue: Float): Float {
        return when {
            this < minValue -> minValue
            this > maxValue -> maxValue
            else -> this
        }
    }