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