Android自定义View-滑动的贝塞尔曲线

2019-10-14  本文已影响0人  爱惜羽毛

前言

这段时间闲了下来,决定把项目中的自定义View都用Kotlin写一遍,撸起来吧

一.TrendCurveView

效果图(有点模糊)

image

地址在最底部。。。

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坐标,重新封装一次

        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
    }

数据处理好了,就可以绘制贝塞尔曲线了。滑动采用Scroller

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中计算出当前需要绘制的数据

/**
     *
     * 保证每次绘制做多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,计算出三阶贝塞尔曲线

/**
     * 根据要展示的条目 计算出需要绘制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了,剩下的就是绘制了

/**
     * 绘制曲线和背景填充
     *
     * @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计算

            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")

到此就结束了,有问题欢迎提出指正!!!

github地址

上一篇下一篇

猜你喜欢

热点阅读