自定义View:画布实现自定义View(折线图的实现)
今天道长打算说一下用画布实现自定义View,这是道长说的自定义View的第四种实现方式了。
第一种:是放好布局后使用NineOldAndroid监听动画实现,想看一下的话点击传送门属性动画(二):如何自定义View以及自定义View:侧滑菜单动画实现。
第二种:是放好布局后使用TouchEvent监听实现,传送门在此自定义View:侧滑菜单实现。
第三种:是继承相关的View,拓展相关View的功能,传送门在此PopWindow:基本使用与自定义PopWindow。
第三种自定义View就是拓展相关View的功能。比如自定义PopWindow要增加出现动画或者展示方式。前两种都是使用已经存在的布局,一种继承FrameLayout,另一种继承ViewGroup。放置好位置后监听事件实现。应该说前两种是自定义组合View。今天说的这种方式继承View,可以用画布绘制各种形状的图形,然后监听事件实现。这里以折线图的实现为例,折线图可以左右滑动。好了咱们开车……
一、效果图
动态图没有,先把效果图放在这里,然后绘制View。
二、绘制View
上面的效果图都看到折线图有网格,有横向限制区域,有标记点,有目标点,有坐标轴单位,Y轴分割为两个区域,还可以左右滑动。
把画布Canvas与生活中的纸张看成一样就可以了,要知道咱们在纸张上写东西时先写的会被后写的遮盖住。所以说绘制View时要注意分层。
- 构造函数,初始化画布,画笔
public CanvasView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public CanvasView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CanvasView(Context context) {
this(context, null);
}
private void init(Context context) {
mTextColorSize = sp2px(context, mTextColorSize);
mTextColorSmall = sp2px(context, mTextColorSmall);
mTrendLineSize = dp2px(context, mTrendLineSize);
mInnerCircleSize = (int) dp2px(context, mInnerCircleSize);
mOuterCircleSize = (int) dp2px(context, mOuterCircleSize);
mOuterCircleRadius = (int) dp2px(context, mOuterCircleRadius);
mInnerCircleRadius = (int) dp2px(context, mInnerCircleRadius);
mYCenterSize = (int) dp2px(context, mYCenterSize);
focusTextSize = (int) dp2px(context, focusTextSize);
mPaint = new Paint();
mPaint.setTextAlign(Align.CENTER);
mPaint.setStyle(Style.STROKE);
mPaint.setAntiAlias(true);
mInnerCirclePaint = new Paint();
mInnerCirclePaint.setTextAlign(Align.CENTER);
mInnerCirclePaint.setColor(mInnerCircleColor);
mInnerCirclePaint.setTextSize(mInnerCircleSize);
mInnerCirclePaint.setAntiAlias(true);
mInnerCirclePaint.setTextSize(mInnerCircleSize);
mOuterCirclePaint = new Paint();
mOuterCirclePaint.setTextAlign(Align.CENTER);
mOuterCirclePaint.setTextSize(mOuterCircleSize);
mOuterCirclePaint.setAntiAlias(true);
mOuterCirclePaint.setTextSize(mOuterCircleSize);
mTitlePaint = new Paint();
mTitlePaint.setTextAlign(Align.CENTER);
mTitlePaint.setTextSize(sp2px(context, 20));
mTitlePaint.setTextAlign(Align.CENTER);
mRangeTrendBackgroundPaint = new Paint();
nRangeTrendBackgroundPaint = new Paint();
mPulsePaint = new Paint();
final ViewConfiguration configuration = ViewConfiguration.get(context);
mTouchSlop = configuration.getScaledTouchSlop();
mYTitleRect = new Rect();
nYTitleRect = new Rect();
pYTitleRect = new Rect();
mPointColors = new int[]{0xFF349800, 0xFF0082b4}; // 画笔的颜色
mYTitleWidth = (int) dp2px(context, mYTitleWidth);
mRangeTrendColors = new int[]{0XFFDBF9CC, 0XFFDBF9CC, 0XFFDBF9CC};
nRangeTrendColors = new int[]{0XFFE0F6FF, 0XFFE0F6FF, 0XFFE0F6FF, 0XFFE0F6FF};
mPulseColors = new int[]{0XFFFFFBE4, 0XFFFFFBE4, 0XFFFFFBE4};
}
- 第一层绘制折线图限制区域
/**
* 设置界限的区域的
*
* @param canvas
* @param paint
* @param rangeTrendColors
* @param up
* @param down
*/
private void drawBackground(Canvas canvas, Paint paint, int[] rangeTrendColors, float up, float down) {
Map<String, Object> params = getViewParams();
Rect rect = new Rect();
rect.set((int) ((Float) params.get("scrollX") + 0), (int) ((Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") * up), (int) ((Float) params.get("scrollX") + getWidth()), (int) (getHeight() - (Integer) params.get("xAxisTitleHeight") - (Integer) params.get("trendHeight") * down));
LinearGradient gradient = new LinearGradient((Float) params.get("scrollX") + getWidth(), (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") * up, (Float) params.get("scrollX") + getWidth(), getHeight() - (Integer) params.get("xAxisTitleHeight") - (Integer) params.get("trendHeight") * down, rangeTrendColors, null, Shader.TileMode.CLAMP);
paint.setShader(gradient);
canvas.drawRect(rect, paint);
canvas.save();
}
效果图如下:
- 第二层绘制表格
/**
* 绘制表格
*
* @param canvas
* @param paint
*/
private void drawForm(Canvas canvas, Paint paint) {
for (int i = 0; i < mDrawCount; i++) {
drawColumnLine(canvas, paint, 0xffd5edff, (int) mTextColorSize, i);
}
for (int i = 1; i <= mDrawCount + 1; i++) {
if (i == 1 || i == 5 || i == 6 || i == 8) {
drawRowLine(canvas, paint, 0xff7ecef9, 1, i);
} else {
drawRowLine(canvas, paint, 0xffe5e5e5, 1, i);
}
}
}
/**
* 绘制表格竖线
*
* @param canvas
* @param paint
* @param lineColor
* @param lineWith
* @param position
*/
private void drawColumnLine(Canvas canvas, Paint paint, int lineColor, int lineWith, int position) {
Map<String, Object> params = getViewParams();
paint.setColor(lineColor);
paint.setTextSize(lineWith);
canvas.drawLine((Float) params.get("scrollX") + mYTitleWidth + mDistance * (position), (Integer) params.get("xAxisHeadHeight"), (Float) params.get("scrollX") + mYTitleWidth + mDistance * (position), (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1), paint);
}
/**
* 绘制表格横线
*
* @param canvas
* @param paint
* @param lineColor
* @param lineWith
* @param position
*/
private void drawRowLine(Canvas canvas, Paint paint, int lineColor, int lineWith, int position) {
Map<String, Object> params = getViewParams();
paint.setColor(lineColor);
paint.setStrokeWidth(lineWith);
canvas.drawLine((Float) params.get("scrollX") + mYTitleWidth - 20, (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (position - 1), (Float) params.get("scrollX") + getWidth(), (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (position - 1), paint);
}
效果图如下:
- 第三层绘制网格分割区域
/**
* 分割网格
*
* @param canvas
*/
private void setSplitForm(Canvas canvas, Paint paint, int color, int width) {
Map<String, Object> params = getViewParams();
paint.setColor(color);
paint.setStrokeWidth(width);
canvas.drawRect((Float) params.get("scrollX") + 0, (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (5 - 1) + 1, (Float) params.get("scrollX") + getWidth(), (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (6 - 1), paint);// 长方形
}
- 第四层绘制中心标记
/**
* 绘制中心标志
*
* @param canvas
* @param paint
* @param position
*/
private void drawCenterSign(Canvas canvas, Paint paint, int position) {
drawCenterLine(canvas, paint, position);
drawTriangle(canvas, paint, position);
}
/**
* 绘制中心线
*
* @param canvas
* @param paint
* @param position
*/
private void drawCenterLine(Canvas canvas, Paint paint, int position) {
Map<String, Object> params = getViewParams();
paint.setColor(mCenterColor); // 修改中心竖线颜色
paint.setStrokeWidth(mYCenterSize);
canvas.drawLine((Float) params.get("scrollX") + mYTitleWidth + mDistance * (position), (Integer) params.get("xAxisHeadHeight"), (Float) params.get("scrollX") + mYTitleWidth + mDistance * (position), (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1) - 20, paint);
}
/**
* 画三角形
*
* @param canvas
* @param paint
* @param position
*/
public void drawTriangle(Canvas canvas, Paint paint, int position) {
Map<String, Object> params = getViewParams();
paint.setStyle(Style.STROKE);
paint.setStrokeWidth(2);
paint.setColor(0xff7ecef9);
Path path = new Path();
path.reset();
path.moveTo((Float) params.get("scrollX") + mYTitleWidth + mDistance * (position), (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1) - 20);// 开始坐标 也就是三角形的顶点
path.lineTo((Float) params.get("scrollX") + mYTitleWidth + mDistance * (position) - 20, (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1));
path.lineTo((Float) params.get("scrollX") + mYTitleWidth + mDistance * (position) + 20, (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1));
path.close();
canvas.drawPath(path, paint);
// 去掉底边
mTitlePaint.setColor(Color.WHITE);
mTitlePaint.setStrokeWidth(3);
canvas.drawLine((Float) params.get("scrollX") + mYTitleWidth + mDistance * (position) - 19, (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1), (Float) params.get("scrollX") + mYTitleWidth + mDistance * (position) + 19, (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1), mTitlePaint);
}
效果图如下:
- 第五层绘制折线
/**
* 绘制转折线
*
* @param canvas
* @param paint
* @param canvasLine
* @param color
*/
private void drawCanvasLine(Canvas canvas, Paint paint, List<Integer> canvasLine, int color) {
Path mPath = new Path(); // 绘制趋势图对于的Path对象
ArrayList<Integer> LinePosition = dealCanvasData(canvasLine);
Map<String, Object> params = getViewParams();
int startPosition = LinePosition.get(0);
int endPosition = LinePosition.get(1);
paint.setColor(color);
// draw trend
if (endPosition > startPosition && endPosition > 0) {
for (int i = startPosition; i < endPosition; i++) {
int currentY = (int) ((Integer) params.get("xAxisHeadHeight") + (mMaxHeight - canvasLine.get(i)) / mScaleValue * ((Integer) params.get("trendHeight") / 8));
// 处理为负的数据,不需要可以屏蔽
if (canvasLine.get(i) < 0) {
double Y = canvasLine.get(i) * 32.0 / 40.0;
currentY = (int) ((Integer) params.get("xAxisHeadHeight") + (mMaxHeight - Y) / scaleValue * ((Integer) params.get("trendHeight") / 8));
}
if (i == startPosition) {
mPath.moveTo(i * mDistance, currentY);
} else {
mPath.lineTo(i * mDistance, currentY);
}
}
}
canvas.drawPath(mPath, paint);
canvas.save();
mPath.reset();
}
效果图如下:
- 第六层绘制圆点
/**
* 绘制圆
*
* @param canvas
* @param paint
* @param canvasLine
* @param color
* @param condition
*/
private void drawCircles(Canvas canvas, Paint paint, List<Integer> canvasLine, int color, ArrayList<Integer> condition) {
ArrayList<Integer> LinePosition = dealCanvasData(canvasLine);
int startPosition = LinePosition.get(0);
int endPosition = LinePosition.get(1);
Map<String, Object> params = getViewParams();
mOuterCirclePaint.setStrokeWidth(mTrendLineSize);
mInnerCirclePaint.setStrokeWidth(mTrendLineSize);
mOuterCirclePaint.setColor(color);
if (endPosition > startPosition && endPosition > 0) {
for (int i = startPosition; i < endPosition; i++) {
int currentY = (int) ((Integer) params.get("xAxisHeadHeight") + (mMaxHeight - canvasLine.get(i)) / mScaleValue * ((Integer) params.get("trendHeight") / 8));
// 处理为负的数据,不需要可以屏蔽
if (canvasLine.get(i) < 0) {
double Y = canvasLine.get(i) * 32.0 / 40.0;
currentY = (int) ((Integer) params.get("xAxisHeadHeight") + (mMaxHeight - Y) / scaleValue * ((Integer) params.get("trendHeight") / 8));
}
if (canvasLine.get(i) > condition.get(1) || canvasLine.get(i) < condition.get(0)) {
drawCircle(canvas, i * mDistance, currentY); // 实心
} else {
// 下面需要对 90~140的数据处理
drawCirque(canvas, i * mDistance, currentY); // 空心
}
}
}
}
/**
* 绘制实心圆
*
* @param canvas
* @param positionX
* @param positionY
*/
private void drawCircle(Canvas canvas, int positionX, int positionY) {
canvas.drawCircle(positionX, positionY, mOuterCircleRadius, mOuterCirclePaint);
}
/**
* 绘制空心圆
*
* @param canvas
* @param positionX
* @param positionY
*/
private void drawCirque(Canvas canvas, int positionX, int positionY) {
canvas.drawCircle(positionX, positionY, mOuterCircleRadius, mOuterCirclePaint);
canvas.drawCircle(positionX, positionY, mInnerCircleRadius, mInnerCirclePaint);
}
效果图如下:
- 第七层绘制X轴Title文字
/**
* 绘制x轴Title文字
*
* @param canvas
* @param paint
* @param data
*/
private void drawXTitle(Canvas canvas, Paint paint, List<String[]> data) {
paint.setColor(0xff888888);
paint.setTextSize(mTextColorSize);
paint.setStyle(Style.FILL);
List<Integer> maxItem = getMaxItem();
Map<String, Object> params = getViewParams();
int startPosition = ((Integer) params.get("firstPosition") - (Integer) params.get("offsetCount")) >= 0 ? ((Integer) params.get("firstPosition") - (Integer) params.get("offsetCount")) : 0;
int endPosition = maxItem.size();
if (endPosition > startPosition && endPosition > 0) {
float textBaseY_x_up = (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1) + 40;
for (int i = startPosition; i < endPosition; i++) {
drawCenterTextColor(i);
// draw x axis up
mYTitleRect.set(mDistance * (i - 1), (getHeight() - (Integer) params.get("xAxisHeight_up") - (Integer) params.get("xAxisHeight_blow") - 10), mDistance * (i + 1), getHeight() - (Integer) params.get("xAxisHeight_blow") - 10);
canvas.drawText(data.get(i)[0].toString(), mYTitleRect.centerX(), textBaseY_x_up, paint);
}
}
}
- 第八层绘制Y轴Title文字
/**
* 绘制Y轴Title文字
*
* @param canvas
* @param paint
* @param backgroundColor
* @param textColor
*/
private void drawYTitle(Canvas canvas, Paint paint, int backgroundColor, int textColor) {
Map<String, Object> params = getViewParams();
FontMetrics fontMetrics = mPaint.getFontMetrics();
float fontHeight = fontMetrics.bottom - fontMetrics.top;
// 由于折线图是左右贯通的,Y轴Title在画布上会造成显示混乱,所以添加底部遮挡
paint.setColor(backgroundColor);
paint.setStyle(Style.FILL);
mYTitleRect.set((int) ((Float) params.get("scrollX") + 0), 0, (int) ((Float) params.get("scrollX") + mYTitleWidth) - 30, getHeight() - 80);
canvas.drawRect(mYTitleRect, paint);
// and y-axis values
paint.setColor(textColor);
paint.setTextSize(mTextColorSize);
//绘制Y轴值
for (int i = 0; i <= 8; i++) {
String showTitle;
if (i >= 6 && i <= 8) {
showTitle = 40 * (6 - i) + 120 + "";
} else {
showTitle = 40 + (5 - i) * 35 + "";
}
float textBaseY = ((Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (i - 1)) * 2 - (((Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (i - 1)) * 2 - fontHeight) / 2 - fontMetrics.bottom;
canvas.drawText(showTitle, ((Float) params.get("scrollX") + mYTitleWidth / 2) - 20, textBaseY, paint);
}
}
- 在onDraw中添加绘制View代码
@Override
protected void onDraw(Canvas canvas) {
mDistance = (getWidth() - mYTitleWidth) / mDrawCount;
currentCenter = (getWidth() - mDistance);
if (mCenterPosition == -1) {
int positionLocal = mCenterRecorded * mDistance;
scrollTo(positionLocal - currentCenter, 0); // 根据可显示的区域 动态计算中点
mCenterPosition = 0;
}
// 设置字体、笔画宽度
// mTitlePaint.setTypeface(Typeface.DEFAULT_BOLD);
// mTitlePaint.setStrokeWidth(4);
// draw trend background
// 设置dataOne界限的区域
drawBackground(canvas, mRangeTrendBackgroundPaint, mRangeTrendColors, RatioUp, RatioDown);
// 设置dataTwo界限的区域
drawBackground(canvas, nRangeTrendBackgroundPaint, nRangeTrendColors, colorUp, colorDown);
// 设置dataThree界限的区域
drawBackground(canvas, mPulsePaint, mPulseColors, mPulseUp, mPulseDown);
// draw form
drawForm(canvas, mTitlePaint);
// split form
setSplitForm(canvas, mTitlePaint, Color.WHITE, 2);
// draw sign
drawCenterSign(canvas, mTitlePaint, 6);
if (mPoints != null) {
// draw canvas line
drawCanvasLine(canvas, mPaint, mPoints[0], mPointColors[0]);
drawCanvasLine(canvas, mPaint, mPoints[1], mPointColors[1]);
// draw circles
ArrayList<Integer> conditionsOne = new ArrayList<>();
conditionsOne.add(90);
conditionsOne.add(140);
ArrayList<Integer> conditionsTwo = new ArrayList<>();
conditionsTwo.add(60);
conditionsTwo.add(90);
drawCircles(canvas, mPaint, mPoints[0], mPointColors[0], conditionsOne);
drawCircles(canvas, mPaint, mPoints[1], mPointColors[1], conditionsTwo);
}
if (mData != null) {
// draw canvas line
drawCanvasLine(canvas, mPaint, mData[0], mPulseColor);
// draw circles
ArrayList<Integer> conditionsThree = new ArrayList<>();
conditionsThree.add(-70);
conditionsThree.add(-10);
drawCircles(canvas, mPaint, mData[0], mPulseColor, conditionsThree);
}
//and x-axis values
if (mXAxisValues != null && mXAxisValues.size() > 0) {
drawXTitle(canvas, mTitlePaint, mXAxisValues);
}
// draw y r xis rect
drawYTitle(canvas, mTitlePaint, Color.WHITE, 0xff888888);
}
效果图如下:
现在咱们的界面绘制完成,要记住顺序,不然会遮挡。然后咱们实现监听。
二、监听事件,实现逻辑
- 实现onTouchEvent监听事件逻辑
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
oldX = (int) event.getX();
if ((mIsBeingDragged)) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
invalidate();
mActivePointerId = event.getPointerId(0);
return true;
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = event.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
break;
}
final int x = (int) event.getX(activePointerIndex);
int deltaX = oldX - x;
if (!mIsBeingDragged && Math.abs(deltaX) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaX > 0) {
deltaX -= mTouchSlop;
} else {
deltaX += mTouchSlop;
}
}
// HorizontalScrollView
if (mIsBeingDragged && Math.abs(deltaX) > mTouchSlop) {
oldX = x;
mTowards = deltaX;
scrollBy(deltaX, 0);
invalidate();
if (mPCenterListener != null) {
int nextCenter = getToNextCenter();
mPCenterListener.passCenter(nextCenter);
}
}
invalidate();
return true;
default:
if (mIsBeingDragged) {
mIsBeingDragged = false;
int nextCenter = getToNextCenter();
mTowards = 0;
int halfWidth = currentCenter;
int positionLocal = nextCenter * mDistance;
scrollTo(positionLocal - halfWidth, 0);
if (mAEndListener != null) {
mAEndListener.actionEnd(nextCenter);
invalidate();
centerPosition = nextCenter;
}
} else {
}
invalidate();
return true;
}
return super.onTouchEvent(event);
}
在监听事件中不只有左右滑动监听,还添加了滑动过中心的监听。这里道长不多说了,这篇博客还是这么干巴巴的,以后道长可能只贴代码了[手动滑稽]。有不明白的地方看Demo。现在自定义View,自定义组合View道长都说了,怎么会不说一下自定义属性,这个在自定义View中也是很常用的。咱们在开一篇博客,画布实现自定义View暂时到这里,希望这篇博客能为你提供一些帮助。
源码下载
推荐阅读
-
小弟我用织梦的自定义表单做了一个人员录入 现在想在前台实现搜索功能 怎么做
-
Android自定义View实现仿1号店垂直滚动广告条代码
-
Android 自定义通用的loadingview实现代码
-
Android自定义View实现仿GitHub的提交活跃表格
-
ThinkPHP自定义Redis处理SESSION的实现方法,thinkphpredis_PHP教程
-
C#实现自定义定时组件的方法
-
SpringAOP拦截Controller,Service实现日志管理(自定义注解的方式)(转载) 博客分类: Spring框架 aopspringaspectj
-
使用Spring自定义注解实现任务路由的方法
-
C#使用yield关键字让自定义集合实现foreach遍历的方法
-
Android编程使用自定义View实现水波进度效果示例