Use a custom view to draw a seekbar with flexible textures

Is Android’s native seekbar too difficult to use? Customized views will solve your worries! The benefits of customizing seekbar are as follows:

  • You can freely set the foreground, background and slider of the progress bar
  • You can set the aspect ratio of each progress bar
  • Vertical progress bar can be set

Effect

The UI gives the rendering on the left side of the picture below (actually drawn by the author himself), and gives three cutouts on the right side of the picture below, which are the foreground, background, and slider of the progress bar.

How to use these resources to create custom views? By intuitively dividing the renderings, we can get these parts:
Split
First draw the complete background of the progress bar, then intercept and draw the foreground according to the progress, and finally draw the slider at the seam between the progress bar and the foreground.

Interpretation

The progress bar is easy to explain at the data layer. It only requires a range and a current value. The main explanation here is the UI level:
The width and height of the custom view depend on the wider one of each slice. For example, in such a vertical seekbar, the width is the width of the slider, and the length is not the height of the progress bar. The limit position of the slider is as shown in the figure. Show:
Slider position
Through analysis, it can be found that the limit position of the slider is not to allow one edge to coincide with the edge of the progress bar, but to be some distance away. On the one hand, this is because the edge of the cut picture is not necessarily the edge of the image. On the other hand, because the radius of the slider and the radius of the progress bar are different, when sliding to the limit position, the slider will definitely be more than the limit of the progress bar. Here you need to define some values:

Progress bar width and height and slider radius The most basic, if you want to draw them up, you need these values to determine the proportion.
The position where the background of the progress bar is drawn Use the slider to determine the coordinate 0.0 in the upper right corner of the upper limit position, and the coordinate where the background of the progress bar actually starts to be drawn.
The actual progress range of the progress bar The two “dots” in the above picture are the actual limit progress shown by the progress bar.

The width, height and slider radius (including their original images) are entered through attr, and the others need to be calculated.

Code

Width and height settings

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {<!-- -->
        //super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        widthMeasureSpec = View.MeasureSpec.getSize(widthMeasureSpec);//dip2px(context,MeasureSpec.getSize(widthMeasureSpec));
        heightMeasureSpec = MeasureSpec.getSize(heightMeasureSpec);//dip2px(context,MeasureSpec.getSize(heightMeasureSpec));
        mThumbRadius = widthMeasureSpec / 2;
        mProgressWidth = mThumbRadius * oriWidth / oriR;
        mProgressHeight = mProgressWidth * oriHeight / oriWidth;
        if (heightMeasureSpec < mProgressHeight) {<!-- -->
            mProgressHeight = heightMeasureSpec - 2 * mThumbRadius;
        }
        mLeft = mThumbRadius - mProgressWidth / 2;
        mTop = mLeft;

        //Rectangular progress bar
        //mMinY = mThumbRadius;
        //mMaxY = mProgressHeight + mThumbRadius;
        //elliptical progress bar
        mMinY = mThumbRadius;
        mMaxY = mProgressHeight-mProgressWidth + mThumbRadius;
        //LogUtil.d("measuring..." + rawMax + " " + progress);
        if (calProgress() != progress) {<!-- -->
            //Calculate whether progress has changed. If it has changed (changed before mMaxX is defined), reset progress.
            updateProgress();
            LogUtil.v("measured progress changed: " + progress);

        }
        //LogUtil.d("measurement results","measured progress changed: " + progress);
        setMeasuredDimension(2 * mThumbRadius, mProgressHeight + 2 * mTop);

    }

After obtaining the approximate area of the view, first set the slider width equal to the width of the area, and then calculate other values based on the ratio.
Sometimes measurements need to be taken many times to get an accurate measurement, so the true progress value is determined repeatedly.
Finally, the calculated width and height are used for subsequent calculations.

Reading and writing of progress

Since this is a widget used for drawing, the author sets the progress reading as an abstract method. Subclasses that inherit this class need to override the initialization method and progress change monitoring.

 protected abstract void setOnSeekbarChanged(int progress, int maxProgress);

    protected abstract void setDefaultPreference();

Rewrite the listening event of clicking or dragging the progress bar. It should be noted that if the drag exceeds the limit of the progress bar, the progress needs to be limited.

 @Override
    public boolean onTouchEvent(MotionEvent event) {<!-- -->

        float y = event.getY();

        //Cannot cross the boundary
        if (y < mMinY) {<!-- -->
            y = mMinY;
        } else if (y > mMaxY) {<!-- -->
            y = mMaxY;
        }

        // Pass it to the corresponding place

        //The difference between this and the horizontal direction is that its progress is the following section
        setOnSeekbarChanged(mMaxY - (int) y, mMaxY - mMinY);

        return true;
    }

After receiving the click event, the setOnSeekbarChanged overridden by the subclass will be triggered. For example, setting the volume can be done in the corresponding method.

Draw

The drawing method requires a progress, which is a value obtained by initialization or subsequent click monitoring. We convert it into the effective length of the progress bar.

 public void setRawProgress(int rawProgress, int rawMax) {<!-- -->
        if (rawProgress > rawMax) {<!-- -->
            LogUtil.w("calculated error! " + rawProgress + ">" + rawMax + ", refuse to draw.");
            return;
        }

        this.rawMax = rawMax;
        this.rawProgress = rawProgress;
        updateProgress();
        LogUtil.i("draw progress : " + rawProgress + " / " + rawMax);
        invalidate();
    }

    private int calProgress() {<!-- -->
        //It is possible to get the value here when mMax-mMin=0, so progress will be recalled at the place of measurement
        int calProgress = rawProgress * (mMaxY - mMinY) / rawMax;
        //Prevent calculation out-of-bounds errors
        if (calProgress >= mMaxY - mMinY){<!-- -->
            calProgress = mMaxY - mMinY - 1;
        } else if (calProgress<=0) {<!-- -->
            calProgress=1;
        }
        return calProgress;
    }

After calling invalidate, start drawing. Here you need to scale the bitmap of the resource file. I personally tested that the image created using createScaledBitmap() will be much blurrier than the direct imageview, so I adopted the scaling method of the online boss.

 private void drawProgress(Canvas canvas) {<!-- -->
        if(bitmapBackground==null){<!-- -->
            bitmapBackground = getResizerBitmap(bitmapOriginBackground, mProgressWidth, mProgressHeight);
            bitmapWholeProgress = getResizerBitmap(bitmapOriginProgress, mProgressWidth, mProgressHeight);
            bitmapThumb = getResizerBitmap(bitmapOriginThumb, 2 * mThumbRadius, 2 * mThumbRadius);
        }
// bitmapBackground = Bitmap.createScaledBitmap(bitmapOriginBackground, mProgressWidth, mProgressHeight, false);
// bitmapWholeProgress = Bitmap.createScaledBitmap(bitmapOriginProgress, mProgressWidth, mProgressHeight, false);
// bitmapThumb = Bitmap.createScaledBitmap(bitmapOriginThumb, 2 * mThumbRadius, 2 * mThumbRadius, false);

        LogUtil.v("Measurement view: " + progress + "/(" + mMaxY + "-" + mMinY + ") Length: " + mProgressHeight + " Width: " + mProgressWidth) ;
        //background
        canvas.drawBitmap(bitmapBackground, mLeft, mTop, paint);
        //The center point of the drawing, which is the Y mentioned above
        int touchY = mMaxY - progress;
        //progress
        if (touchY > mTop & amp; & amp; touchY < mProgressHeight + mTop) {<!-- -->
            Rect srcRect = new Rect(0, touchY-mTop, mProgressWidth, mProgressHeight);
            Rect dstRect = new Rect(mLeft, touchY, mLeft + mProgressWidth, mTop + mProgressHeight);
            canvas.drawBitmap(bitmapWholeProgress, srcRect, dstRect, paint);
        }

        //thumb
        canvas.drawBitmap(bitmapThumb, 0, touchY-mThumbRadius, paint);
    }

    public static Bitmap getResizerBitmap(Bitmap bitmap,int newWidth,int newHeight) {<!-- -->
        Bitmap scaledBitmap = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);

        float ratioX = newWidth / (float) bitmap.getWidth();
        float ratioY = newHeight / (float) bitmap.getHeight();
        float middleX = newWidth / 2.0f;
        float middleY = newHeight / 2.0f;

        Matrix scaleMatrix = new Matrix();
        scaleMatrix.setScale(ratioX, ratioY, middleX, middleY);

        Canvas canvas = new Canvas(scaledBitmap);
        canvas.setMatrix(scaleMatrix);
        canvas.drawBitmap(bitmap, middleX - bitmap.getWidth() / 2f, middleY - bitmap.getHeight() / 2f, new Paint(Paint.FILTER_BITMAP_FLAG));

        return scaledBitmap;

    }

The steps to draw the foreground interception of the progress bar here are: first, srcRect draws the part of the original image that should be intercepted, and then dstRect indicates in which coordinates the intercepted image should be drawn, and calls the corresponding method to draw it.

Appendix

The complete code is as follows. First define the parameters to be used in attr:

<declare-styleable name="SeekbarVerticalWithThumb">
        <!--Customized progress bar resources-->
        <attr name="src_bk" format="reference"/>
        <attr name="src_front" format="reference"/>
        <attr name="src_thumb" format="reference"/>
        <attr name="thumb_r" format="integer"/>
        <attr name="progress_width" format="integer"/>
        <attr name="progress_height" format="integer"/>
    </declare-styleable>

Then put SeekBarVerticalWithThumb in the widget: LogUtil manages LOG and can be changed to Log to view the output normally.

package com.demo.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import com.demo.R;
import com.demo.util.LogUtil;

public abstract class SeekbarVerticalWithThumb extends View {

    private Context context;


    private int progress = 0;
    private int rawMax = 1, rawProgress = 0;
    //The width and height of the progress bar
    private int mProgressWidth, mProgressHeight;
    private int mThumbRadius;

    //Progress bar positioning
    private int mLeft, mTop;
    //The range that the slider can be dragged
    private int mMinY, mMaxY;

    Paint paint;
    Bitmap bitmapProgress, bitmapBackground, bitmapWholeProgress, bitmapThumb;
    Bitmap bitmapOriginProgress, bitmapOriginBackground, bitmapOriginThumb;

    public SeekbarVerticalWithThumb(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;

        init(attrs);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {<!-- -->

        float y = event.getY();

        //Cannot cross the boundary
        if (y < mMinY) {<!-- -->
            y = mMinY;
        } else if (y > mMaxY) {<!-- -->
            y = mMaxY;
        }

        // Pass it to the corresponding place

        //The difference between this and the horizontal direction is that its progress is the following section
        setOnSeekbarChanged(mMaxY - (int) y, mMaxY - mMinY);

        return true;
    }


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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        widthMeasureSpec = View.MeasureSpec.getSize(widthMeasureSpec);//dip2px(context,MeasureSpec.getSize(widthMeasureSpec));
        heightMeasureSpec = MeasureSpec.getSize(heightMeasureSpec);//dip2px(context,MeasureSpec.getSize(heightMeasureSpec));
        mThumbRadius = widthMeasureSpec / 2;
        mProgressWidth = mThumbRadius * oriWidth / oriR;
        mProgressHeight = mProgressWidth * oriHeight / oriWidth;
        if (heightMeasureSpec < mProgressHeight) {
            mProgressHeight = heightMeasureSpec - 2 * mThumbRadius;
        }
        mLeft = mThumbRadius - mProgressWidth / 2;
        mTop = mLeft;

        //Rectangular progress bar
        //mMinY = mThumbRadius;
        //mMaxY = mProgressHeight + mThumbRadius;
        //elliptical progress bar
        mMinY = mThumbRadius;
        mMaxY = mProgressHeight-mProgressWidth + mThumbRadius;
        //LogUtil.d("measuring..." + rawMax + " " + progress);
        if (calProgress() != progress) {
            //Calculate whether progress has changed. If it has changed (changed before mMaxX is defined), reset progress.
            updateProgress();
            LogUtil.v("measured progress changed: " + progress);

        }
\t\t
        //LogUtil.d("measurement results","measured progress changed: " + progress);
        setMeasuredDimension(2 * mThumbRadius, mProgressHeight + 2 * mTop);

    }


    public void setRawProgress(int rawProgress, int rawMax) {<!-- -->
        if (rawProgress > rawMax) {<!-- -->
            LogUtil.w("calculated error! " + rawProgress + ">" + rawMax + ", refuse to draw.");
            return;
        }

        this.rawMax = rawMax;
        this.rawProgress = rawProgress;
        updateProgress();
        LogUtil.i("draw progress : " + rawProgress + " / " + rawMax);
        invalidate();
    }

    private int calProgress() {<!-- -->
        //It is possible to get the value here when mMax-mMin=0, so progress will be recalled at the place of measurement
        int calProgress = rawProgress * (mMaxY - mMinY) / rawMax;
        //Prevent calculation out-of-bounds errors
        if (calProgress >= mMaxY - mMinY){<!-- -->
            calProgress = mMaxY - mMinY - 1;
        } else if (calProgress<=0) {<!-- -->
            calProgress=1;
        }
        return calProgress;
    }

    private void updateProgress() {
        progress = calProgress();
    }

    protected abstract void setOnSeekbarChanged(int progress, int maxProgress);

    protected abstract void setDefaultPreference();


    int oriR,oriWidth,oriHeight;
    private void init(AttributeSet attrs) {
        paint = new Paint();
        paint.setAntiAlias(true);
        //Assume it is a view with thumb wider than progress
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SeekbarVerticalWithThumb);
        int bkId = typedArray.getResourceId(R.styleable.SeekbarVerticalWithThumb_src_bk,0);
        int frontId = typedArray.getResourceId(R.styleable.SeekbarVerticalWithThumb_src_front,0);
        int thumbId = typedArray.getResourceId(R.styleable.SeekbarVerticalWithThumb_src_thumb,0);
        oriR = typedArray.getInt(R.styleable.SeekbarVerticalWithThumb_thumb_r,0);
        oriWidth= typedArray.getInt(R.styleable.SeekbarVerticalWithThumb_progress_width,0);
        oriHeight= typedArray.getInt(R.styleable.SeekbarVerticalWithThumb_progress_height,0);

        Drawable drawableBackground = context.getDrawable(bkId);
        Drawable drawableProgress = context.getDrawable(frontId);
        Drawable drawableThumb=context.getDrawable(thumbId);
        bitmapOriginBackground = ((BitmapDrawable) drawableBackground).getBitmap();
        bitmapOriginProgress = ((BitmapDrawable) drawableProgress).getBitmap();
        bitmapOriginThumb = ((BitmapDrawable) drawableThumb).getBitmap();
        typedArray.recycle();
    }

    private void drawProgress(Canvas canvas) {<!-- -->
        if(bitmapBackground==null){<!-- -->
            bitmapBackground = getResizerBitmap(bitmapOriginBackground, mProgressWidth, mProgressHeight);
            bitmapWholeProgress = getResizerBitmap(bitmapOriginProgress, mProgressWidth, mProgressHeight);
            bitmapThumb = getResizerBitmap(bitmapOriginThumb, 2 * mThumbRadius, 2 * mThumbRadius);
        }
// bitmapBackground = Bitmap.createScaledBitmap(bitmapOriginBackground, mProgressWidth, mProgressHeight, false);
// bitmapWholeProgress = Bitmap.createScaledBitmap(bitmapOriginProgress, mProgressWidth, mProgressHeight, false);
// bitmapThumb = Bitmap.createScaledBitmap(bitmapOriginThumb, 2 * mThumbRadius, 2 * mThumbRadius, false);

        LogUtil.v("Measurement view: " + progress + "/(" + mMaxY + "-" + mMinY + ") Length: " + mProgressHeight + " Width: " + mProgressWidth) ;
        //background
        canvas.drawBitmap(bitmapBackground, mLeft, mTop, paint);
        //The center point of the drawing, which is the Y mentioned above
        int touchY = mMaxY - progress;
        //progress
        if (touchY > mTop & amp; & amp; touchY < mProgressHeight + mTop) {<!-- -->
            Rect srcRect = new Rect(0, touchY-mTop, mProgressWidth, mProgressHeight);
            Rect dstRect = new Rect(mLeft, touchY, mLeft + mProgressWidth, mTop + mProgressHeight);
            canvas.drawBitmap(bitmapWholeProgress, srcRect, dstRect, paint);
        }

        //thumb
        canvas.drawBitmap(bitmapThumb, 0, touchY-mThumbRadius, paint);
    }

    public static Bitmap getResizerBitmap(Bitmap bitmap,int newWidth,int newHeight) {<!-- -->
        Bitmap scaledBitmap = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);

        float ratioX = newWidth / (float) bitmap.getWidth();
        float ratioY = newHeight / (float) bitmap.getHeight();
        float middleX = newWidth / 2.0f;
        float middleY = newHeight / 2.0f;

        Matrix scaleMatrix = new Matrix();
        scaleMatrix.setScale(ratioX, ratioY, middleX, middleY);

        Canvas canvas = new Canvas(scaledBitmap);
        canvas.setMatrix(scaleMatrix);
        canvas.drawBitmap(bitmap, middleX - bitmap.getWidth() / 2f, middleY - bitmap.getHeight() / 2f, new Paint(Paint.FILTER_BITMAP_FLAG));

        return scaledBitmap;

    }
}

Happy using it