Search code examples
androidandroid-layoutseekbar

How to hide tickmark of SeekBar when seeking?


I want to design a custom Seekbar like the image below: desired seekbar

However, here is my output:

I have some problems. When I seek forward with SeekBar, the tickmarks yet still exists and I want to hide this. My other problem is two tickmarks are placed out of SeekBar. Also, I want to put the number of each step on the SeekBar, above the SeekBar. I tried the code below but only in onProgressChanged method is the correct position of the SeekBar obtained.

 private fun setNumber(){
    for(i in 0..10 step 2){
        var pos=i*(seekbar.width - 2 * seekbar.thumbOffset)/seekbar.max
        Log.e(TAG,"pos $i= $pos")
        when(i){
            0 -> {
                txt_label_zero.setX(seekbar.x + pos + seekbar.thumbOffset / 2)
            }
            2 -> {
                txt_label_two.setX(seekbar.x + pos + seekbar.thumbOffset / 2)
            }
            4 ->{
                txt_label_four.setX(seekbar.x + pos + seekbar.thumbOffset / 2)
            }
            6 ->{
                txt_label_six.setX(seekbar.x + pos + seekbar.thumbOffset / 2)
            }
            8 ->{
                txt_label_eight.setX(seekbar.x + pos + seekbar.thumbOffset / 2)
            }
            10 ->{
                txt_label_ten.setX(seekbar.x + pos + seekbar.thumbOffset / 2)
            }
        }

    }
}

private fun setSeekbar() {

    seekbar?.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
        override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {

        }

        override fun onStartTrackingTouch(seekBar: SeekBar) {

        }

        override fun onStopTrackingTouch(seekBar: SeekBar) {


        }
    })
}

And here drawable and xml layout: drw_bg_seekbar:

 <?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background"
          android:gravity="center_vertical">
        <shape android:shape="rectangle"
              >
            <corners android:radius="15dp"/>
            <size android:height="30dp" />
            <solid android:color="@color/mainGrey" />
        </shape>
    </item>
    <item android:id="@android:id/progress"
          android:gravity="center_vertical">
        <scale android:scaleWidth="100%">
            <selector>
                <item>
                    <shape android:shape="rectangle"
                         >
                        <corners android:radius="15dp"/>
                        <size android:height="30dp" />
                        <solid android:color="@color/lightGreen" />
                    </shape>
                </item>
            </selector>
        </scale>
    </item>
</layer-list>

drw_thumb_seekbar:

    <shape
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="oval"
        >
    <solid
            android:color="@color/darkGrey" />
    <size
            android:width="24dp"
            android:height="24dp" />
</shape>

drw_bg_tickmark:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
   android:shape="oval"
   android:tint="@color/darkGrey">
<corners android:radius="4dp"/>
<size android:width="16dp"
      android:height="16dp" />
<solid android:color="@color/darkGrey" />

And my layout:

    <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"
             tools:context=".OneFragment"
>


    <LinearLayout
            android:id="@+id/lay_number"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:layout_marginTop="40dp"
            android:visibility="visible"
    >
        <TextView
                android:id="@+id/txt_label_zero"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="0"
        />
        <TextView
                android:id="@+id/txt_label_two"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="2"
        />
        <TextView
                android:id="@+id/txt_label_four"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="4"
        />
        <TextView
                android:id="@+id/txt_label_six"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="6"
        />
        <TextView
                android:id="@+id/txt_label_eight"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="8"
        />
        <TextView
                android:id="@+id/txt_label_ten"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="10"
        />
    </LinearLayout>
    <androidx.appcompat.widget.AppCompatSeekBar
            android:id="@+id/seekbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="15dp"
            android:layout_marginRight="15dp"
            android:layout_marginTop="10dp"
            android:layout_below="@+id/lay_number"
            style="@style/Widget.AppCompat.SeekBar.Discrete"
            android:max="10"
            android:visibility="visible"
            android:progress="4"
            android:thumb="@drawable/drw_thumb_seekbar"
            android:progressDrawable="@drawable/drw_bg_seekbar"
            android:tickMark="@drawable/drw_bg_tickmark"/>

</RelativeLayout>

Solution

  • You can extend AppCompatSeekBar, just override onDraw() method, and redraw everything:

    I had to inspect:

    https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/widget/AbsSeekBar.java

    to figure out how things are drawn.

    So, you need to draw

    Drawing order is important:

    • Background
    • Tick Marks
    • Progress
    • Thumb

    Drawing order is not important:

    • Values

    Note: I had to separate SeekBar background and Progress Drawables in order to satisfy the drawing order above.

    Note: I removed thumb shadow by using android:background="@null" (If thumb shadow is included, it will show up dislocated):

    Remove SeekBar shadow

    CustomSeekBar class

    public class CustomSeekBar extends AppCompatSeekBar {
    
    /**
     * The value that the canvas is translated by in order to show progress values
     * This value is dependent on progress values text size
     */
    private final int DY = 35;
    
    public CustomSeekBar(Context context) {
        super(context);
    }
    
    public CustomSeekBar(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    
    public CustomSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    
    @Override
    protected synchronized void onDraw(Canvas canvas) {
        //super.onDraw(canvas);
        drawBackground(canvas);
        drawTickMarks(canvas);
        drawProgress(canvas);
        drawThumb(canvas);
        drawValues(canvas);
    }
    
    
    @TargetApi(Build.VERSION_CODES.N)
    void drawBackground(Canvas canvas) {
        final Drawable b = ContextCompat.getDrawable(getContext(),
                R.drawable.drw_background_seekbar);
        Drawable mTickMark = getTickMark();
        if (b != null) {
            final int saveCount = canvas.save();
            canvas.translate(getPaddingLeft(), DY);
            Rect rectE = new Rect(-mTickMark.getBounds().width(), getHeight() / 4,
                    getWidth() - 2*mTickMark.getBounds().width(), 3 * getHeight() / 4);
            b.setBounds(rectE);
            b.draw(canvas);
            canvas.restoreToCount(saveCount);
        }
    }
    
    @TargetApi(Build.VERSION_CODES.N)
    void drawProgress(Canvas canvas) {
        final Drawable d = getProgressDrawable();
        Drawable mTickMark = getTickMark();
        if (d != null) {
            final int saveCount = canvas.save();
            canvas.translate(getPaddingLeft(), DY);
            Rect rectE = new Rect(-mTickMark.getBounds().width(), getHeight() / 4,
                    getWidth() - 2 * mTickMark.getBounds().width(), 3 * getHeight() / 4);
            d.setBounds(rectE);
            d.draw(canvas);
            canvas.restoreToCount(saveCount);
        }
    }
    
    @TargetApi(Build.VERSION_CODES.N)
    private void drawTickMarks(Canvas canvas) {
        Drawable mTickMark = getTickMark();
        if (mTickMark != null) {
            int count = getMax();
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                count = getMax() - getMin();
            }
            if (count > 1) {
                final int w = mTickMark.getIntrinsicWidth();
                final int h = mTickMark.getIntrinsicHeight();
                final int halfW = w >= 0 ? w / 2 : 1;
                final int halfH = h >= 0 ? h / 2 : 1;
                mTickMark.setBounds(-halfW, -halfH, halfW, halfH);
    
                final float spacing = (getWidth() - getPaddingLeft() - getPaddingRight()) / (float) count;
                final int saveCount = canvas.save();
                canvas.translate(getPaddingLeft(), getHeight() / 2);
                canvas.translate(0, DY);
                for (int i = 0; i <= count; i++) {
                    mTickMark.draw(canvas);
                    canvas.translate(spacing, 0);
                }
                canvas.restoreToCount(saveCount);
            }
        }
    }
    
    @TargetApi(Build.VERSION_CODES.N)
    private void drawValues(Canvas canvas) {
        Drawable mTickMark = getTickMark();
        if (mTickMark != null) {
            int count = getMax();
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                count = getMax() - getMin();
            }
            if (count > 1) {
                final float spacing = (getWidth() - getPaddingLeft() - getPaddingRight()) / (float) count;
                final int saveCount = canvas.save();
                canvas.translate(getPaddingLeft(), getHeight() / 2 - DY);
                for (int i = 0; i <= count; i = i+2) {
                    drawValue(canvas, String.valueOf(i));
                    canvas.translate(2 * spacing, 0);
                }
                canvas.restoreToCount(saveCount);
            }
        }
    }
    
    private void drawValue(Canvas canvas, String text){
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(Color.GRAY);
        paint.setStyle(Paint.Style.FILL);
        paint.setTextSize(35);
        paint.setTextAlign(Paint.Align.CENTER);
        canvas.drawText(text, 0, 0, paint);
    }
    
    
    /**
     * Draw the thumb.
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    void drawThumb(Canvas canvas) {
        Drawable mThumb = getThumb();
        if (mThumb != null) {
            final int saveCount = canvas.save();
            canvas.translate(getPaddingLeft() - getThumbOffset(), DY + getPaddingTop());
            mThumb.draw(canvas);
            canvas.restoreToCount(saveCount);
        }
    }
    
    }
    

    Example:

    MainActivity class:

    public class MainActivity extends AppCompatActivity {
    
    private final String TAG = MainActivity.class.getSimpleName();
    private AppCompatSeekBar seekbar;
    private CustomSeekBar csb;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        seekbar = (AppCompatSeekBar) findViewById(R.id.seekbar);
        seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
                Snackbar.make(seekBar, "Progress " + i, Snackbar.LENGTH_INDEFINITE).show();
            }
    
            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
    
            }
    
            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
    
            }
        });
        csb = (CustomSeekBar) findViewById(R.id.csb);
        csb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
                Snackbar.make(seekBar, "Custom Progress " + i, Snackbar.LENGTH_INDEFINITE).show();
            }
    
            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
    
            }
    
            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
    
            }
        });
    
        }
    

    main_activity.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".MainActivity">
    
    <androidx.appcompat.widget.AppCompatSeekBar
        android:id="@+id/seekbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="15dp"
        android:layout_marginRight="15dp"
        android:layout_marginTop="100dp"
        android:max="10"
        android:visibility="visible"
        android:progress="4"
        android:splitTrack="false"
        android:tickMark="@drawable/drw_bg_tickmark"
        android:thumb="@drawable/drw_thumb_seekbar"
        android:progressDrawable="@drawable/drw_bg_seekbar"/>
    
    <com.example.rabee.myapplication.CustomSeekBar
        android:id="@+id/csb"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:layout_marginLeft="15dp"
        android:layout_marginRight="15dp"
        android:layout_marginTop="10dp"
        android:layout_below="@id/seekbar"
        android:max="10"
        android:visibility="visible"
        android:progress="4"
        android:background="@null"
        android:tickMark="@drawable/drw_bg_tickmark"
        android:thumb="@drawable/drw_thumb_seekbar"
        android:progressDrawable="@drawable/drw_progress_seekbar"/>
    
    </RelativeLayout>
    

    drw_bg_seekbar.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    
    <item android:id="@android:id/background"
        android:gravity="center_vertical">
        <shape android:shape="rectangle">
            <corners android:radius="15dp"/>
            <size android:height="30dp" />
            <solid android:color="#DCDCDC" />
        </shape>
    </item>
    
    <item android:id="@android:id/progress"
        android:gravity="center_vertical">
        <scale android:scaleWidth="100%">
            <selector>
                <item>
                    <shape android:shape="rectangle"
                        >
                        <corners android:radius="15dp"/>
                        <size android:height="30dp" />
                        <solid android:color="@android:color/holo_green_light" />
                    </shape>
                </item>
            </selector>
        </scale>
    </item>
    
    </layer-list>
    

    drw_background_seekbar.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    
    <item android:id="@android:id/background"
        android:gravity="center_vertical">
        <shape android:shape="rectangle">
            <corners android:radius="15dp"/>
            <size android:height="30dp" />
            <solid android:color="#DCDCDC" />
        </shape>
    </item>
    
    </layer-list>
    

    drw_progress_seekbar.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    
    <item
        android:id="@android:id/progress"
        android:gravity="center_vertical">
                    <scale android:scaleWidth="100%">
                    <shape android:shape="rectangle">
                        <corners android:radius="15dp"/>
                        <size android:height="30dp" />
                        <solid android:color="@android:color/holo_green_light" />
                    </shape>
                    </scale>
                </item>
    
    </layer-list>
    

    drw_bg_tickmark.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval"
    android:tint="#696969">
    <corners android:radius="4dp"/>
    <size android:width="10dp"
        android:height="10dp" />
    <solid android:color="@android:color/darker_gray" />
    

    drw_thumb_seekbar.xml:

    <shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval"
    >
    <solid
        android:color="#696969" />
    <size
        android:width="40dp"
        android:height="40dp" />
    </shape>
    

    Output:

    output