I'm trying to make this custom SeekBar in Android 2.2 and everything I do seems to be wrong! I'm trying to display the value of the seekbar over the thumb image of the SeekBar. Does anybody have some experiences with this?
11 Answers
I have followed a different approach which provides more possibilities to customize the thumb. Final output will look like following:
First you have to design the layout which will be set as thumb drawable.
layout_seekbar_thumb.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="@dimen/seekbar_thumb_size"
android:layout_height="@dimen/seekbar_thumb_size"
android:background="@drawable/ic_seekbar_thumb_back"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tvProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="#000000"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
Here seekbar_thumb_size
can be any small size as per your requirement. I have used 30dp
here. For background you can use any drawable/icon of your choice.
Now you need this view to be set as thumb drawable so get it with following code:
View thumbView = LayoutInflater.from(YourActivity.this).inflate(R.layout.layout_seekbar_thumb, null, false);
Here I suggest to initialize this view in onCreate()
so no need to inflate it again and again.
Now set this view as thumb drawable when seekBar progress is changed. Add the following method in your code:
public Drawable getThumb(int progress) {
((TextView) thumbView.findViewById(R.id.tvProgress)).setText(progress + "");
thumbView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
Bitmap bitmap = Bitmap.createBitmap(thumbView.getMeasuredWidth(), thumbView.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
thumbView.layout(0, 0, thumbView.getMeasuredWidth(), thumbView.getMeasuredHeight());
thumbView.draw(canvas);
return new BitmapDrawable(getResources(), bitmap);
}
Now call this method from onProgressChanged()
.
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
// You can have your own calculation for progress
seekBar.setThumb(getThumb(progress));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
Note: Also call getThumb()
method when you initialize seekBar
to initialize it with default value.
With this approach, you can have any custom view on progress change.

- 1,744
- 2
- 26
- 28
-
Isn't this approach a little more expensive since every time the seekbar value changes you inflate a new layout and draw it onto a canvas? – andrea.rinaldi Feb 24 '18 at 07:19
-
@andrea.rinaldi as I suggested in my description, you can inflate your custom view in `onCreate()` so it won't be re-inflating again. If we consider `Canvas` operation then it depends on how complex view you have used to draw as thumb. Here I have only one textView inside my layout and its dimensions are 30x30 dp. So it won't be that much expensive. If you have more complex and bigger layout for thumb then yes it will be expensive. – Chintan Shah Feb 26 '18 at 04:46
-
This only works when the seekbar value has been changed, not when it is initialized. Still really cool and I'm trying to figure out how to fix the issue I mentioned. Nice work! – AjCodez Apr 22 '18 at 22:46
-
1@AjayAujla You can initialize it. Read my last **Note**. If you have default value then call `getThumb()` method wherever you want (e.g. in `onCreate()` after basic initialization of views). :) – Chintan Shah Apr 23 '18 at 03:07
-
Oh nice I missed that section. Thanks for clearing it up! – AjCodez Apr 23 '18 at 19:40
-
Great Worked Like a Charm – Vinesh Chauhan May 18 '18 at 11:52
-
i added your code but in thumb sorrunding having some other color can you help me – humayoon siddique Nov 08 '18 at 07:11
-
@humayoonsiddique can please tell me what you have set as seekbar thumb? – Chintan Shah Nov 10 '18 at 18:12
-
@s.j can you please provide more info? Is it not rendering or giving some error? – Chintan Shah Feb 25 '20 at 10:22
I assume you've already extended the base class, so you have something like:
public class SeekBarHint extends SeekBar {
public SeekBarHint (Context context) {
super(context);
}
public SeekBarHint (Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public SeekBarHint (Context context, AttributeSet attrs) {
super(context, attrs);
}
}
Now you override the onDraw
method with some of your own code. Insert the following:
@Override
protected void onDraw(Canvas c) {
super.onDraw(c);
}
Now, you want to draw some text near the thumb, but there isn't a convenient way to get the thumb's x-position. We just need a little math.
@Override
protected void onDraw(Canvas c) {
super.onDraw(c);
int thumb_x = ( (double)this.getProgress()/this.getMax() ) * (double)this.getWidth();
int middle = this.getHeight()/2;
// your drawing code here, ie Canvas.drawText();
}

- 3,777
- 3
- 35
- 46
-
1Great tip! I think the drawText() call comes *after* the super invocation. – Stephen Niedzielski Feb 13 '13 at 04:18
-
-
This code only draws the text once. How can you make it so that it keeps drawing text as the slider is moved? – DMC Jun 24 '13 at 11:53
-
1onDraw will be called whenever the thumb is moved, whenever the slider is touched, etc. – Snailer Jun 24 '13 at 16:45
-
@Snailer +1 from me..can u update ur answer with full solution?I have also checked this but i cannot understand it properly http://stackoverflow.com/questions/9272384/how-add-textview-in-middle-of-seekbar-thumb – TheFlash Aug 24 '13 at 07:53
-
This won't move unless progress is complete. Since both `this.getProcess()` and `this.getMax()` are **`int`**; you will always get `0 * this.getWidth() == 0` (except for `getProgress() == getMax()`) resulting a stable `TextView`. – Saro Taşciyan Feb 21 '14 at 09:57
-
I suggest you use cast to **`double`** as follows: `int thumb_x = (int)((double)(this.getProgress() / this.getMax()) * this.getWidth());` – Saro Taşciyan Feb 21 '14 at 09:58
-
Yes as I stated in the answer, you probably do have to cast them. I just haven't gotten around to updating the answer (I'm on a cell phone right now). – Snailer Feb 21 '14 at 22:31
-
-
6You could replace the code to calculate the thumb_x with this one -> `int thumb_x = this.getThumb().getBounds().exactCenterX()` – muilpp Aug 10 '15 at 20:58
-
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean b) {
int val = (progress * (seekBar.getWidth() - 2 * seekBar.getThumbOffset())) / seekBar.getMax();
text_seekbar.setText("" + progress);
text_seekbar.setX(seekBar.getX() + val + seekBar.getThumbOffset() / 2);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
text_seekbar.setVisibility(View.VISIBLE);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
text_seekbar.setVisibility(View.GONE);
}
});

- 281
- 3
- 5
This worked for me
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
int val = (progress * (seekBar.getWidth() - 2 * seekBar.getThumbOffset())) / seekBar.getMax();
_testText.setText("" + progress);
_testText.setX(seekBar.getX() + val + seekBar.getThumbOffset() / 2);
}

- 747
- 6
- 17
Hey I found another solution, seems simpler:
private void setText(){
int progress = mSeekBar.getProgress();
int max= mSeekBar.getMax();
int offset = mSeekBar.getThumbOffset();
float percent = ((float)progress)/(float)max;
int width = mSeekBar.getWidth() - 2*offset;
int answer =((int)(width*percent +offset - mText.getWidth()/2));
mText.setX(answer);
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
setText();
mText.setText(""+progress);
}

- 650
- 7
- 23
-
this is not working when we are setting the progress programmatically then calling this method and not by dragging the seek. – Rishabh Srivastava Nov 03 '15 at 11:40
-
1In your function that changes the bar programmatically, call setText() and problem solved! – Yakir Yehuda Apr 04 '16 at 07:16
This follow code aligns your TextView
center to your SeekBar
thumb center.
YOUR_TEXT_VIEW width must be wrap_content in xml.
Hope this code will help you.
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
YOUR_TEXT_VIEW.setText(Integer.toString(progress));
double pourcent = progress / (double) seekBar.getMax();
int offset = seekBar.getThumbOffset();
int seekWidth = seekBar.getWidth();
int val = (int) Math.round(pourcent * (seekWidth - 2 * offset));
int labelWidth = YOUR_TEXT_VIEW.getWidth();
YOUR_TEXT_VIEW.setX(offset + seekBar.getX() + val
- Math.round(pourcent * offset)
- Math.round(pourcent * labelWidth/2));
}

- 297
- 4
- 17
-
It works, but the text isn't centered and it goes out bounds, do you have a solution for that? – Tamim Attafi Jan 29 '20 at 11:25
I used this library to create drawable text view and put that drawable into thumb programmatically.
https://github.com/amulyakhare/TextDrawable
Code is something like this:
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
String dynamicText = String.valueOf(progress);
TextDrawable drawable = TextDrawable.builder()
.beginConfig()
.endConfig()
.buildRoundRect(dynamicText , Color.WHITE ,20);
seekBar.setThumb(drawable);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});

- 41
- 1
Works not bad for me
there is a little hardcode)
please write improvements which smbd may has
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.v7.widget.AppCompatSeekBar;
import android.util.AttributeSet;
import android.util.TypedValue;
public class CustomSeekBar extends AppCompatSeekBar {
@SuppressWarnings("unused")
private static final String TAG = CustomSeekBar.class.getSimpleName();
private Paint paint;
private Rect bounds;
public String dimension;
public CustomSeekBar(Context context) {
super(context);
init();
}
public CustomSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CustomSeekBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init(){
paint = new Paint();
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.STROKE);
paint.setTextSize(sp2px(14));
bounds = new Rect();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String label = String.valueOf(getProgress()) + dimension;
paint.getTextBounds(label, 0, label.length(), bounds);
float x = (float) getProgress() * (getWidth() - 2 * getThumbOffset()) / getMax() +
(1 - (float) getProgress() / getMax()) * bounds.width() / 2 - bounds.width() / 2
+ getThumbOffset() / (label.length() - 1);
canvas.drawText(label, x, paint.getTextSize(), paint);
}
private int sp2px(int sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
}
}

- 7,997
- 3
- 56
- 43
-
what is mean of dimension parameter in this code because this string parameter does not have any string value. – Kishan Donga Jan 20 '18 at 13:58
IMO best way is to do it through code. It is really not that scary and we are all programmers after all :)
class ThumbDrawable(context: Context) : Drawable() {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val textBounds = Rect()
private var shadowColor = context.resources.getColor(R.color.wallet_screen_option_shadow)
private val size = context.resources.getDimensionPixelSize(R.dimen.thumbRadius).toFloat()
private val textSize = context.resources.getDimensionPixelSize(R.dimen.thumbTextSize).toFloat()
var progress: Int = 0
init {
textPaint.typeface = Typeface.createFromAsset(context.assets, "font/avenir_heavy.ttf")
val accentColor = context.resources.getColor(R.color.accent)
paint.color = accentColor
textPaint.color = accentColor
textPaint.textSize = textSize
paint.setShadowLayer(size / 2, 0f, 0f, shadowColor)
}
override fun draw(canvas: Canvas) {
Timber.d("bounds: $bounds")
val progressAsString = progress.toString()
canvas.drawCircle(bounds.left.toFloat(), bounds.top.toFloat(), size, paint)
textPaint.getTextBounds(progressAsString, 0, progressAsString.length, textBounds)
//0.6f is cause of the avenirs spacing, should be .5 for proper font
canvas.drawText(progressAsString, bounds.left.toFloat() - textBounds.width() * 0.6f, bounds.top.toFloat() - size * 2, textPaint)
}
override fun setAlpha(alpha: Int) {
}
override fun getOpacity(): Int {
return PixelFormat.OPAQUE
}
override fun setColorFilter(colorFilter: ColorFilter?) {
}
}
and in your seekbar implementation
class CustomSeekBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
SeekBar(context, attrs, defStyleAttr) {
init {
thumb = ThumbDrawable(context)
setPadding(0, 0, 0, 0);
}
override fun invalidate() {
super.invalidate()
if (thumb is ThumbDrawable) (thumb as ThumbDrawable).progress = progress
}
}

- 182
- 2
- 9
I created this example to show how textview should be supported to different types of screen size and how to calculate the real position of Thumb because sometimes the position could be 0.
public class CustomProgressBar extends RelativeLayout implements AppCompatSeekBar.OnSeekBarChangeListener {
@BindView(R.id.userProgressBar)
protected AppCompatSeekBar progressSeekBar;
@BindView(R.id.textPorcent)
protected TextView porcent;
@BindView(R.id.titleIndicator)
protected TextView title;
public CustomProgressBar(Context context) {
super(context);
init();
}
public CustomProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.custom_progressbar_view, this, true);
ButterKnife.bind(this);
setColors(R.color.green, R.color.progress_bar_remaining);
progressSeekBar.setPadding(0, 0, 0, 0);
progressSeekBar.setOnSeekBarChangeListener(this);
}
private void setPorcentTextViewPosition(float widthView) {
int width = CoreUtils.getScreenSize().x;
float xPosition = ((float) progressSeekBar.getProgress() / 100) * width;
float finalPosition = xPosition - (widthView / 2f);
if (width - xPosition < widthView) {
porcent.setX(width - widthView);
} else if (widthView < finalPosition) {
porcent.setX(finalPosition);
}
}
public void setColors(int progressDrawable, int remainingDrawable) {
LayerDrawable layer = (LayerDrawable) progressSeekBar.getProgressDrawable();
Drawable background = layer.getDrawable(0);
Drawable progress = layer.getDrawable(1);
background.setColorFilter(ContextCompat.getColor(getContext(), remainingDrawable), PorterDuff.Mode.SRC_IN);
progress.setColorFilter(ContextCompat.getColor(getContext(), progressDrawable), PorterDuff.Mode.SRC_IN);
}
public void setValues(int progress, int remaining) {
int value = (progress * remaining) / 100;
progressSeekBar.setMax(remaining);
porcent.setText(String.valueOf(value).concat("%"));
porcent.post(new Runnable() {
@Override
public void run() {
setPorcentTextViewPosition(porcent.getWidth());
}
});
progressSeekBar.setProgress(value);
}
public void setTitle(String title) {
this.title.setText(title);
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
}

- 36
- 4
Add a TextView to your layout. Add onSeekBarChangeListener
.
You will want precision so that the text is exactly in the middle of your seek bar thumb, you have to a little calculation. This is because the width of the text is different. Say, you want to show numbers from 0 to 150. Width of 188 will be different from 111. Because of this, the text you are showing will always tilt to some side.
The way to solve it is to measure the width of the text, remove that from the width of the seekbar thumb, divide it by 2, and add that to the result that was given in the accepted answer. Now you would not care about how large the number range. Here is the code:
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
val value = progress * (seekBar.width - 2 * seekBar.thumbOffset) / seekBar.max
label.text = progress.toString()
label.measure(0, 0)
val textWidth = label.measuredWidth
val firstRemainder = seekThumbWidth - textWidth
val result = firstRemainder / 2
label.x = (seekBar.x + value + result)
}

- 1,920
- 3
- 35
- 54