前言
这段时间闲了下来,决定把项目中的自定义View都用Kotlin写一遍,撸起来吧
一.TrendCurveView
效果图
地址在最底部。。。
1.绘制背景
/*** 绘制背景线** @param canvas*/private fun drawHorizontalLine(canvas: Canvas) {val baseHeight = mAvailableAreaHeight / 5for (i in 0 until 6) {val startY = baseHeight * i + mAvailableAreaTopcanvas.drawLine(0f, startY, mViewWidth, startY, mHorizontalLinePaint)}//画底部linemPaint.shader = nullmPaint.style = Paint.Style.STROKEmPaint.strokeWidth = mBottomLineHeightmPaint.color = mBottomLineColorcanvas.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.recordDatetry {val parse = simpleDateFormat.parse(recordDate)calendar.time = parse//计算所有文字的坐标val textBean = getTextBean(pm,trendDataBean.value.toString(),calendar, pointF)textBean.pointF = pointFmCacheList.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.scaledMinimumFlingVelocitymMaximumFlingVelocity = configuration.scaledMaximumFlingVelocity.toFloat()}override fun computeScroll() {if (mScroller!!.computeScrollOffset()) {//判断左右边界mMove = mScroller.currXif (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: Intvar end: Intif (absMove < mCenterX) {end = mTotalSizestart = mTotalSize - ((absMove + mCenterX) / mEveryRectWidth + 3)} else {val exceedStart = (absMove - mCenterX) / mEveryRectWidthend = mTotalSize - (exceedStart - 3)start = mTotalSize - (exceedStart + NUB + 3)}//越界处理end = if (mTotalSize > end) end else mTotalSizestart = if (start > 0) start else 0mShowList.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.NaNvar prePreviousPointY = java.lang.Float.NaNvar previousPointX = java.lang.Float.NaNvar previousPointY = java.lang.Float.NaNvar currentPointX = java.lang.Float.NaNvar currentPointY = java.lang.Float.NaNvar nextPointX: Floatvar nextPointY: Floatval lineSize = pointFList.sizefor (i in 0 until lineSize) {if (java.lang.Float.isNaN(currentPointX)) {val point = pointFList[i].pointFcurrentPointX = point!!.x + mMovecurrentPointY = point.y}if (java.lang.Float.isNaN(previousPointX)) {//是否是第一个点if (i > 0) {val point = pointFList[i - 1].pointFpreviousPointX = point!!.x + mMovepreviousPointY = point.y} else {//是的话就用当前点表示上一个点previousPointX = currentPointXpreviousPointY = currentPointY}}if (java.lang.Float.isNaN(prePreviousPointX)) {//是否是前两个点if (i > 1) {val point = pointFList[i - 2].pointFprePreviousPointX = point!!.x + mMoveprePreviousPointY = point.y} else {//是的话就用当前点表示上上个点prePreviousPointX = previousPointXprePreviousPointY = previousPointY}}// 判断是不是最后一个点了if (i < lineSize - 1) {val point = pointFList[i + 1].pointFnextPointX = point!!.x + mMovenextPointY = point.y} else {//是的话就用当前点表示下一个点nextPointX = currentPointXnextPointY = currentPointY}if (i == 0) {// 将Path移动到开始点mPath.moveTo(currentPointX, currentPointY)} else {// 求出控制点坐标val firstDiffX = currentPointX - prePreviousPointXval firstDiffY = currentPointY - prePreviousPointYval secondDiffX = nextPointX - previousPointXval secondDiffY = nextPointY - previousPointYval firstControlPointX = previousPointX + lineSmoothness * firstDiffXval firstControlPointY = previousPointY + lineSmoothness * firstDiffYval secondControlPointX = currentPointX - lineSmoothness * secondDiffXval secondControlPointY = currentPointY - lineSmoothness * secondDiffY//画出曲线mPath.cubicTo(firstControlPointX,firstControlPointY,secondControlPointX,secondControlPointY,currentPointX,currentPointY)}// 更新值,prePreviousPointX = previousPointXprePreviousPointY = previousPointYpreviousPointX = currentPointXpreviousPointY = currentPointYcurrentPointX = nextPointXcurrentPointY = nextPointY}}
5.绘制Path 和 文字
有了Path和需要绘制的数据点,就easy了,剩下的就是绘制了
/*** 绘制曲线和背景填充** @param canvas*/private fun drawCurveLineAndBgPath(canvas: Canvas) {if (mShowList.size > 0) {val firstX = mShowList[0].pointF!!.x + mMoveval 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].centerStrval valueWidth = mTopTextPaint.measureText(centerValue)val unitWidth = if (TextUtils.isEmpty(mUnit)) 0f else mUnitPaint.measureText(mUnit)val centerTvWidth = valueWidth + unitWidth + 1fval topRectPath = getTopRectPath(centerTvWidth)mPaint.style = Paint.Style.FILLmPaint.color = mCurveLineColorcanvas.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.fontMetricsIntmTopBaseLineY =((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 = mBottomTextSizecanvas.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.finalXval 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.actionval pointerUp = action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_POINTER_UPval skipIndex = if (pointerUp) event.actionIndex else -1// Determine focal pointvar sumX = 0fvar sumY = 0fval count = event.pointerCountfor (i in 0 until count) {if (skipIndex == i) continuesumX += event.getX(i)sumY += event.getY(i)}val div = if (pointerUp) count - 1 else countval focusX = sumX / divval focusY = sumY / divwhen (event.action) {MotionEvent.ACTION_DOWN -> {mLastFocusX = focusXmDownFocusX = mLastFocusXmLastFocusY = focusYmDownFocusY = mLastFocusYreturn true}MotionEvent.ACTION_MOVE ->if (abs(mMove) <= mMaxMove) {val scrollX = (mLastFocusX - focusX).toInt()smoothScrollBy(-scrollX, 0)mLastFocusX = focusXmLastFocusY = 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.finalXval 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!!.finalXval 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)}
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地址