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

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

Posted by: admin May 14, 2020 Leave a comment

Questions:

In my current app there is requirement to create custom video player and the special requirement is, to display or mark video progress-bar with different color at some given time-index just like Youtube marks its video with yellow color to indicate ads on its video progress bar.

For more clarity please check below screen-shot:

enter image description here

At this moment I’m done with Video Player functionality using VideoView and for now, I’m using Horizontal ProgressBar to show video progress. I did lot of research, but unfortunately no result so far. There is no compulsion to use only Horizontal ProgressBar, we can use SeekBar or inbuilt MediaController too, but the thing is we should be able to mark it with different color at some given position. Below is my code I have written so far:

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000"
    android:paddingLeft="5dp"
    android:paddingTop="5dp"
    android:paddingRight="5dp"
    android:paddingBottom="5dp"
    tools:context=".MainActivity">

    <VideoView
        android:id="@+id/myVideo"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_centerInParent="true" />

    <RelativeLayout
        android:id="@+id/rlVidProgress"
        android:layout_width="match_parent"
        android:layout_alignParentBottom="true"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/tvCurrentTime"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_marginLeft="5dp"
            android:layout_marginRight="20dp"
            android:text="00:00"
            android:textColor="@android:color/white"
            android:textSize="13dp"
            android:textStyle="bold" />

        <ProgressBar
            android:id="@+id/videoProgress"
            style="@android:style/Widget.ProgressBar.Horizontal"
            android:layout_centerVertical="true"
            android:layout_toLeftOf="@+id/tvTotalTime"
            android:layout_width="fill_parent"
            android:layout_toRightOf="@+id/tvCurrentTime"
            android:layout_height="10dp"
            android:layout_marginBottom="10dp" />

        <TextView
            android:id="@+id/tvTotalTime"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_alignParentRight="true"
            android:layout_marginLeft="20dp"
            android:layout_marginRight="5dp"
            android:text="00:00"
            android:textColor="@android:color/white"
            android:textSize="13dp"
            android:textStyle="bold" />

    </RelativeLayout>

    <TextView
        android:id="@+id/tvAutoSave"
        android:layout_width="wrap_content"
        android:textColor="@android:color/white"
        android:layout_alignParentRight="true"
        android:layout_marginBottom="10dp"
        android:text="@string/click_to_auto_save_offer"
        android:background="@drawable/white_border_bg"
        android:textSize="13dp"
        android:layout_above="@+id/rlVidProgress"
        android:visibility="gone"
        android:paddingBottom="3dp"
        android:paddingTop="3dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:textStyle="bold"
        android:layout_height="wrap_content" />

</RelativeLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity implements View.OnClickListener
{
    ProgressBar videoProgress;
    TextView tvCurrentTime;
    Handler handler;
    VideoView vidView;
    String strTotalDuration;
    TextView tvAutoSave;
    Animation animFadeIn, animFadeOut;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        animFadeIn = new AlphaAnimation(0, 1);
        animFadeIn.setInterpolator(new DecelerateInterpolator()); //add this
        animFadeIn.setDuration(1000);

        animFadeOut = new AlphaAnimation(1, 0);
        animFadeOut.setInterpolator(new AccelerateInterpolator()); //and this
        //animFadeOut.setStartOffset(500);
        animFadeOut.setDuration(500);


        tvCurrentTime = (TextView) findViewById(R.id.tvCurrentTime);
        final TextView tvTotalTime = (TextView) findViewById(R.id.tvTotalTime);
        tvAutoSave = (TextView) findViewById(R.id.tvAutoSave);
        tvAutoSave.setOnClickListener(this);

        vidView = (VideoView)findViewById(R.id.myVideo);

        videoProgress = (ProgressBar) findViewById(R.id.videoProgress);
        videoProgress.setProgress(0);
        videoProgress.setMax(100);

        final String videoSource = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
        vidView.setKeepScreenOn(true);
        vidView.setVideoURI(Uri.parse(videoSource));
        vidView.setMediaController(null);

        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);
            }
        };

        vidView.setOnCompletionListener(new MediaPlayer.OnCompletionListener()
        {
            @Override
            public void onCompletion(MediaPlayer mp)
            {
                if(tvAutoSave.getVisibility() == View.VISIBLE)
                {
                    tvAutoSave.setVisibility(View.GONE);
                    tvAutoSave.startAnimation(animFadeOut);
                }
            }
        });

        vidView.setOnPreparedListener(new MediaPlayer.OnPreparedListener()
                                      {
                                          @Override
                                          public void onPrepared(MediaPlayer mp)
                                          {
                                              vidView.start();
                                              strTotalDuration = msToTimeConverter(vidView.getDuration());
                                              tvTotalTime.setText(""+strTotalDuration);
                                              // Start the initial runnable task by posting through the handler
                                              handler.post(runnableCode);
                                              //startHandler();
                                          }
                                      }


        );
    }


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

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

        if(strArr.length==2 && (strArr[1].equals("06") || strArr[1].equals("6")))
        {
            Toast.makeText(MainActivity.this, "Trigger success at 6 sec position.......", Toast.LENGTH_SHORT).show();
            tvAutoSave.setVisibility(View.VISIBLE);
            tvAutoSave.startAnimation(animFadeIn);
        }

        int progress = vidView.getCurrentPosition() * 100 / vidView.getDuration();
        videoProgress.setProgress(progress);

        tvCurrentTime.setText(""+currentPosition);
    }

    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)));
    }

    @Override
    public void onClick(View v)
    {
        switch (v.getId())
        {
            case R.id.tvAutoSave:
                tvAutoSave.setVisibility(View.GONE);
                tvAutoSave.startAnimation(animFadeOut);
                Toast.makeText(MainActivity.this, "Offer(s) saved in wallet successfully", Toast.LENGTH_SHORT).show();
                break;
        }
    }
}
How to&Answers:

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

Answer:

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);
       }