美文网首页
Android自定义View-滑动的贝塞尔曲线

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

作者: 爱惜羽毛 | 来源:发表于2019-10-14 23:15 被阅读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地址

    相关文章

      网友评论

          本文标题:Android自定义View-滑动的贝塞尔曲线

          本文链接:https://www.haomeiwen.com/subject/elzdmctx.html