First, gestures bring better experience to line charts

1. Left and right sliding line chart.

Good customization depends on gestures. You’ve all seen the K-line map and witnessed the evolution of several tech groups into everyday stock sharing groups.

Look at the stock currency friends should be very familiar with. You can see that not only can you move left and right but you can also zoom. The interactivity is so cool. So let’s take it one step at a time. Gestural sliding is crucial but not beyond the analytical skills of junior high school students, so we can do it. Let’s start with a quick example. We set the position of the circle in the middle of the screen by measuring the gesture sliding distance:

First create a simple class to draw a circle and override the onTouchEvent event:

class LHC_Scroll_distance_View @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyle) {
    //
    var viewxToY = 60f
     var maxXInit=0f
    override fun onDraw(canvas: Canvas) {
        maxXInit= measuredWidth.toFloat()
        drawCircle(canvas)
    }

    private fun drawCircle(canvas: Canvas) {
        val linePaint = Paint()
        linePaint.isAntiAlias = true
        linePaint.strokeWidth = 20f
        linePaint.strokeCap = Paint.Cap.ROUND
        linePaint.color = Color.RED
        linePaint.style = Paint.Style.FILL

       
        canvas.drawCircle(viewxToY, (measuredHeight/2).toFloat(), 60f, linePaint)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
            
            }
            MotionEvent.ACTION_MOVE -> {
               
            }
      
        }
        return true
    }
}

Copy the code

After running

OnTouchEvent can be calculated based on on-screen MotionEvent information… If you’re not sure, check out my other article or some other good article about the click event distribution process for views and Viewgroups.

1.MotionEvent.ACTION_DOWN literally knows that the callback is triggered when the down action is triggered. 2. Motionevent. ACTION_MOVE returns to pressing and lifting all intermediate points. /** * Constant for {@link #getActionMasked}: A change has happened during a * press gesture (between {@link #ACTION_DOWN} and {@link #ACTION_UP}). * The motion contains the most recent point, as well as any intermediate * points since the last down or move event. */ public static final int ACTION_MOVE = 2;Copy the code

Press, slide, lift and so on will receive the screen click MotionEvent(location, time… Details), the location of the pressed point on the screen can be easily obtained. So how do you get the distance of each swipe by swiping left and right?

ACTION_MOVE event of type onTouchEvent is constantly called back between the press and the lift, so we constantly receive on-screen MotionEvent messages under MotionEvent.ACTION_MOVE. Event. x is constantly changing, and the corresponding circle should also be constantly shifting in sync with subtle changes. So you can swipe left and right.

MeasureHeight /2). The slide process ensures that the viewxToY will refresh as the slide changes, and the circle will shift!

As shown in the figure above, there are actually continuous motionEvent.action_move notifications between DOWN and UP. We use event.x- the last recorded event.x() as the distance segment of each sliding notification. All the slippage segments add up to the slippage distance… Make yourself clear.

  override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> startX = event.x
            MotionEvent.ACTION_MOVE -> {
                // Each notification counts a bit of sliding
                val dis = event.x - startX
                // Event. x, which records the end of the move, is where the next slide starts
                startX = event.x
                // Add each slide segment to the current distance
                viewxToY=viewxToY+dis
                // Refresh the View
                invalidate()
            }
        }
        return true
    }

Copy the code

Can’t wait to see what happens.

So we see basically sliding syncing, basically no big problem. So let’s go through a wave of our code and look at the line diagram. Let’s do a simple shift of the X-axis. Take your time… HMM.. Originally engaged in a random color of embarrassment 😅.

    // Record the sliding distance
    private var viewxToY=0f
    
     // Draw the X-axis
    val pathx = Path()
    // Gesture sliding distance plus
    pathx.moveTo(0f+viewxToY, 0f)
    pathx.lineTo(measuredWidth - 20f+viewxToY, 0f)
    
     var  startX=0f
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (event.action == MotionEvent.ACTION_DOWN) {
            clickDow = true
            startX=event.x
            return true
        }
        if (event.action==MotionEvent.ACTION_MOVE){
            // Each notification counts a bit of sliding
            val dis = event.x - startX
            // Event. x, which records the end of the move, is where the next slide starts
            startX = event.x
            // Add each slide segment to the current distance
            viewxToY += dis
            invalidate()
        }
        ...
        


Copy the code

Let’s change the coordinates of the font below the x axis.

 // Draw the background
 // Left +right +viewxToY for both background and seat
 canvas.drawRoundRect(-getTextWidth(xtitle_paint, "${index + 2}Month. "") / 2+viewxToY, -getTextHeight(xtitle_paint) / 2, getTextWidth(xtitle_paint, "${index + 2}Month. "") / 2+viewxToY, getTextHeight(xtitle_paint) / 2.10f.10f, getTextBackgroudPaint(40.5.0))
            // Draw text
 canvas.drawText("${index + 2}Month. "".0."${index + 2}Month. "".length, -getTextWidth(xtitle_paint, "${index + 2}Month. "") / 2+viewxToY, getTextHeight(xtitle_paint) / 3, xtitle_paint)
       

Copy the code

\

Here we have implemented the calculation of horizontal relative sliding position with gesture. But it looks awkward…. Notice that the slide is arbitrary. But we shouldn’t slide just for the sake of sliding. It is necessary to slide off the X-axis. So you need to do the limit in the gesture slide. Let’s continue our analysis:

  • At the beginning of polyline drawing, we did not combine business or formally plan every detail. Many of the scenes and requirements of the line chart presentation can not be fully represented within a screen width, and we did not take this into account in the beginning, I think this is easy for you, horizontal we continue to add months and coordinates, horizontal x axis length we passWidth per cell * number of coordinates -1That’s equal to all the widths to plot the X-axis.
1.We have to know the maximum and minimum distance that the X axis can be shifted left and right.2.Because the X-axis is transformed, the maximum sliding distance is minXInit=0, means that you can't slide to the right of the dot.3.And our X-axis is out of the width of the screen so we want to see something on the right of the screen = the width of the X-axis - the width of the screen. I don't know if I said OK. Take a look at the picture below for clarity.Copy the code

The code is as follows:

 if (event.action==MotionEvent.ACTION_MOVE){
            // Each notification counts a bit of sliding
            val dis = event.x - startX
            // Event. x, which records the end of the move, is where the next slide starts
            startX = event.x
            // Add each slide segment to the current distance
            minXInit=measuredWidth-xwidthMax
            if (viewxToY + dis < minXInit) {
                viewxToY = minXInit
            } else if (viewxToY + dis > maxXInit) {
                viewxToY = maxXInit
            } else {
                viewxToY += dis
            }
            invalidate()
        }
Copy the code

A little impatient, and here’s what it looks like:

2. Gesture scalable line chart

As a notification tool class for gesture detection in Android, ScaleGestureDetector is rarely used by us. Common scenes include pictures and charts, line charts, and zooming. Of course, onTouchEvent can also calculate the scale by using multiple contacts (Pythagorean theorem), ha ha, take a look at the ScaleGestureDetector.

1. Calculate the scale using onTouchEvent

1. Do we measure onTouchEvent by ourselves? Or should we look inside the ScaleGestureDetector and see how the event is scaled? Override Fun onScale(Detector: ScaleGestureDetector) public float getScaleFactor() { if (inAnchoredScaleMode()) { // Drag is moving up; the further away from the gesture // start, the smaller the span should be, the closer, // the larger the span, and therefore the larger the scale final boolean scaleUp = (mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan < mPrevSpan)) || (! mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan > mPrevSpan)); final float spanDiff = (Math.abs(1 - (mCurrSpan / mPrevSpan)) * SCALE_FACTOR); return mPrevSpan <= mSpanSlop ? 1 : scaleUp ? (1 + spanDiff) : (1 - spanDiff); } return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1; } Here we see roughly the mPrevSpan,mCurrSpan variables using various judgments and calculations to get the scaling factor. Let's go to mPrevSpan,mCurrSpan. OnTouchEvnet public Boolean onTouchEvent(MotionEvent) {onTouchEvnet public Boolean onTouchEvent(MotionEvent) {... // Calculate the hypotenuse using the groove theorem in scale sliding mode. if (inAnchoredScaleMode()) { span = spanY; } else { span = (float) Math.hypot(spanX, spanY); If (action == motionEvent.action_move) {mCurrSpan = spanX; mCurrSpanY = spanY; mCurrSpan = span; boolean updatePrev = true; if (mInProgress) { updatePrev = mListener.onScale(this); } if (updatePrev) { mPrevSpanX = mCurrSpanX; mPrevSpanY = mCurrSpanY; MPrevSpan = mCurrSpan; mPrevTime = mCurrTime; }}} From above we know that we can divide back and forth by the distance between the two toes to get real time scaling. So let's try onTouchEvent on our own.Copy the code

Next we imitate one ourselves to measure the scaling ratio and divide the distance between the two toes:

The following code:

when (event.action and MotionEvent.ACTION_MASK) {
            MotionEvent.ACTION_DOWN -> {
                //1. Indicates a single event
                eventModeType = 1f
            }
            MotionEvent.ACTION_POINTER_DOWN -> {
                // Multi-touch
                oriDis = distance(event)
                if (oriDis > 10f) {
                    //2. Indicates the multi-touch type
                    eventModeType = 2f
                }

            }
            MotionEvent.ACTION_MOVE -> {
                if (eventModeType == 2f) {
                        // Get the distance between two fingers when zooming
                        val newDist = distance(event)
                        if (newDist > 10f) {
                            // Real time zoom is achieved by dividing the distance between the two toes by the current distance
                            curScale = newDist / oriDis
                        }
                }
                // Refresh the View
                invalidate()
            }
            MotionEvent.ACTION_UP->{
                eventModeType = 0f
            }
            MotionEvent.ACTION_POINTER_UP->{
                eventModeType = 0f}}return true
    }

Copy the code

Effect:

2. Use ScaleGestureDetector to monitor the scaling factor for scaling

We have seen part of the code of ScaleGestureDetector above, and you can read the research extensively if you have time. We next use the ScaleGestureDetector to scale. First initialize and then print

//1. Set event in onTouchEvnet
 override fun onTouchEvent(event: MotionEvent): Boolean{ mScaleGestureDetector!! .onTouchEvent(event) .. }private fun initScaleGestureDetector(a) {
        mScaleGestureDetector = ScaleGestureDetector(context, object : SimpleOnScaleGestureListener() {
            override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
                return true
            }

            override fun onScale(detector: ScaleGestureDetector): Boolean {
                Log.e(TAG, "onScale:" + detector.scaleFactor)
                return false
            }

            override fun onScaleEnd(detector: ScaleGestureDetector){}})}Copy the code

Detector. ScaleFactor when the finger spacing is enlarged from small to large -> print the scaleFactor to see:

OnScale:1.0849714
onScale: 1.109393
onScale: 1.1507562
onScale: 1.1814013
onScale: 1.2101753
onScale: 1.2385316
onScale: 1.2752386
onScale: 1.3162968
onScale: 1.3745319
onScale: 1.0194724
onScale: 1.0061256
onScale: 1.0237223
onScale: 1.0157682
// Zoom out
 0.99745494
 0.99683857
 0.9947594
 0.98914015
 0.9827625
 0.9775298
 0.9719838
 0.96702707
 0.9618665
 0.9551136
 0.94867754
 0.94313
 0.9390567
 0.9344024
 0.93175083
 0.928374
 0.9219731
 0.9163468
 0.91060144
 0.90418917
 0.8970686
 0.8903642
 0.8834786
 0.879944
 0.87690777
 0.8743616
 0.87640566
 0.8807793
 0.88613236
 0.8932392
 0.89954007
 0.89262646
 0.9854972
 0.9857342
 0.9946703
 1.0082809
 0.99545985
 0.9920981
 0.9926125
 0.9973208
 1.0117983
 1.0115927
 1.002579
 0.9923982
 0.98811483
 0.98945755
 0.99297947
 1.0000068
 1.0023786
 1.00011
 0.994298
 0.9923681
 0.9915295
 0.99028814
 0.99649304
 1.0011351
 1.0022005
 0.9973822
 0.99489397
 0.98849684
 0.9891182
 0.9916089
 0.99125963
 0.991371
 0.99094766
 0.9904697
 0.9905223
 0.98756933
 0.9896349
 1.0016375
 1.0006968
 0.99415475
 0.98720026
 0.9908081
Copy the code

The zoom factor and as we want to increase gradually, are just starting to some of the data embodies the trend of increase, because the source know, double foot spacing in addition to the above is the current one moment and double toe very short interval time of local press area is larger, the area of the touch screen center part of a series of touch point in different will lead to different results. For example:

The distance between the two fingers on the screen at the last moment was 10, now it is 11 so the scaling factor =11/10=1.1 so the distance between the two fingers on the screen at the last moment is 15, now it is 15.1 so the scaling factor =15.1/15=1.0000000… 1 maybe I’m making a mistake here, it’s not the spacing between the fingers that makes the scaling factor big. . Therefore, the scaling factor is 1 and 1.0 because the screen is pressed for a short time and the center of gravity of the point touching the screen changes greatly. 1.1.. 1.2.. Floating numbers.

How do we map these columns of numbers up and down with superposition?

Just as we’re translating and adding? This will only lead to infinite magnification… In this case, if we can’t determine whether to zoom in or zoom out around 1.0, we can’t use addition but should use multiplication. As follows 1.01.0=1.0 1.0+1.0=2.0 and at the instant the finger is zoomed + will make this number become extremely large. 1.00.5=0.5 1.0+0.5=1.5 Multiplication not only increases because the scaling factor will be greater than 1.0, and decreases to a few tenths <1.0, so multiplication exactly conforms to our scaling law. Next, we do a back and forth calculation of scaling.

  // Insert the code slice here
  private fun initScaleGestureDetector(a) {
        mScaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
            override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
                return true
            }

            override fun onScale(detector: ScaleGestureDetector): Boolean {
               // To maintain continuity -> current scale value * previous scale value
                curScale = detector.scaleFactor * preScale
                Log.e("ScaleGestureDetector"."onScale: "+ detector.scaleFactor )
                // Do not extend when the magnification is greater than 2 or the reduction is less than 0.1 times
                if (curScale > 2 || curScale < 0.1) {
                    preScale = curScale;
                    return true
                }
                // Save the last scaling value
                preScale = curScale;
                invalidate()
                return false
            }

            override fun onScaleEnd(detector: ScaleGestureDetector){}})}Copy the code

Next we set the X-axis of the code and the x and y coordinates of the line to be multiplied by curScale


        // Draw the X-axis
        val pathx = Path()
        // Gesture sliding distance plus
        pathx.moveTo((0f + viewxToY)*curScale, 0f)
        pathx.lineTo((xwidthMax - marginXAndY + viewxToY)*curScale, 0f)

        val horizontalPath = Path()
        horizontalPath.moveTo((xwidthMax - arrowLength + viewxToY)*curScale, arrowLRHeight)
        horizontalPath.lineTo((xwidthMax - marginXAndY + viewxToY)*curScale, 0f)
        horizontalPath.lineTo((xwidthMax - arrowLength + viewxToY)*curScale, -arrowLRHeight)
        pathx.addPath(horizontalPath)
        / / draw the x axis
        canvas.drawPath(pathx, x_paint)

Copy the code

Draw a line diagram multiplied by scaling


  // Draw a line graph
    private fun drawLine(pointList: java.util.ArrayList<ViewPoint>, canvas: Canvas) {
        val linePaint = Paint()
        val path = Path()
        linePaint.style = Paint.Style.STROKE
        linePaint.color = Color.argb(255.225.225.255)
        linePaint.strokeWidth = 10f


        val circle_paint = Paint()
        circle_paint.strokeWidth = 10f
        circle_paint.style = Paint.Style.FILL


        / / the attachment
        path.moveTo(viewxToY*curScale, 0f)
        for (index in 0 until pointList.size) {
            path.lineTo((pointList[index].x + viewxToY)*curScale, pointList[index].y*curScale)
        }
        canvas.drawPath(path, linePaint)

        path.reset()
        // Fill the gradient dish
        for (index in 0 until pointList.size) {
            path.lineTo((pointList[index].x + viewxToY)*curScale, pointList[index].y*curScale)
        }
        val endIndex = pointList.size - 1
        path.lineTo((pointList[endIndex].x + viewxToY)*curScale, 0f)
        path.close()
        linePaint.style = Paint.Style.FILL
        linePaint.shader = getShader()
        linePaint.setShadowLayer(16f.6f, -6f, Color.argb(100.100.255.100))
        canvas.drawPath(path, linePaint)


        // Draw a fixed point circle
        for (index in 0 until pointList.size) {
            circle_paint.shader = getShaders()
            canvas.drawCircle((pointList[index].x + viewxToY)*curScale, pointList[index].y*curScale, 16f, circle_paint)
        }


    }

Copy the code

See the effect

Then the next thing is relatively simple, everything you can see you want to scale by this scale.

Writing here, even if we do not worship can also use your precious finger like and comment communication?

You think this is the end of it? Then we have to do something big. A friend wants to draw a curve, let you draw a good enough.