21

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?

Gary
  • 13,303
  • 18
  • 49
  • 71

11 Answers11

36

I have followed a different approach which provides more possibilities to customize the thumb. Final output will look like following:

enter image description here

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.

Chintan Shah
  • 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
21

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();
}
Snailer
  • 3,777
  • 3
  • 35
  • 46
  • 1
    Great tip! I think the drawText() call comes *after* the super invocation. – Stephen Niedzielski Feb 13 '13 at 04:18
  • You're right stephen, I forgot the super always has to be first! – Snailer Feb 13 '13 at 16:12
  • 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
  • 1
    onDraw 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
  • what to do in onDraw(Canvas c) ? – Maveňツ Jun 23 '15 at 07:42
  • 6
    You 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
  • Where do you get the paint for this drawText? – Bhargav Mar 31 '16 at 06:12
11
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);
            }
        });
Chirag Darji
  • 281
  • 3
  • 5
6

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);
}
patrickfdsouza
  • 747
  • 6
  • 17
6

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);
}
Yakir Yehuda
  • 650
  • 7
  • 23
6

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));
}
Sigvent
  • 297
  • 4
  • 17
4

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) {

        }
    });
3

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());
    }
}
Vlad
  • 7,997
  • 3
  • 56
  • 43
3

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

    }



}

final result is something like this seek bar implementation result

0

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) {
    }
}
0

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)
                }
Sermilion
  • 1,920
  • 3
  • 35
  • 54