Android implements a draggable progress bar with a coordinate ruler

After getting the UI effect picture above, my first impression is that it is too simple to implement. SeekBar can be done easily. Change the thumb, add a gradient, and it’s done. Just do it, do it. I’m depressed when I’m doing it, and the bottom coordinate ruler can still be used. After equal proportion division, just set a few more TextViews under the SeekBar. What about the equal proportion small division line in the middle? Moreover, it needs to be available before and after sliding, and a small distance should be left for the left and right dividing lines. The gradient color should be displayed along with the sliding distance instead of the entire width. SeekBar is difficult to satisfy under various conditions. This demand, how to do? Can only be customized.

Still in accordance with the usual practice, make a rough outline:

1. Analyze the elements and determine the implementation plan

2. Analyze the main code

3. Open source address and usage

4. Summary

1. Analyze the elements and determine the implementation plan

Canvas draws such a draggable coordinate ruler, which can basically be split into four parts. The first part is the background and the default discrete interval, the second part is the moving background and the discrete interval, and the third part is the moving picture. thumb, the last part is the bottom text coordinates.

The four parts are basically drawn, but in addition to drawing, other factors need to be considered, such as height, such as finger movement events, etc.

1. Set the default height

The reason for setting the default height is to allow the View to better display a suitable size, so that it will not be displayed when wrap_content is set. The specific settings can be controlled according to the currently set mode. There are three modes. This is the previous one. It was introduced in the article, so I won’t go into details here. When the control sets wrap_content, the mode at this time is MeasureSpec.AT_MOST. In this mode, we need to give a default height.

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        var windowHeight = heightMeasureSpec
        if (heightMode == MeasureSpec.AT_MOST) {
            windowHeight = mDefaultHeight.toInt()//默认的高度
        }
        setMeasuredDimension(widthMeasureSpec, windowHeight)
    }

2. Drag event

To achieve the drag effect, we need to monitor the user's finger movement event, that is, we need to rewrite the onTouchEvent method in the custom View. In this method, we need to do corresponding processing for the finger's pressing, lifting, and moving .

In onTouchEvent, I did the following processing. One is to return directly without executing the event consumption. The purpose is to make the custom View realize static display and dynamic display. It is controlled by a variable mProgressIsIntercept; the second is to solve the problem with For the sliding conflict event of the parent View, when there is a horizontal or vertical sliding event, there will inevitably be a conflict when dragging, so you need to notify the parent View not to consume the event, that is, execute the requestDisallowInterceptTouchEvent method.

All the dragging effects are realized in the move event, which constantly changes the coordinates to update the UI. mMoveProgress is the coordinates of the finger movement.

onTouchEvent(event: MotionEvent?): Boolean {
        super.onTouchEvent(event)
        //如果为true直接返回,不进行拖拽
        if (mProgressIsIntercept) {
            return mProgressIsIntercept
        }
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                parent.requestDisallowInterceptTouchEvent(mDisallowIntercept)
                val downX = getChangeX(event.x)
                val startX = mMoveOldX - mProgressMarginLeftRight
                val endX = mMoveOldX + mProgressMarginLeftRight
                return downX in startX..endX
            }
            MotionEvent.ACTION_MOVE -> {
                //移动
                var moveX = getChangeX(event.x)
                //滑动至最右边
                //计算最后边的坐标
                val viewWidth = getViewWidth()

                if (moveX >= viewWidth) {
                    moveX = viewWidth
                }

                mMoveProgress = moveX

                invalidate()
            }
            MotionEvent.ACTION_UP -> {
                //手指谈起
                mMoveOldX = getChangeX(event.x)

                val viewWidth = getViewWidth()

                if (mMoveOldX >= viewWidth) {
                    mMoveOldX = viewWidth
                }
            }
        }
        return true
    }

Second, the main code for analysis

1. Draw the background

There is nothing to say about the background. It is a simple rounded rectangle, which can be drawn with drawRoundRect. What needs to be determined is the spacing between the upper left and the lower right.

 /**
     * AUTHOR:AbnerMing
     * INTRODUCE:绘制背景
     */
    private fun canvasBackground(canvas: Canvas) {
        mPaint!!.color = mProgressBackground
        val rect = RectF().apply {
            left = mProgressMarginLeftRight
            top = mProgressMarginTopBottom
            right = width.toFloat() - mProgressMarginLeftRight
            bottom = mProgressHeight + mProgressMarginTopBottom
        }
        canvas.drawRoundRect(rect, mProgressRadius, mProgressRadius, mPaint!!)
    }

2. Draw discrete intervals

For discrete intervals, you need to determine the number of intervals, and then dynamically calculate the position of each interval according to the number of intervals. You can use drawLine to draw a small vertical line. The vertical line also needs to determine the distance from top to bottom and its own width; in special cases Next, discrete intervals, the color is different before and after sliding, so here is also a judgment to dynamically change the color.

 /**
     * AUTHOR:AbnerMing
     * INTRODUCE:绘制离散间隔
     */
    private fun canvasIntervalLine(canvas: Canvas, isCanvas: Boolean) {
        val rect =
            (width - mProgressMarginLeftRight * 2 - mIntervalParentLeftRight * 2) / mIntervalSize
        if (isCanvas) {
            mPaint!!.color = mIntervalSelectColor
        } else {
            mPaint!!.color = mIntervalColor
        }

        mPaint!!.strokeWidth = mIntervalWidth
        for (a in 0..mIntervalSize) {
            val x = (rect * a) + mProgressMarginLeftRight + mIntervalParentLeftRight
            val y = mIntervalMarginTopBottom + mProgressMarginTopBottom
            canvas.drawLine(
                x,
                y,
                x,
                mProgressHeight + mProgressMarginTopBottom - mIntervalMarginTopBottom,
                mPaint!!
            )
        }
    }

3. Draw the moving thumb

Regarding the thumb, the first thing to determine is the size. If the width and height are set, then you need to use Bitmap to reset the height and change the coordinates of the thumb. You only need to continuously change the left coordinate point of the picture, that is, through the above-mentioned Move event Set the moving coordinates in .

 /**
     * AUTHOR:AbnerMing
     * INTRODUCE:绘制移动的图标
     */
    private fun canvasMoveIcon(canvas: Canvas) {
        mProgressThumb?.let {
            var decodeResource = BitmapFactory.decodeResource(resources, it)
            mProgressThumbWidth = decodeResource.width
            if (mThumbWidth != 0f) {
                val height: Int = decodeResource.height
                // 设置想要的大小
                val newWidth = mThumbWidth
                val newHeight = mThumbHeight
                // 计算缩放比例
                val scaleWidth = newWidth / width
                val scaleHeight = newHeight / height
                // 取得想要缩放的matrix参数
                val matrix = Matrix()
                matrix.postScale(scaleWidth, scaleHeight)
                // 得到新的图片
                decodeResource =
                    Bitmap.createBitmap(decodeResource, 0, 0, width, height, matrix, true)

            }

            var mThumpLeft = mMoveProgress
            if (mThumpLeft < (mProgressThumbWidth / 2 - mIntervalParentLeftRight + mProgressThumbSpacing)) {
                mThumpLeft =
                    mProgressThumbWidth / 2 - mIntervalParentLeftRight + mProgressThumbSpacing
            }

            if (mThumpLeft > (getViewWidth() - mIntervalParentLeftRight + mProgressThumbSpacing)) {
                mThumpLeft = getViewWidth() - mIntervalParentLeftRight + mProgressThumbSpacing
            }

            canvas.drawBitmap(
                decodeResource, mThumpLeft, mThumbMarginTop, mIconPaint!!
            )
        }
    }

4. Draw the progress of the movement

The progress of the movement is the same as the drawing of the background, except that it needs to move the distance bit by bit according to the coordinates of the finger, that is, to constantly change the coordinate value on the right. Similarly, it is also dynamic through the progress of mMoveProgress in the Move event. calculation. The gradient of the progress is relatively simple, using the shader property of the brush, and currently using the horizontal linear gradient LinearGradient.

/**
     * AUTHOR:AbnerMing
     * INTRODUCE:绘制进度
     */
    private fun canvasMoveProgress(canvas: Canvas) {
        //为空
        if (mColorArray.isEmpty()) {
            mColorArray = intArrayOf(
                ContextCompat.getColor(context, R.color.text_ff3e3e93),
                ContextCompat.getColor(context, R.color.text_ff8548d2),
            )
        }
        val linearShader = LinearGradient(
            0f,
            0f,
            mMoveProgress + mProgressMarginLeftRight,
            mProgressHeight,
            mColorArray,
            floatArrayOf(0f, 1f),
            Shader.TileMode.CLAMP
        )
        mProgressPaint!!.shader = linearShader


        //等于0时
        val rect = RectF()
        rect.left = mProgressMarginLeftRight
        rect.top = mProgressMarginTopBottom
        rect.right = mMoveProgress + mProgressMarginLeftRight
        rect.bottom = mProgressHeight + mProgressMarginTopBottom
        canvas.drawRoundRect(rect, mProgressRadius, mProgressRadius, mProgressPaint!!)


        //计算比例
          
        mGraduationResult =
            ((mMoveProgress / getViewWidth()) * mMaxProgress).roundToInt()//(endProgress * mMaxProgress).roundToInt()

        if (mGraduationResult < 1) {
            mGraduationResult = if (mGraduationSectionZero) {
                0
            } else {
                1
            }
        }
        if (mGraduationResult >= mMaxProgress) {
            mGraduationResult = mMaxProgress
        }

        mMoveProgressCallback?.invoke(mGraduationResult)

    }

5. Draw text scale

In fact, you can find that the discrete interval and the coordinate text scale at the bottom are actually in one-to-one correspondence. Since they are related to each other, we can put them together directly, that is, when traversing the discrete interval, we directly draw the coordinate scale at the bottom scale.

Coordinate scale, there are four effects, the first is no scale value, the second is only the start and end scale value, the third is to display all scale values, and the fourth is whether the scale value starts from 0 or 1 .

mIsGraduation is a variable used to determine whether a scale value is required. If it is true, it needs to be drawn, otherwise it will not be drawn, that is, no scale value is required. mHideGraduationSectionCenter is a variable that hides the middle scale. It is true to hide, otherwise it is not hidden. The specific code is as follows:

 /**
     * AUTHOR:AbnerMing
     * INTRODUCE:绘制离散间隔
     */
    private fun canvasIntervalLine(canvas: Canvas, isCanvas: Boolean) {
        val rect =
            (width - mProgressMarginLeftRight * 2 - mIntervalParentLeftRight * 2) / mIntervalSize
        if (isCanvas) {
            mPaint!!.color = mIntervalSelectColor
        } else {
            mPaint!!.color = mIntervalColor
        }

        mPaint!!.strokeWidth = mIntervalWidth
        for (a in 0..mIntervalSize) {
            val x = (rect * a) + mProgressMarginLeftRight + mIntervalParentLeftRight
            val y = mIntervalMarginTopBottom + mProgressMarginTopBottom
            canvas.drawLine(
                x,
                y,
                x,
                mProgressHeight + mProgressMarginTopBottom - mIntervalMarginTopBottom,
                mPaint!!
            )
            //绘制刻度值
            if (mIsGraduation && isCanvas) {

                if (mHideGraduationSectionCenter && (a != 0 && a != mIntervalSize)) {
                    //隐藏中间
                    continue
                }

                var graduation = a * mGraduationSection
                //是否从0开始记录
                if (graduation == 0 && !mGraduationSectionZero) {
                    graduation = 1
                }

                //如果移动到了,改变颜色


                if (mGraduationResult >= graduation && mGraduationResult < graduation + mGraduationSection) {
                    mGraduationPaint?.color = mGraduationSelectTextColor
                } else {
                    mGraduationPaint?.color = mGraduationTextColor
                }


                val text = graduation.toString()
                val rectText = Rect()
                mGraduationPaint!!.getTextBounds(text, 0, text.length, rectText)
                val textWidth = rectText.width()
                val textHeight = rectText.height()
                canvas.drawText(
                    text,
                    x - textWidth / 2,
                    mProgressHeight + mProgressMarginTopBottom * 2 + textHeight + mGraduationMarginTop,
                    mGraduationPaint!!
                )

            }

        }
    }

3. Open source address and usage

At present, it has been uploaded to Github. It is a simple class itself. There are not many things. If you need iron, you can directly view the source code.

Address: https://github.com/AbnerMing888/MoveProgress

If you are too lazy to download the source code and want to use it directly, there is no problem. I have uploaded it to the remote Maven, and you can rely on it.

1. Under the build.gradle file under your root project, import maven.

allprojects {
    repositories {
        maven { url "https://gitee.com/AbnerAndroid/almighty/raw/master" }
    }
}

2. Introduce dependencies under the build.gradle file in the Module you need to use.

dependencies {
    implementation 'com.vip:moveprogress:1.0.0'
}

3. Just import XML

 <com.vip.moveprogress.MoveProgress
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:ms_graduation_hide_center="true" />

related attributes

Attributes

type

overview

ms_height

dimension

The height of the View view

ms_progress_height

dimension

the height of the progress bar

ms_progress_thumb

reference

The icon of the progress bar

ms_progress_margin_top_bottom

dimension

The up and down distance of the progress bar from the icon

ms_progress_margin_left_right

dimension

The progress bar distance from the left and right margins

ms_progress_radius

dimension

Rounded corners of the progress bar

ms_progress_background

color

The background color of the progress bar

ms_interval_color

color

Spacer color

ms_interval_select_color

color

Interval selection color

ms_interval_parent_margin_left_right

dimension

Spacer distance from parent left and right

ms_interval_size

integer

Number of interval lines

ms_interval_width

dimension

Spacer Width

ms_interval_margin_top_bottom

dimension

Spacer top and bottom margins

ms_progress_move_color

reference

defined mobile color

ms_progress_max

integer

maximum progress

ms_progress_default

integer

default progress

ms_is_graduation

boolean

Whether to display the scale

ms_graduation_text_size

dimension

scale text size

ms_graduation_text_color

color

scale text color

ms_graduation_select_text_color

color

Scale text selection color

ms_graduation_section

integer

Scale value segment

ms_graduation_section_zero

boolean

Scale value segments start at zero

ms_graduation_hide_center

boolean

Whether to hide the middle of the scale value segment

ms_graduation_margin_top

dimension

The distance from the scale value to the upper edge

ms_progress_thumb_width

dimension

The width of the icon

ms_progress_thumb_height

dimension

The height of the icon

ms_progress_thumb_margin_top

dimension

The height of the icon from the top

ms_progress_thumb_spacing

dimension

icon padding

ms_progress_disallow_intercept

boolean

Whether to intercept

ms_progress_is_intercept

boolean

Whether dragging is prohibited

related method

method

parameter

overview

getProgress

No reference

return current progress

changeProgress

Int

change current progress

getMoveProgress

returns Int

Callback

setProgressIsIntercept

Boolean

Set whether to intercept

Four. Summary

Regarding the gradient, it should be noted that the range of the gradient is not the default fixed distance from left to right, but the distance from left to finger sliding. This needs to be noted, that is, when setting the gradient, the X coordinate of the termination needs to be according to the gesture Dynamic settings on the left.

From this simple drag and drop progress bar, we can understand that canvas drawing lines, rounded rectangles, pictures and related knowledge points combined with gestures are not difficult in themselves.

Guess you like

Origin blog.csdn.net/ming_147/article/details/131251506