Preface: why this title? Because the slide button doesn’t look so stiff, haha. Due to the limitation of space, I will not explain all the knowledge points once, but only select some points that need to be paid attention to and some places that are not easy to understand.

rendering

Let me show you a final implementation, so you can look at it and think about how it’s done.

The main content of the lecture

The following contents will be selected for explanation

  • How to make the button move freely
  • A way to deal with transgression
  • How to handle springback (i.e. not sliding to the specified position and returning to the origin)
  • How to disable sliding after sliding to a specified position
  • The method of making text follow a button
  • Custom View add shadow method

I’m going to pick the case where the button starts in the middle, because when the button starts in the left, that’s when the button starts in the middle.

Let the button move with your finger

To make the button move with the finger, you need to override the View’s onTouchEvent method, which listens for several finger movements, such as “down,” “slide,” and “lift,”

Once you capture these actions, you can do something with each action to make the button move with your finger. The specific code is as follows

override fun onTouchEvent(event: MotionEvent): Boolean {
      
        when (event.action) {
            // Press your finger down
            MotionEvent.ACTION_DOWN -> {
                val clickX = event.x
            }

			// Move your finger
            MotionEvent.ACTION_MOVE -> {
                slideX = event.x
                / / refresh the view
                postInvalidate()

            }
			// Raise your finger
            MotionEvent.ACTION_UP -> {
                
            }
        }
        return super.onTouchEvent(event)
    }
Copy the code

To briefly explain the code above, get the coordinates of the phone’s position when the finger is pressed, use the clickX variable to save the X-axis position when the finger is pressed (used later), use the global variable slideX to save the X-axis position when the finger is slid, and then refresh the view. The lifting of the finger is not dealt with here. The above code just gets the X-axis coordinates of the finger movement. Now we will draw the middle button using the View onDraw method. The code is as follows

private fun drawSnake(canvas: Canvas) {
    // Calculate the radius of the circle
        val circleRadius = mSnakeRadius.toFloat() - mShadowRadius / 2
		// Calculate the x-coordinate of the middle button. At the beginning, slideX is 0
        val circleCenter = if (slideX == 0f) {
            if (mSlideState == SlideState.INIT_LEFT) {
                // The x-coordinate of the center of the circle when the button is on the left
                mSnakeRadius + mShadowRadius / 2
            } else {
                // mResultWidth is the width of the View
                mResultWidth / 2.toFloat()

            }
        } else {
            // After the finger swipe
            slideX
        }
		// The X coordinate of the circle is determined by the position of the finger
        canvas.drawCircle(circleCenter, mResultHeight / 2.toFloat(), circleRadius, mSnakePaint)

    }
Copy the code

Each line of code above is commented, so I won’t go through each line here.

A way to deal with transgression

If you press the code above to make the button follow your finger, when you move to the edge you will notice that the moving button is out of the background! So what’s the problem? Because the X coordinate of the moving position of the finger is used as the X coordinate of the center of the circle. When the finger moves to the edge, the X coordinate of the center of the circle is also on the edge, so the button will be out of the background. So how to solve this problem? Simply reassign the slideX variable when the x-coordinate of the finger position is greater than the total length of the View minus the radius of the button. The following code

override fun onTouchEvent(event: MotionEvent): Boolean {
      
        when (event.action) {
            // Press your finger down
            MotionEvent.ACTION_DOWN -> {
                val clickX = event.x
            }

			// Move your finger
            MotionEvent.ACTION_MOVE -> {
                slideX = event.x
                
                if (slideX > mResultWidth - mSnakeRadius - mShadowRadius / 2) {
                    // Prevent crossing the right boundary
                    slideX = mResultWidth - mSnakeRadius-mShadowRadius / 2
                } else if (slideX < mSnakeRadius + mShadowRadius / 2) {
                    // To prevent going beyond the left boundary
                    slideX = mSnakeRadius+mShadowRadius / 2
                }
                / / refresh the view
                postInvalidate()

            }
			// Raise your finger
            MotionEvent.ACTION_UP -> {
                
            }
        }
        return super.onTouchEvent(event)
    }
Copy the code

As you can see from the code above, the way to handle the out-of-bounds of the button is to recalculate the X-axis of the center of the circle instead of directly using the X coordinates of the finger as the center of the circle when the X coordinates of the center of the circle are going to be greater or less than the required coordinates.

Handle the springback

This part deals with whether to return the button to its original position or whether to return the button to its target position by lifting the finger when the button has not moved to its target position. To deal with this problem is also very simple, that is, it is necessary to set a position in advance. When lifting the finger, judge whether the X coordinate of the center of the circle is greater than this position or less than this position. If it is greater than, it will directly move the button to the target position, if it is less than, it will move the button to the initial position. The following figure

The position specified here is shown in the figure above, and the distance from the black background boundary is exactly equal to the radius of the button. When sliding to the left, when the finger is lifted, if the x-coordinate of the center of the circle is greater than 2 times the radius of the button, that is, the diameter, it will return to the initial position, otherwise it will slide to the left, the judgment of sliding to the right is the same. The specific code is as follows

override fun onTouchEvent(event: MotionEvent): Boolean {
      
        when (event.action) {
            // Press your finger down
            MotionEvent.ACTION_DOWN -> {
                val clickX = event.x
            }

			// Move your finger
            MotionEvent.ACTION_MOVE -> {
                slideX = event.x
                
                if (slideX > mResultWidth - mSnakeRadius - mShadowRadius / 2) {
                    // Prevent crossing the right boundary
                    slideX = mResultWidth - mSnakeRadius-mShadowRadius / 2
                } else if (slideX < mSnakeRadius + mShadowRadius / 2) {
                    // To prevent going beyond the left boundary
                    slideX = mSnakeRadius+mShadowRadius / 2
                }
                / / refresh the view
                postInvalidate()

            }
			// Raise your finger
            MotionEvent.ACTION_UP -> {
                // Determine whether to swipe left or right
                if (slideX > center) {
                    // The boundary of the button is beyond the position specified on the right
                        if (slideX > mResultWidth - 2 * mSnakeRadius - mShadowRadius) {
                            // Slide directly to the target
                            slideAnimate(slideX.toInt(), (mResultWidth - mSnakeRadius - mShadowRadius / 2).toInt())
                            mSlideState = SlideState.RIGHT_FINISH
                            if(slideListener ! =null) { slideListener!! .onSlideRightFinish() } }else {
                            setInitText()
                            slideAnimate(slideX.toInt(), center)
                        }

                    } else if (slideX < center) {
                        if (slideX < 2 * mSnakeRadius + mShadowRadius) {
                            // Slide directly to the target
                            slideAnimate(slideX.toInt(), (mSnakeRadius + mShadowRadius / 2).toInt())
                            mSlideState = SlideState.LEFT_FINISH
                            if(slideListener ! =null) { slideListener!! .onSlideLiftFinish() } }else {
                            setInitText()
                            slideAnimate(slideX.toInt(), center)
                        }
                    } else {
                        // Return to the origin
                        slideAnimate(slideX.toInt(), center)
                    }
            }
        }
        return super.onTouchEvent(event)
    }
Copy the code

The slideAnimate method in the above code adds a displacement animation

 private fun slideAnimate(start: Int, end: Int) {
        val valueAnimator = ValueAnimator.ofInt(start, end)
        valueAnimator.addUpdateListener { animation ->
            val animatedValue = animation.animatedValue as Int
            slideX = animatedValue.toFloat()
            postInvalidate()
        }
        valueAnimator.start()
    }
Copy the code

How to disable sliding after sliding to a specified position

What does sliding to a specified position mean? Here is an example of a View where the button is in the middle, for example, when the button slides to both ends, it is in the specified position, and then you lift your finger, and the next time you slide the button, you can’t slide it. This problem is also easy to solve, there are many ways to solve, the method adopted in this paper is to initialize a member variable, each time into the onTouchEvent method, first judge this variable, if the condition is true, directly return false. If not, it will continue to respond to the action of the finger. When the button slides to the specified position, it will change the value of this variable, so that the condition will be valid when entering the method next time. The following code

override fun onTouchEvent(event: MotionEvent): Boolean {
       // Disable sliding after sliding
        if (mSlideState == SlideState.LEFT_FINISH || mSlideState == SlideState.RIGHT_FINISH) {
            return false
        }
        when (event.action) {
            // Press your finger down
            MotionEvent.ACTION_DOWN -> {
                val clickX = event.x
            }

			// Move your finger
            MotionEvent.ACTION_MOVE -> {
                slideX = event.x
                
                if (slideX > mResultWidth - mSnakeRadius - mShadowRadius / 2) {
                    // Prevent crossing the right boundary
                    slideX = mResultWidth - mSnakeRadius-mShadowRadius / 2
                } else if (slideX < mSnakeRadius + mShadowRadius / 2) {
                    // To prevent going beyond the left boundary
                    slideX = mSnakeRadius+mShadowRadius / 2
                }
                / / refresh the view
                postInvalidate()

            }
			// Raise your finger
            MotionEvent.ACTION_UP -> {
                // Determine whether to swipe left or right
                if (slideX > center) {
                    // The boundary of the button is beyond the position specified on the right
                        if (slideX > mResultWidth - 2 * mSnakeRadius - mShadowRadius) {
                            // Slide directly to the target
                            slideAnimate(slideX.toInt(), (mResultWidth - mSnakeRadius - mShadowRadius / 2).toInt())
                            // Change the state of a variable
                            mSlideState = SlideState.RIGHT_FINISH
                            if(slideListener ! =null) { slideListener!! .onSlideRightFinish() } }else {
                            setInitText()
                            slideAnimate(slideX.toInt(), center)
                        }

                    } else if (slideX < center) {
                        if (slideX < 2 * mSnakeRadius + mShadowRadius) {
                            // Slide directly to the target
                            slideAnimate(slideX.toInt(), (mSnakeRadius + mShadowRadius / 2).toInt())
                            // Change the state of a variable
                            mSlideState = SlideState.LEFT_FINISH
                            if(slideListener ! =null) { slideListener!! .onSlideLiftFinish() } }else {
                            setInitText()
                            slideAnimate(slideX.toInt(), center)
                        }
                    } else {
                        // Return to the origin
                        slideAnimate(slideX.toInt(), center)
                    }
            }
        }
        return super.onTouchEvent(event)
    }
Copy the code

Make the text follow the button

This is probably the most complicated part of customizing the View, but it’s not complicated once you figure it out. Here take the button from the middle to the right to explain, the button from the middle to the left to slide the calculation method is similar to the right, first look at the picture

Ok, now we can figure out the X-axis position of the initial and final text based on the picture above,

Initial status X = (mResultWidth /2 – mSnakeRadius) /2

Final state X=mResultWidth / 2

MResultWidth is the width of the View and mSnakeRadius is the radius of the button

Now that we know the initial and final X positions, we can change the X coordinates of the text based on how far the button moves

val distance = end – start

val resultLeftX = start + distance * proportion

ResultLeftX is the X coordinate of the text to be drawn. Proportion is the percentage of proportion. According to the formula, the proportion value ranges from 0 to 1.

Now, the most important point is how to change the PROPORTION in the above announcement according to the movement of the button. Continue to look at the picture

Proportion (X to y). The middle line is the slideX, which changes according to the slide of the button so that the proportion goes from 0 to 1. The formula is as follows

proportion = (slideX – halfResultWidth) / (mResultWidth – mSnakeRadius – halfResultWidth – mShadowRadius / 2)

The specific code for this part is as follows

private fun drawInnerText(canvas: Canvas) {
		// This part is clipping the canvas, because when Viwe adds shadows, the text will cross the boundary. If you want to see the effect, you can comment out the clipping code
        canvas.save()
    // The size of the clipping is the size of the black background and the shape is a rectangle
        canvas.clipRect(
            mShadowRadius,
            mShadowRadius,
            mResultWidth.toFloat() - mShadowRadius,
            mResultHeight.toFloat() - mShadowRadius
        )
        val fontMetrics = mInnerTextPaint.fontMetrics

        // Draw the text inside the ring
        val baseline = mResultHeight / 2 + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom
        // The starting position of the text
        val halfResultWidth = mResultWidth / 2
        val start = (halfResultWidth - mSnakeRadius) / 2
        // The final position of the text
// val end = (mResultWidth - 2 * mSnakeRadius) / 2
        val end = mResultWidth / 2
        / / distance
        val distance = end - start
    // Set the initial position of the button according to the state
        var proportion = if (mSlideState == SlideState.INIT_LEFT) {
            - 1f
        } else {
            0f

        }

        if(slideX ! =0f) {
            // Calculate the scale to move the text distance
            proportion = (slideX - halfResultWidth) / (mResultWidth - mSnakeRadius - halfResultWidth - mShadowRadius / 2)}val resultLeftX = start + distance * proportion
// Draw the text to the left of the button
        canvas.drawText(
            leftContent,
            resultLeftX,
            baseline,
            mInnerTextPaint
        )
        val resultRightX =
            halfResultWidth + (mSnakeRadius / 2) + (halfResultWidth / 2) + (distance * proportion)
    // Draw the text to the right of the button
        canvas.drawText(
            rightContent,
            resultRightX,
            baseline,
            mInnerTextPaint
        )
        canvas.restore()
    }
Copy the code

The following code is highlighted here

private fun drawInnerText(canvas: Canvas) {

        canvas.save()
        canvas.clipRect(
            mShadowRadius,
            mShadowRadius,
            mResultWidth.toFloat() - mShadowRadius,
            mResultHeight.toFloat() - mShadowRadius
        )
       / /... Omit some code
        canvas.restore()
    }
Copy the code

** This code is designed to solve the problem of moving text beyond the background while shading a View. ** When the view adds a shadow, you can comment out this code to see the difference. Another point is to write text in the specified position, we can refer to my article Android custom View of the fixed point to write text.

Custom View add shadow method

The way to add a shadow is to set setShadowLayer for your brush. Let’s look at this method

setShadowLayer(float radius, float dx, float dy, int shadowColor)
Copy the code

There are four parameters in the method, which are used as follows

  • Radius: Sets the blur radius of the shadow, actually sets the size of the shadow, when 0, there is no shadow
  • Dx: the offset of the shadow on the X-axis
  • Dy: the offset of the shadow on the Y axis
  • ShadowColor: The color of the shadow

Note: Setting this only for brushes does not work, and hardware acceleration needs to be turned off.

conclusion

So far, the article has solved all the problems that need to be solved. In fact, in addition to these mentioned in the article, there are some other details that need to be paid attention to, such as sliding only when clicked within the range of the button, the callback when the state changes, etc. These details are processed in the code, you can click here to get the source code, feel good, just click a star!

This article has been published by the public id AndroidShared

Scan the code to pay attention to the public number, reply “get information” have surprise