How to mark horizontal ProgressBar with different color at some index just like Youtube video yellow color ad marker in Android

One possibility is to create a custom view. By doing so you can draw exactly what you need on a canvas, and for a custom progress-bar view this is rather easy. However, it is not as quick as using built-in Views, but the advantage is you can customize it exactly as you want it. Do note this code is just a draft showing it is possible.

I created attributes so it is easy to customize the color of the progress-bar's components, and you can modify the height. The gif below shows the progress bar created at the bottom:

Progress-bar in action

class IndicatorProgressBar(context: Context, attrs: AttributeSet) : View(context, attrs) {
    private val TAG = "IndicatorProgressBar"

    private var barColor = Color.GRAY
    private var barHeight = 25F
    private var indicatorColor = Color.CYAN
    private var progressColor = Color.GREEN
    private val paint = Paint()

    lateinit var indicatorPositions: List<Float>
    var progress = 0F // From float from 0 to 1
        set(state) {
            field = state
            invalidate()
        }

    init {
        paint.isAntiAlias = true
        setupAttributes(attrs)
    }

    private fun setupAttributes(attrs: AttributeSet?) {
        context.theme.obtainStyledAttributes(
            attrs, R.styleable.IndicatorProgressBar,
            0, 0
        ).apply {
            barColor = getColor(R.styleable.IndicatorProgressBar_barColor, barColor)
            barHeight = getFloat(R.styleable.IndicatorProgressBar_barHeight, barHeight)
            progress = getFloat(R.styleable.IndicatorProgressBar_progress, progress)
            progressColor = getColor(R.styleable.IndicatorProgressBar_progressColor, progressColor)
            indicatorColor =
                getColor(R.styleable.IndicatorProgressBar_indicatorColor, indicatorColor)
            recycle()
        }
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        paint.style = Paint.Style.FILL // We will only use FILL for the progress bar's components.
        drawProgressBar(canvas)
        drawProgress(canvas)
        drawIndicators(canvas)
    }

    /**
     * Used to get the measuredWidth from the view as a float to be used in the draw methods.
     */
    private fun width(): Float {
        return measuredWidth.toFloat()
    }

    private fun drawProgressBar(canvas: Canvas) {
        paint.color = barColor
        drawCenteredBar(canvas, 0F, width())
    }

    private fun drawProgress(canvas: Canvas) {
        paint.color = progressColor

        val barWidth = (progress) * width()
        drawCenteredBar(canvas, 0F, barWidth)
    }

    private fun drawIndicators(canvas: Canvas) {
        paint.color = indicatorColor
        indicatorPositions.forEach {
            val barPositionCenter = it * width()
            val barPositionLeft = barPositionCenter - 3F
            val barPositionRight = barPositionCenter + 3F

            drawCenteredBar(canvas, barPositionLeft, barPositionRight)
        }
    }

    private fun drawCenteredBar(canvas: Canvas, left: Float, right: Float) {
        val barTop = (measuredHeight - barHeight) / 2
        val barBottom = (measuredHeight + barHeight) / 2

        val barRect = RectF(left, barTop, right, barBottom)
        canvas.drawRoundRect(barRect, 50F, 50F, paint)
    }

    override fun onSaveInstanceState(): Parcelable {
        val bundle = Bundle()
        bundle.putFloat("progress", progress)
        bundle.putParcelable("superState", super.onSaveInstanceState())
        return bundle
    }

    override fun onRestoreInstanceState(state: Parcelable) {
        var viewState = state

        if (viewState is Bundle) {
            progress = viewState.getFloat("progress", progress)
            viewState = viewState.getParcelable("superState")!!
        }

        super.onRestoreInstanceState(viewState)
    }

    override fun performClick(): Boolean {
        super.performClick()
        return true
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        super.onTouchEvent(event)

        Log.d(TAG, "x=${event.x} / ${width()} (${event.x / measuredWidth}%), y=${event.y}")
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                return updateProgress(event)
            }
            MotionEvent.ACTION_MOVE -> {
                return updateProgress(event)
            }
            MotionEvent.ACTION_UP -> {
                performClick()
                return true
            }
        }
        return false
    }

    private fun updateProgress(event: MotionEvent): Boolean {
        // percent may be outside the range (0..1)
        val percent = event.x / width()
        val boundedPercent = min(max(percent, 0F), 1F) // not above 1
        progress = boundedPercent

        invalidate() // Make the view redraw itself
        return true
    }
}

The attributes for custom views are defined in res/values/attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="IndicatorProgressBar">
        <!-- The color of the progressbar (not the progress)-->
        <attr name="barColor" format="color" />
        <!-- The color of the indicators on the progress bar (such as ads on YouTube)-->
        <attr name="indicatorColor" format="color" />
        <!-- The color of the progressed time/work of the progressbar-->
        <attr name="progressColor" format="color" />
        <!-- The initial progress value, a value from 0 to 1 -->
        <attr name="progress" format="float"/>
        <!-- The height of the progress bar, note that layout_height should be set to a larger
        number so the onTouchEvent listener is more easy to trigger-->
        <attr name="barHeight" format="float"/>

    </declare-styleable>
</resources>

You use the custom view in layouts like this:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000"
    android:gravity="bottom"
    android:paddingLeft="5dp"
    android:paddingTop="5dp"
    android:paddingRight="5dp"
    android:paddingBottom="5dp"
    tools:context=".MainActivity">

    <com.yourpackagename.progressbarindicator.IndicatorProgressBar
        android:id="@+id/indicatorProgressBar"
        android:layout_width="wrap_content"
        android:layout_height="25dp"
        android:layout_centerVertical="true"
        android:foregroundGravity="center"
        app:barColor="@color/colorAccent"
        app:barHeight="12"
        app:indicatorColor="#ffffff"
        app:progress="0"
        app:progressColor="#11c011" />

</RelativeLayout>

Main activity:

class MainActivity : AppCompatActivity() {
    private lateinit var indicatorProgressBar: IndicatorProgressBar
    private val scope: CoroutineScope = CoroutineScope(Dispatchers.Unconfined)
    private val TAG = "MainActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        indicatorProgressBar = findViewById(R.id.indicatorProgressBar)
        indicatorProgressBar.indicatorPositions = listOf(0.13F, 0.34F, 0.57F, 0.85F, 0.92F)

        updateCurrentTime()

        indicatorProgressBar.setOnClickListener {
            if(indicatorProgressBar.progress >= 1F){
                updateCurrentTime()
            }
        }
    }

    private fun updateCurrentTime() {
        scope.launch {
            while (indicatorProgressBar.progress <= 1F){
                Log.d(TAG, "In while loop")
                delay(33)
                runOnUiThread{
                    indicatorProgressBar.progress += 0.003F
                    Log.d(TAG, "Progress is now: ${indicatorProgressBar.progress}")
                }
            }

        }
    }

Add Kotlin Coroutines to your dependencies in build.gradle (app) if you want to run the updateCurrentTime method in the MainActivity:

dependencies {
    ...

    def coroutines_version = "1.3.1"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
}

Finally I got the solution. Below are the steps to implement the same--

Step-1] Create one "attrs.xml" file in "res/values/" folder and paste below code in that file--

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="DottedSeekBar">
        <attr name="dots_positions" format="reference"/>
        <attr name="dots_drawable" format="reference"/>
    </declare-styleable>
</resources>

Step-2] Prepare one image icon which you want to use to mark on progress bar and name it "video_mark.png".

Step-3] Create one custom SeekBar as below--

public class DottedSeekBar extends AppCompatSeekBar {

    /** Int values which corresponds to dots */
    private int[] mDotsPositions = null;
    /** Drawable for dot */
    private Bitmap mDotBitmap = null;

    public DottedSeekBar(final Context context) {
        super(context);
        init(null);
    }

    public DottedSeekBar(final Context context, final AttributeSet attrs) {
        super(context, attrs);
        init(attrs);
    }

    public DottedSeekBar(final Context context, final AttributeSet attrs, final int defStyle) {
        super(context, attrs, defStyle);
        init(attrs);
    }

    /**
     * Initializes Seek bar extended attributes from xml
     *
     * @param attributeSet {@link AttributeSet}
     */
    private void init(final AttributeSet attributeSet) {
        final TypedArray attrsArray = getContext().obtainStyledAttributes(attributeSet, R.styleable.DottedSeekBar, 0, 0);

        final int dotsArrayResource = attrsArray.getResourceId(R.styleable.DottedSeekBar_dots_positions, 0);

        if (0 != dotsArrayResource) {
            mDotsPositions = getResources().getIntArray(dotsArrayResource);
        }

        final int dotDrawableId = attrsArray.getResourceId(R.styleable.DottedSeekBar_dots_drawable, 0);

        if (0 != dotDrawableId) {
            mDotBitmap = BitmapFactory.decodeResource(getResources(), dotDrawableId);
        }
    }

    /**
     * @param dots to be displayed on this SeekBar
     */
    public void setDots(final int[] dots) {
        mDotsPositions = dots;
        invalidate();
    }

    /**
     * @param dotsResource resource id to be used for dots drawing
     */
    public void setDotsDrawable(final int dotsResource)
    {
        mDotBitmap = BitmapFactory.decodeResource(getResources(), dotsResource);
        invalidate();
    }

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

        final float width=getMeasuredWidth()-getPaddingLeft()-getPaddingRight();
        final float step=width/(float)(getMax());

        if (null != mDotsPositions && 0 != mDotsPositions.length && null != mDotBitmap) {
            // draw dots if we have ones
            for (int position : mDotsPositions) {
                canvas.drawBitmap(mDotBitmap, position * step, 0, null);
            }
        }
    }
}

Step-4] Use this custom SeekBar in your activity.xml file as below--

<com.your_package.DottedSeekBar
                        android:id="@+id/videoProgress"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"/>                    

Step-5] Add below code in "onCreate()" method of your "Activity.java" class--

        DottedSeekBar videoProgress = (DottedSeekBar) findViewById(R.id.videoProgress);
        // Disable SeekBar Thumb Drag. (Optional)
                videoProgress.setOnTouchListener(new View.OnTouchListener()
                {
                    @Override
                    public boolean onTouch(View view, MotionEvent motionEvent)
                    {
                        return true;
                    }
                });


    // Set custom thumb icon color here (Optional)

        videoProgress.getThumb().setColorFilter(getResources().getColor(R.color.cerulean_blue), PorterDuff.Mode.SRC_IN);

    // Add below line to avoid unnecessary SeekBar padding. (Optional)

        videoProgress.setPadding(0, 0, 0, 0);

// Handler to update video progress time--

handler = new Handler();
        // Define the code block to be executed
        final Runnable runnableCode = new Runnable() {
            @Override
            public void run()
            {
                updateCurrentTime();
                // Repeat this the same runnable code block again another 1 seconds
                // 'this' is referencing the Runnable object
                handler.postDelayed(this, 1000);
            }
        };

Use "videoView.setOnPreparedListener()" method to calculate total video time in seconds



     yourVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener()
                                              {
                                                  @Override
                                                  public void onPrepared(MediaPlayer mp)
                                                  {


                                                      String strTotalDuration = msToTimeConverter(vidView.getDuration());

                                                      String[] strTimeArr = strTotalDuration.split(":");

                                                      int min = Integer.parseInt(strTimeArr[0]);
                                                      int videoLengthInSec = Integer.parseInt(strTimeArr[1]);
                                                      videoLengthInSec = videoLengthInSec + (min*60);

                                                      videoProgress.setProgress(0);
                                                      videoProgress.setMax(videoLengthInSec);

                                                      // Start the initial runnable task by posting through the handler
                                                      handler.post(runnableCode);

                                                      initVideoMarkers();
                                                  }
                                              }

                );

Step-6] Copy below required methods in your "Activity.java" class--

// Method to update time progress

 private void updateCurrentTime()
        {
            if (videoProgress.getProgress() >= 100)
            {
                handler.removeMessages(0);
            }
            String currentPosition = msToTimeConverter(vidView.getCurrentPosition());

            String[] strArr = currentPosition.split(":");

            int progress = vidView.getCurrentPosition() * videoLengthInSec / vidView.getDuration();

            videoProgress.setProgress(progress);
        }

// Milliseconds to Time converter Method

     String msToTimeConverter(int millis)
        {
            return String.format("%02d:%02d", TimeUnit.MILLISECONDS.toMinutes(millis) - TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(millis)),
                    TimeUnit.MILLISECONDS.toSeconds(millis) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(millis)));
        }

// Method to set Marker values

private void initVideoMarkers()
        {
            // Here I'm adding markers on 10, 15 and 20 Second index
            videoProgress.setDots(new int[] {10, 15, 20});
            videoProgress.setDotsDrawable(R.drawable.video_mark);
       }

Tags:

Android