preface

This time free down, decided to project in the custom View with Kotlin to write all over, wank up

A TrendCurveView.

rendering

1. Paint the background

/** @param canvas */ private fun drawHorizontalLine(canvas: Canvas) { val baseHeight = mAvailableAreaHeight / 5for (i in0 until 6) { val startY = baseHeight * i + mAvailableAreaTop canvas.drawLine(0f, startY, mViewWidth, startY, MHorizontalLinePaint)} // Draw bottom line mPaint. Shader = null mPaint. Style = Paint mBottomLineHeight mPaint.color = mBottomLineColor canvas.drawLine( 0f, mTotalHeight - mBottomLineHeight, mViewWidth, mTotalHeight - mBottomLineHeight, mPaint ) }Copy the code

2. Draw unit text

/** @param canvas */ private fun drawUnitDes(canvas: canvas) {if(! TextUtils.isEmpty(mUnitDes)) { canvas.drawText( mUnitDes!! , width.toFloat() - mMarginRight - mUnitDesTextWidth / 2, mMarginTop + mUnitDesTextHeight / 2, mUnitDesPaint ) } }Copy the code

3. Data processing

{“value”:53.5126,”recordDate”:”2019-10-12″

Val diff = max-min // draw a line if the maximum and minimum values are equal //mAvailableAreaHeight * 0.8f because when calculating Bessel, the vertex coordinates will exceed val scale =if0.6 f (diff = = 0.0)elseMAvailableAreaHeight * 0.8f/diff.tofloat () val mCacheList = ArrayList<TextBean>()for (i inData. Indices) {val trendDataBean = data[I] * mEveryRectWidth). ToFloat () val y = (mAvailableAreaTop + (Max -) trendDataBean.value) * scale).toFloat() val pointF = PointF(x, y) val recordDate = trendDataBean.recordDate try { val parse = simpleDateFormat.parse(recordDate) calendar.time = parse / / calculate the coordinates all words val textBean = getTextBean (PM, trendDataBean. Value. The toString (), calendar, pointF) textBean.pointF = pointF mCacheList.add(textBean) } catch (e: ParseException) { e.printStackTrace() } }Copy the code
private inner class TextBean internal constructorToFloat () var centerX: Float = 0.tofloat () var centerY: Float = 0.tofloat () var centerStr: String? Var bottomX: Float = 0. ToFloat () var bottomY: Float = 0. ToFloat () var bottomY: Float = 0. ToFloat () var circleY: Float = 0.tofloat () var pointF: pointF? = null }Copy the code

Once the data is processed, bezier curves can be plotted. Scroller is used for sliding

init {
        initSize()
        initPaint()
        mScroller = Scroller(getContext())
        val configuration = ViewConfiguration.get(context)
        mMinimumFlingVelocity = configuration.scaledMinimumFlingVelocity
        mMaximumFlingVelocity = configuration.scaledMaximumFlingVelocity.toFloat()
    }
    
    override fun computeScroll() {
        if(mScroller!! .computescrolloffSet ()) {// determine the left and right boundaries mMove = mscroll.currxif (mMove > mMaxMove) {
                mMove = mMaxMove
            } else if (mMove < 0) {
                mMove = 0
            }
            invalidate()
        }
    }
Copy the code

4. Calculate curve points

According to the sliding distance, the current data to be drawn is calculated from the cacheList

/** ** Ensure that nub +3 +3 third-order Bessel three control points around each three * according to the sliding distance to calculate the display of items ** @param move */ private Fun calculateShowList(move: Int) {if (mCacheList.isEmpty()) {
            return
        }
        val absMove = abs(move)
        var start: Int
        var end: Int
        if (absMove < mCenterX) {
            end = mTotalSize
            start = mTotalSize - ((absMove + mCenterX) / mEveryRectWidth + 3)
        } else{ val exceedStart = (absMove - mCenterX) / mEveryRectWidth end = mTotalSize - (exceedStart - 3) start = mTotalSize - ExceedStart + NUB + 3} // exceedStart end =if (mTotalSize > end) end else mTotalSize
        start = if (start > 0) start else 0
        mShowList.clear()
        //        mShowList.addAll(mCacheList.subList(start,end));
        for (i in start until end) {
            mShowList.add(mCacheList[i])
        }
    }
Copy the code

According to the mShowList obtained, the third-order Bessel curve is calculated

/** */ private Fun measurePath(pointFList: pointFList) List<TextBean>) { mPath.reset() var prePreviousPointX = java.lang.Float.NaN var prePreviousPointY = java.lang.Float.NaN var previousPointX = java.lang.Float.NaN var previousPointY = java.lang.Float.NaN var currentPointX = java.lang.Float.NaN var currentPointY = java.lang.Float.NaN var nextPointX: Float var nextPointY: Float val lineSize = pointFList.sizefor (i in 0 until lineSize) {
            if(java.lang.Float.isNaN(currentPointX)) { val point = pointFList[i].pointF currentPointX = point!! .x + mMove currentPointY = point.y }if(java.lang.float. IsNaN (previousPointX)) {// is the first pointif(i > 0) { val point = pointFList[i - 1].pointF previousPointX = point!! .x + mMove previousPointY = point.y }elsePreviousPointX = currentPointX previousPointY = currentPointY}}if(java.lang.float. IsNaN (prePreviousPointX)) {// whether it is the first two pointsif(i > 1) { val point = pointFList[i - 2].pointF prePreviousPointX = point!! .x + mMove prePreviousPointY = point.y }elsePreviousPointX = previousPointX prePreviousPointY = previousPointY}if(i < lineSize - 1) { val point = pointFList[i + 1].pointF nextPointX = point!! .x + mMove nextPointY = point.y }elseNextPointX = currentPointX nextPointY = currentPointY}if(I == 0) {// Move Path to start mPath. MoveTo (currentPointX, currentPointY)}elseVal firstDiffY = currentPointy-prepreViouspointx val firstDiffY = currentPointy-prepreViouspointy val secondDiffX = nextPointX - previousPointX val secondDiffY = nextPointY - previousPointY val firstControlPointX = previousPointX + lineSmoothness * firstDiffX val firstControlPointY = previousPointY + lineSmoothness * firstDiffY val secondControlPointX = currentPointX - lineSmoothness * secondDiffX val secondControlPointY = currentPointY - LineSmoothness * secondDiffY // cubicTo(firstControlPointX, firstControlPointY, secondControlPointX, SecondControlPointY, currentPointX, currentPointY)} prePreviousPointX = previousPointX prePreviousPointY = previousPointY previousPointX = currentPointX previousPointY = currentPointY currentPointX = nextPointX currentPointY = nextPointY } }Copy the code

5. Draw Path and text

Once you have the Path and the data points you need to draw, easy is all that is left to draw

@param canvas */ private fun drawCurveLineAndBgPath(canvas: canvas) {if(mShowList.size > 0) { val firstX = mShowList[0].pointF!! .x + mMove val lastX = mShowList[mShowList.size - 1].pointF!! DrawPath (mPath, mCurvePaint) // Fill the background mPath. LineTo (lastX, mCurvePaint) mAvailableAreaTop + mAvailableAreaHeight) mPath.lineTo(firstX, mAvailableAreaTop + mAvailableAreaHeight) mPath.close() canvas.drawPath(mPath, mPathPaint) } }Copy the code
/ * * * map at the top of the rectangle and text and vertical line * * @ param canvas * / private fun drawTopAndVerticalLineView (canvas: Canvas) {val scrollX = abs(mMove) val baseWidth = mEveryRectWidth / 2f // Var nub = mTotalSize - 1 - ((scrollX + baseWidth)/mEveryRectWidth).toint ()if (nub > mTotalSize - 1) {
            nub = mTotalSize - 1
        }
        if (nub < 0) {
            nub = 0
        }
        val centerValue = mCacheList[nub].centerStr
        val valueWidth = mTopTextPaint.measureText(centerValue)
        val unitWidth = if (TextUtils.isEmpty(mUnit)) 0f elsemUnitPaint.measureText(mUnit) val centerTvWidth = valueWidth + unitWidth + 1f val topRectPath = getTopRectPath(centerTvWidth) mPaint.style = Paint.Style.FILL mPaint.color = mCurveLineColor Canvas. drawPath(topRectPath, mPaint) // Draw center line Canvas. drawLine(McEnterx.tofloat (), mAvailableAreaTop - mArrowBottomMargin, mCenterX.toFloat(), mTotalHeight.toFloat() - mBottomHeight - mBottomLineHeight, Set (McEnterx-centertvwidth / 2f, mMarginTop, mCenterX + centerTvWidth / 2, mMarginTop + mTopTvVerticalMargin * 2 + mTopTextHeight )if(mTopBaseLineY == 0) { val pm = mTextPaint.fontMetricsInt mTopBaseLineY = ((mRectF.bottom + mRectF.top - Pm.bottom.tofloat () -pm.top.tofloat ()) / 2f).toint ()} canvas. DrawText (centerValue!! , mRectF.centerX() - centerTvWidth / 2 + valueWidth / 2, mTopBaseLineY.toFloat(), mTopTextPaint )if(! Textutils.isempty (mUnit)) {// canvas. DrawText (mUnit!! , mRectF.centerX() + centerTvWidth / 2 - unitWidth / 2, mTopBaseLineY.toFloat(), MUnitPaint)} /** * top rectangle + Triangle ** @param rectWidth */ private fun getTopRectPath(rectWidth: Float): Path { mRectF.set( mCenterX.toFloat() - rectWidth / 2f - mTopTvHorizontalMargin, mMarginTop, mCenterX.toFloat() + rectWidth / 2f + mTopTvHorizontalMargin, MMarginTop + mTopTvVerticalMargin * 2 + mTopTextHeight) mtoppath.reset () // mtoppath.addroundRect (mRectF, Mtoppath.moveto (mrectf.centerx () -MarrowWidth / 2f, mTopRectRadius, mTopRectRadius, path.direction.ccw) mMarginTop + mRectF.height()) mTopPath.lineTo(mRectF.centerX(), mMarginTop + mRectF.height() + mArrowWidth / 2f) mTopPath.lineTo(mRectF.centerX() + mArrowWidth / 2f, mMarginTop + mRectF.height()) mTopPath.close()return** @param canvas */ private fun drawValueAndPoint(canvas: canvas) {for (i inMshowlist. indices) {val textBean = mShowList[I] val centerX = textBean.centerX + mMove canvas.drawText(textBean.centerStr!! , centerX, textBean.centerY, TextSize = mBottomTextSize canvas. DrawText (textBean.bottomstr!! , centerX, textBean.bottomY, mTextPaint) canvas.drawCircle(centerX, textBean.circleY, mInnerRadius, mInnerCirclePaint) canvas.drawCircle( centerX, textBean.circleY, mInnerRadius + mOuterRadiusWidth / 2, mOuterCirclePaint ) } }Copy the code

6.onTouchEvent

The last is gesture processing, and the scrolling rebound effect, the rebound effect is calculated according to scroll.finalx

            var finalX = mScroller.finalX
            val distance = abs(finalX % mEveryRectWidth)
            if (distance < mEveryRectWidth / 2) {
                finalX -= distance
            } else {
                finalX += (mEveryRectWidth - distance)
            }
Copy the code
 override fun onTouchEvent(event: MotionEvent): Boolean {
        if(mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain() } mVelocityTracker!! .addMovement(event) val action = event.action val pointerUp = action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_POINTER_UP val skipIndex =if (pointerUp) event.actionIndex else -1
        // Determine focal point
        var sumX = 0f
        var sumY = 0f
        val count = event.pointerCount
        for (i in 0 until count) {
            if (skipIndex == i) continue
            sumX += event.getX(i)
            sumY += event.getY(i)
        }
        val div = if (pointerUp) count - 1 else count
        val focusX = sumX / div
        val focusY = sumY / div

        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                mLastFocusX = focusX
                mDownFocusX = mLastFocusX
                mLastFocusY = focusY
                mDownFocusY = mLastFocusY
                return true
            }
            MotionEvent.ACTION_MOVE ->

                if(abs(mMove) <= mMaxMove) { val scrollX = (mLastFocusX - focusX).toInt() smoothScrollBy(-scrollX, 0) mLastFocusX = focusX mLastFocusY = focusY } MotionEvent.ACTION_UP -> { mVelocityTracker!! .computeCurrentVelocity(1000, mMaximumFlingVelocity) val velocityX = mVelocityTracker!! .xVelocity //if(abs(velocityX) > mMinimumFlingVelocity) { mScroller!! .fling( mMove, 0, velocityX.toInt(), mVelocityTracker!! .yVelocity.toInt(), 0, mMaxMove, 0, 0 ) var finalX = mScroller.finalX val distance = abs(finalX % mEveryRectWidth)if (distance < mEveryRectWidth / 2) {
                        finalX -= distance
                    } else {
                        finalX += (mEveryRectWidth - distance)
                    }
                    mScroller.finalX = finalX

                } else {
                    setClick(event.x.toInt(), mDownFocusX)
                }
                getCurrentIndex()

                if(mVelocityTracker ! = null) { // This may have been cleared when we called out to the // application above. mVelocityTracker!! .recycle() mVelocityTracker = null } }else -> {
            }
        }//                invalidate();
        return super.onTouchEvent(event)
    }


    private fun setClick(upX: Int, downX: Float) { var finalX = mScroller!! .finalX val distance: Intif (abs(downX - upX) > 10) {
            distance = abs(finalX % mEveryRectWidth)
            if (distance < mEveryRectWidth / 2) {
                finalX -= distance
            } else {
                finalX += (mEveryRectWidth - distance)
            }

        } else {
            val space = (mCenterX - upX).toFloat()
            distance = abs(space % mEveryRectWidth).toInt()
            val nub = (space / mEveryRectWidth).toInt()
            if (distance < mEveryRectWidth / 2) {
                if(nub ! = 0) { finalX =if (space > 0) {
                        (finalX + (space - distance)).toInt()
                    } else {
                        (finalX + (space + distance)).toInt()
                    }
                }
            } else {
                if (space > 0) {
                    finalX += (nub + 1) * mEveryRectWidth
                } else {
                    finalX = (finalX + space - (mEveryRectWidth - distance)).toInt()

                }

            }
        }
        if (finalX < 0) {
            finalX = 0
        } else if (finalX > mMaxMove) {
            finalX = mMaxMove
        }
        smoothScrollTo(finalX, 0)
    }

Copy the code

7. Fill in the data

val list = (0.. 1000).toList() val mutableList = mutableListOf<DataBean>()for (i in list) {
            mutableList.add(
                DataBean(
                    "2019-10-10", random.nextint (100) + 0.5))} trendCurveView.setData(mutableList,"kg")
Copy the code

This is the end of the question welcome to put forward corrections!!

Making the address