Android自定義View-滑動的貝塞爾曲線

前言

這段時間閒了下來,決定把項目中的自定義View都用Kotlin寫一遍,擼起來吧java

一.TrendCurveView

效果圖git

地址在最底部。。。

1.繪製背景

/**
     * 繪製背景線
     *
     * @param canvas
     */
    private fun drawHorizontalLine(canvas: Canvas) {
        val baseHeight = mAvailableAreaHeight / 5
        for (i in 0 until 6) {
            val startY = baseHeight * i + mAvailableAreaTop
            canvas.drawLine(0f, startY, mViewWidth, startY, mHorizontalLinePaint)
        }
        //畫底部line
        mPaint.shader = null
        mPaint.style = Paint.Style.STROKE
        mPaint.strokeWidth = mBottomLineHeight
        mPaint.color = mBottomLineColor
        canvas.drawLine(
            0f,
            mTotalHeight - mBottomLineHeight,
            mViewWidth,
            mTotalHeight - mBottomLineHeight,
            mPaint
        )
    }
複製代碼

2.繪製單位文字

/**
     * 繪製右邊的 單位:kg
     *
     * @param canvas
     */
    private fun drawUnitDes(canvas: Canvas) {
        if (!TextUtils.isEmpty(mUnitDes)) {
            canvas.drawText(
                mUnitDes!!,
                width.toFloat() - mMarginRight - mUnitDesTextWidth / 2,
                mMarginTop + mUnitDesTextHeight / 2,
                mUnitDesPaint
            )
        }
    }
複製代碼

3.數據處理

數據是{"value":53.5126,"recordDate":"2019-10-12"}這樣的格式,而繪製曲線的時候須要x,y座標,從新封裝一次github

val diff = max - min
        //若是最大值和最小值相等 就繪製一條線
        //mAvailableAreaHeight * 0.8f 是由於計算貝塞爾的時候,頂點座標會超出,全部預留一段
        val scale = if (diff == 0.0) 0.6f else mAvailableAreaHeight * 0.8f / diff.toFloat()
         val mCacheList = ArrayList<TextBean>()
        for (i in data.indices) {
            //計算全部點座標
            val trendDataBean = data[i]
            //從右向左繪製的,偏移viewWidth的一半
            val x = (mCenterX - (data.size - 1 - 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
                //計算全部文字的座標
                val textBean = getTextBean(pm,trendDataBean.value.toString(),
                calendar, pointF)
                textBean.pointF = pointF
                mCacheList.add(textBean)
            } catch (e: ParseException) {
                e.printStackTrace()
            }
        }
複製代碼
private inner class TextBean internal constructor() {
        //數據文字座標
        var centerX: Float = 0.toFloat()
        var centerY: Float = 0.toFloat()
        //數據文字
        var centerStr: String? = null
        //底部日期座標
        var bottomX: Float = 0.toFloat()
        var bottomY: Float = 0.toFloat()
        //底部日期
        var bottomStr: String? = null
        //數據圓點座標
        var circleX: Float = 0.toFloat()
        var circleY: Float = 0.toFloat()
        //座標點
        var pointF: PointF? = null
    }
複製代碼

數據處理好了,就能夠繪製貝塞爾曲線了。滑動採用Scrollercanvas

init {
        initSize()
        initPaint()
        mScroller = Scroller(getContext())
        val configuration = ViewConfiguration.get(context)
        mMinimumFlingVelocity = configuration.scaledMinimumFlingVelocity
        mMaximumFlingVelocity = configuration.scaledMaximumFlingVelocity.toFloat()
    }
    
    override fun computeScroll() {
        if (mScroller!!.computeScrollOffset()) {
            //判斷左右邊界
            mMove = mScroller.currX
            if (mMove > mMaxMove) {
                mMove = mMaxMove
            } else if (mMove < 0) {
                mMove = 0
            }
            invalidate()
        }
    }
複製代碼

4.計算曲線點

根據滑動距離,從cacheList中計算出當前須要繪製的數據bash

/**
     *
     * 保證每次繪製作多nub + 3+3  三階貝塞爾 三個控制點 左右各三個
     * 根據滑動距離計算展現的條目
     *
     * @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)
        }
        //越界處理
        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])
        }
    }
複製代碼

根據獲得的mShowList,計算出三階貝塞爾曲線app

/**
     * 根據要展現的條目 計算出須要繪製path
     *
     * @param pointFList
     */
    private fun measurePath(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.size
        for (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)) {
                //是不是第一個點
                if (i > 0) {
                    val point = pointFList[i - 1].pointF
                    previousPointX = point!!.x + mMove
                    previousPointY = point.y
                } else {
                    //是的話就用當前點表示上一個點
                    previousPointX = currentPointX
                    previousPointY = currentPointY
                }
            }

            if (java.lang.Float.isNaN(prePreviousPointX)) {
                //是不是前兩個點
                if (i > 1) {
                    val point = pointFList[i - 2].pointF
                    prePreviousPointX = point!!.x + mMove
                    prePreviousPointY = point.y
                } else {
                    //是的話就用當前點表示上上個點
                    prePreviousPointX = previousPointX
                    prePreviousPointY = previousPointY
                }
            }

            // 判斷是否是最後一個點了
            if (i < lineSize - 1) {
                val point = pointFList[i + 1].pointF
                nextPointX = point!!.x + mMove
                nextPointY = point.y
            } else {
                //是的話就用當前點表示下一個點
                nextPointX = currentPointX
                nextPointY = currentPointY
            }

            if (i == 0) {
                // 將Path移動到開始點
                mPath.moveTo(currentPointX, currentPointY)
            } else {
                // 求出控制點座標
                val firstDiffX = currentPointX - 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
                //畫出曲線
                mPath.cubicTo(
                    firstControlPointX,
                    firstControlPointY,
                    secondControlPointX,
                    secondControlPointY,
                    currentPointX,
                    currentPointY
                )
            }
            // 更新值,
            prePreviousPointX = previousPointX
            prePreviousPointY = previousPointY
            previousPointX = currentPointX
            previousPointY = currentPointY
            currentPointX = nextPointX
            currentPointY = nextPointY
        }
    }
複製代碼

5.繪製Path 和 文字

有了Path和須要繪製的數據點,就easy了,剩下的就是繪製了dom

/**
     * 繪製曲線和背景填充
     *
     * @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!!.x + mMove
            //先畫曲線
            canvas.drawPath(mPath, mCurvePaint)
            //再填充背景
            mPath.lineTo(lastX, mAvailableAreaTop + mAvailableAreaHeight)
            mPath.lineTo(firstX, mAvailableAreaTop + mAvailableAreaHeight)
            mPath.close()
            canvas.drawPath(mPath, mPathPaint)
        }

    }
複製代碼
/**
     * 繪製頂部矩形和文字 以及垂直線
     *
     * @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 else mUnitPaint.measureText(mUnit)

        val centerTvWidth = valueWidth + unitWidth + 1f

        val topRectPath = getTopRectPath(centerTvWidth)
        mPaint.style = Paint.Style.FILL
        mPaint.color = mCurveLineColor
        canvas.drawPath(topRectPath, mPaint)
        //畫居中線
        canvas.drawLine(
            mCenterX.toFloat(),
            mAvailableAreaTop - mArrowBottomMargin,
            mCenterX.toFloat(),
            mTotalHeight.toFloat() - mBottomHeight - mBottomLineHeight,
            mPaint
        )

        //計算text Y座標
        mRectF.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
            )
        }


    }

    /**
     * 頂部矩形+三角
     *
     * @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, mTopRectRadius, mTopRectRadius, Path.Direction.CCW)
        //畫三角
        mTopPath.moveTo(mRectF.centerX() - mArrowWidth / 2f, mMarginTop + mRectF.height())
        mTopPath.lineTo(mRectF.centerX(), mMarginTop + mRectF.height() + mArrowWidth / 2f)
        mTopPath.lineTo(mRectF.centerX() + mArrowWidth / 2f, mMarginTop + mRectF.height())
        mTopPath.close()
        return mTopPath
    }


    /**
     * 繪製每一個點的值和圓
     *
     * @param canvas
     */
    private fun drawValueAndPoint(canvas: Canvas) {
        for (i in mShowList.indices) {
            val textBean = mShowList[i]
            val centerX = textBean.centerX + mMove
            //繪製值
            canvas.drawText(textBean.centerStr!!, centerX, textBean.centerY, mTextPaint)
            //繪製底部日期
            mTextPaint.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
            )
        }
    }
複製代碼

6.onTouchEvent

最後的就是手勢處理,以及滾動回彈效果,回彈效果根據Scroller.finalX計算ide

var finalX = mScroller.finalX
            val distance = abs(finalX % mEveryRectWidth)
            if (distance < mEveryRectWidth / 2) {
                finalX -= distance
            } else {
                finalX += (mEveryRectWidth - distance)
            }
複製代碼
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: Int
        if (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)
    }

複製代碼

7.填充數據

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")
複製代碼

到此就結束了,有問題歡迎提出指正!!!ui

github地址spa

相關文章
相關標籤/搜索