前言
这段时间闲了下来,决定把项目中的自定义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")
到此就结束了,有问题欢迎提出指正!!!
网友评论