Android discrete selection control

Keywords: Java Android Apache

Recently, we need to use the drag bar in the project, and the drag bar can only be between certain values, so we found some data.

SeekBar

SeekBar enables discrete value selection by setting the Widget.AppCompat.SeekBar.Discrete theme.

<androidx.appcompat.widget.AppCompatSeekBar
        android:id="@+id/seekBar"
        style="@style/Widget.AppCompat.SeekBar.Discrete"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="4"
        android:maxHeight="4dp"
        android:progress="3"
        android:progressDrawable="@drawable/progress"
        android:thumb="@drawable/thumb"
        app:tickMark="@drawable/tickmark" />

Corresponding progress.xml resource:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background">
        <shape>
            <corners android:radius="20dp" />
            <solid android:color="#DCDCDC" />
        </shape>
    </item>
    <item android:id="@android:id/progress">
        <clip>
            <shape>
                <corners android:radius="20dp" />
                <solid android:color="#A7F2AA" />
            </shape>
        </clip>
    </item>
</layer-list>

Corresponding thumb.xml resource:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape android:shape="oval">
            <solid android:color="#BBEEBD" />
            <size
                android:width="15dp"
                android:height="15dp" />
        </shape>
    </item>

    <item
        android:bottom="5dp"
        android:left="5dp"
        android:right="5dp"
        android:top="5dp">
        <shape android:shape="oval">
            <solid android:color="#21B827" />
            <size
                android:width="10dp"
                android:height="10dp" />
        </shape>
    </item>
</layer-list>

Corresponding tickmark.xml resource:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">

    <solid android:color="#72EA77" />
    <size
        android:width="10dp"
        android:height="10dp" />
</shape>

Slider

The Slider is a control in the Material Design library. The specific introduction methods are as follows:

	implementation 'com.google.android.material:material:1.4.0'

The Slider can be used as either a continuous Slider or a discrete Slider. The operation mode is controlled by the value of the step size. If the step size is set to 0, the Slider runs as a continuous Slider, and the Slider can move to any position along the horizontal line. If the step size is set to a number greater than 0, the Slider runs as a discrete Slider and the Slider snaps to the nearest valid value.
The Slider can display indication labels when sliding. The LabelFormatter interface is used to define the format of the text rendered in the instruction label during interaction.
Use the default style com.google.android.material.R.style#Widget_MaterialComponents_Slider, use colorPrimary and colorOnPrimary to customize the color of the slider, and use colorOnSurface to define the disabled color. The following attributes are used to further customize the appearance of the slider:

  • haloColor: the color of the halo around the thumb
  • haloRadius: the radius of the halo around the thumb
  • labelBehavior: label behavior, which can be LABEL_FLOATING,LABEL_WITHIN_BOUNDS or LABEL_GONE
  • labelStyle: the style applied to the value indicator TooltipDrawable
  • thumbColor: the color of the thumb
  • thumbStrokeColor: stroke color of thumb
  • thumbStrokeWidth: stroke width of thumb
  • thumbElevation: stroke height of thumb
  • thumbRadius: the radius of the thumb
  • tickColorActive: the scale color of the sliding part. Used only when the slider is in discrete mode
  • tickColorInactive: the tick mark color of the non sliding part of the slider. Used only when the slider is in discrete mode
  • tickColor: the color of the slider tick mark. Only if the slider is in discrete mode. When tickecoloractive and tickecolorinactive are the same, you can only set this value, which takes precedence over tickecoloractive and tickecolorinactive
  • tickVisible: whether to display tick marks. Only if the slider is in discrete mode
  • trackColorActive: the color of the track sliding part
  • trackColorInactive: the color of the non sliding part of the track
  • trackColor: the color of the entire track. Only this value can be set when trackColorActive and trackColorInactive are the same. Priority over trackColorActive and trackColorInactive
  • trackHeight: track height
  • android:valueFrom: required. The minimum value of the slider, which must be less than valueTo
  • android:valueTo: required. The maximum value of the slider, which must be greater than valueFrom
  • android:value: optional. Initial value of slider
  • android:stepSize: optional. This value indicates whether the slider operates in continuous mode or discrete mode. If missing or equal to 0, the slider is in continuous mode. If it is greater than 0 and the valueFrom and valueTo are evenly divided, the slider runs in discrete mode

The corresponding usage is as follows:

<com.google.android.material.slider.Slider
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:stepSize="1"
        android:value="3"
        android:valueFrom="0"
        android:valueTo="4"
        app:haloColor="@color/black"
        app:haloRadius="8dp"
        app:thumbColor="#60C664"
        app:thumbRadius="8dp"
        app:tickColor="#60C664"
        app:tickColorActive="#22B528"
        app:tickColorInactive="#A0AAA0"
        app:tickVisible="true"
        app:trackColor="#60C664"
        app:trackColorActive="#22B528"
        app:trackColorInactive="#A0AAA0"
        app:trackHeight="4dp" />

Custom control

Customize discrete sliding controls by inheriting View.

public class DiscreteSeekBar extends View {

    public interface OnDiscreteValueChangedListener {
        void onValueChanged(DiscreteSeekBar seekBar, int value);
    }

    private Drawable mThumb;
    private Drawable mTrackActive;
    private Drawable mTrackInactive;
    private Drawable mTickMarkActive;
    private Drawable mTickMarkInactive;
    private int mTrackHeight;
    private int mLabelTextColor;
    private int mLabelTextSize;
    private int mLabelPadding;

    private List<Rect> mTickRectList = new ArrayList<>();

    private List<String> mTickLabelList = new ArrayList<>();
    private int mLabelCount = 0;

    private Paint mPaint;
    private Paint mLabelPaint;

    private int mValue;
    private int mValueFrom;
    private int mValueTo;
    private int mStepSize;

    private Rect mRect = new Rect();

    private OnDiscreteValueChangedListener mOnDiscreteValueChangedListener;

    public DiscreteSeekBar(Context context) {
        this(context, null);
    }

    public DiscreteSeekBar(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DiscreteSeekBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.DiscreteSeekBar, defStyleAttr, 0);
        mThumb = ta.getDrawable(R.styleable.DiscreteSeekBar_android_thumb);
        mTrackActive = ta.getDrawable(R.styleable.DiscreteSeekBar_trackActive);
        mTrackInactive = ta.getDrawable(R.styleable.DiscreteSeekBar_trackInactive);
        mTickMarkActive = ta.getDrawable(R.styleable.DiscreteSeekBar_tickMarkActive);
        mTickMarkInactive = ta.getDrawable(R.styleable.DiscreteSeekBar_tickMarkInactive);
        mTrackHeight = ta.getDimensionPixelSize(R.styleable.DiscreteSeekBar_trackHeight, 4);
        mLabelTextColor = ta.getColor(R.styleable.DiscreteSeekBar_labelTextColor, Color.BLACK);
        mLabelTextSize = ta.getDimensionPixelSize(R.styleable.DiscreteSeekBar_labelTextSize, 16);
        mLabelPadding = ta.getDimensionPixelSize(R.styleable.DiscreteSeekBar_labelPadding, 0);
        mValueFrom = (int) ta.getFloat(R.styleable.DiscreteSeekBar_android_valueFrom, 0f);
        mValueTo = (int) ta.getFloat(R.styleable.DiscreteSeekBar_android_valueTo, 10f);
        mValue = (int) ta.getFloat(R.styleable.DiscreteSeekBar_android_value, mValueFrom);
        mStepSize = (int) ta.getFloat(R.styleable.DiscreteSeekBar_android_stepSize, 1);
        ta.recycle();

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setStrokeWidth(mTrackHeight);
        mPaint.setColor(Color.RED);

        mLabelPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mLabelPaint.setTextSize(mLabelTextSize);
        mLabelPaint.setColor(mLabelTextColor);
    }

    public int getLabelPadding() {
        return this.mLabelPadding;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int dw = MeasureSpec.getSize(widthMeasureSpec);
        int dh = MeasureSpec.getSize(heightMeasureSpec);

        final Drawable d = mThumb;
        if (d != null) {
            dh = Math.max(dh, d.getIntrinsicHeight());
        }

        int labelHeight = 0;
        if (mLabelCount != 0) {
            Paint.FontMetrics fm = mLabelPaint.getFontMetrics();
            labelHeight = (int) Math.ceil(fm.bottom - fm.top);
        }

        dh += getPaddingTop() + getPaddingBottom() + getLabelPadding() + labelHeight;

        final int measuredWidth = resolveSizeAndState(dw, widthMeasureSpec, 0);
        final int measuredHeight = resolveSizeAndState(dh, heightMeasureSpec, 0);
        setMeasuredDimension(measuredWidth, measuredHeight);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        mTickRectList.clear();
        if (mThumb != null) {
            final int count = getTickCount();
            int tickPadding;
            if (mLabelCount > 0) {
                int start = Math.max((int) Math.ceil(mLabelPaint.measureText(mTickLabelList.get(0))), mThumb.getIntrinsicWidth()) >> 1;
                int end = Math.max((int) Math.ceil(mLabelPaint.measureText(mTickLabelList.get(mLabelCount - 1))), mThumb.getIntrinsicWidth()) >> 1;
                tickPadding = (w - start - end - getPaddingLeft() - getPaddingRight()) / (count - 1);
            } else {
                tickPadding = (w - mThumb.getIntrinsicWidth() - getPaddingLeft() - getPaddingRight()) / (count - 1);
            }
            for (int i = 0; i < count; i++) {
                Rect rect = new Rect();
                rect.left = getPaddingLeft() + tickPadding * i;
                rect.top = getPaddingTop();
                rect.right = rect.left + mThumb.getIntrinsicWidth();
                rect.bottom = rect.top + mThumb.getIntrinsicHeight();
                mTickRectList.add(rect);
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        final int activePosition = (mValue - mValueFrom) / mStepSize;
        final int tickCount = getTickCount();
        final int halfTrackHeight = mTrackHeight >> 1;
        Rect startRect = mTickRectList.get(0);
        Rect centerRect = mTickRectList.get(activePosition);
        Rect endRect = mTickRectList.get(tickCount - 1);
        if (activePosition != 0) {
            mTrackActive.setBounds(startRect.centerX(), startRect.centerY() - halfTrackHeight, centerRect.centerX(), centerRect.centerY() + halfTrackHeight);
            mTrackActive.draw(canvas);
        }
        if (activePosition != tickCount - 1) {
            mTrackInactive.setBounds(centerRect.centerX(), centerRect.centerY() - halfTrackHeight, endRect.centerX(), endRect.centerY() + halfTrackHeight);
            mTrackInactive.draw(canvas);
        }

        for (int i = 0; i < tickCount; i++) {
            Rect rect = mTickRectList.get(i);
            if (i < activePosition) {
                setTickDrawableBound(mTickMarkActive, rect);
                mTickMarkActive.draw(canvas);
            } else if (i > activePosition) {
                setTickDrawableBound(mTickMarkInactive, rect);
                mTickMarkInactive.draw(canvas);
            } else {
                mThumb.setBounds(rect);
                mThumb.draw(canvas);
            }
        }

        if (mLabelCount > 0) {
            drawLabel(canvas);
        }
    }

    private void drawLabel(Canvas canvas) {
        final int tickCount = getTickCount();
        Rect rect;
        String label;
        Paint.FontMetrics fm = mLabelPaint.getFontMetrics();
        final float baseline = (fm.bottom - fm.top) / 2 + (fm.descent - fm.ascent) / 2 - fm.descent;
        for (int i = 0; i < tickCount; i++) {
            label = getLabel(i);
            rect = mTickRectList.get(i);
            if (!TextUtils.isEmpty(label)) {
                float labelWidth = mLabelPaint.measureText(label);
                canvas.drawText(label, rect.centerX() - labelWidth / 2, rect.bottom + mLabelPadding + baseline, mLabelPaint);
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int action = event.getActionMasked();
        if (action == MotionEvent.ACTION_DOWN) {
            for (int i = 0; i < getTickCount(); i++) {
                Rect rect = mTickRectList.get(i);
                if (rect.contains((int) event.getX(), (int) event.getY())) {
                    return true;
                }
            }
        } else if (action == MotionEvent.ACTION_UP) {
            if (event.getEventTime() - event.getDownTime() < ViewConfiguration.getTapTimeout()) {
                for (int i = 0; i < getTickCount(); i++) {
                    Rect rect = mTickRectList.get(i);
                    if (rect.contains((int) event.getX(), (int) event.getY())) {
                        mValue = mValueFrom + i * mStepSize;
                        invalidate();
                        break;
                    }
                }
            }
        } else if (action == MotionEvent.ACTION_MOVE) {
            final int x = (int) event.getX();
            int index = 0;
            int centerX = mTickRectList.get(0).centerX();
            for (int i = 1, size = getTickCount(); i < size; i++) {
                Rect rect = mTickRectList.get(i);
                centerX = (centerX + rect.centerX()) >> 1;
                if (x < centerX) {
                    index = i - 1;
                    break;
                }

                if (i == size - 1 && x >= centerX) {
                    index = i;
                    break;
                }
                centerX = rect.centerX();
            }
            final int value = mValueFrom + index * mStepSize;
            if (value != mValue) {
                mValue = value;
                invalidate();
                if (mOnDiscreteValueChangedListener != null) {
                    mOnDiscreteValueChangedListener.onValueChanged(this, value);
                }
            }
        }
        return super.onTouchEvent(event);
    }

    public void setLabelList(List<String> list) {
        mTickLabelList.clear();
        if (list != null) {
            mTickLabelList.addAll(list);
        }
        mLabelCount = mTickLabelList.size();
        requestLayout();
        invalidate();
    }

    public void addLabel(String label) {
        mTickLabelList.add(label);
        mLabelCount = mTickLabelList.size();
        requestLayout();
        invalidate();
    }

    private String getLabel(int position) {
        if (position < mLabelCount) {
            return mTickLabelList.get(position);
        }

        return "";
    }

    public int getTickCount() {
        return (mValueTo - mValueFrom) / mStepSize + 1;
    }

    private void setTickDrawableBound(Drawable drawable, Rect rect) {
        if (drawable.getIntrinsicWidth() > 0 && drawable.getIntrinsicHeight() > 0) {
            int dx = Math.max(rect.width() - drawable.getIntrinsicWidth(), 0) >> 1;
            int dy = Math.max(rect.height() - drawable.getIntrinsicHeight(), 0) >> 1;
            mRect.set(rect);
            mRect.inset(dx, dy);
            drawable.setBounds(mRect);
        } else {
            drawable.setBounds(rect);
        }
    }

    public void setValue(int value) {
        this.mValue = value;
        invalidate();
    }

    public void setOnDiscreteValueChangedListener(OnDiscreteValueChangedListener listener) {
        this.mOnDiscreteValueChangedListener = listener;
    }
}

Thank you for your support. Please correct any errors. If you need to reprint, please indicate the source of the original text!

Posted by mw-dnb on Fri, 22 Oct 2021 21:48:51 -0700