共计 9389 个字符,预计需要花费 24 分钟才能阅读完成。
导读 | 这篇文章主要为大家详细介绍了 android 实现可以滑动的平滑曲线图,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 |
本文实例为大家分享了 android 实现可以滑动的平滑曲线图的具体代码,供大家参考,具体内容如下
直接上代码,里面有详细注解
1 attr 属性编写
2 ChartView
package com.laisontech.commonuilibrary.customviews; | |
import android.animation.Animator; | |
import android.animation.ValueAnimator; | |
import android.content.Context; | |
import android.content.res.TypedArray; | |
import android.graphics.Canvas; | |
import android.graphics.Color; | |
import android.graphics.Paint; | |
import android.graphics.Path; | |
import android.graphics.PorterDuff; | |
import android.graphics.PorterDuffXfermode; | |
import android.graphics.Rect; | |
import android.graphics.RectF; | |
import android.util.AttributeSet; | |
import android.util.Log; | |
import android.util.TypedValue; | |
import android.view.MotionEvent; | |
import android.view.VelocityTracker; | |
import android.view.View; | |
import android.view.animation.DecelerateInterpolator; | |
import com.laisontech.commonuilibrary.R; | |
import java.util.ArrayList; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
/** | |
* 自定义折线图 | |
*/ | |
public class ChartView extends View { | |
private static final int FIRST = 1; | |
private static final int MIDDLE = 2; | |
private static final int END = 3; | |
//xy 坐标轴颜色 | |
private int xyLineColor = 0xffCFE2CF; | |
// 折线选中的圆形颜色 | |
private int selectCircleColor = 0xff00A8FF; | |
// 选中数据提示框颜色 | |
private int selectReminderColor = 0xff00A8FF; | |
// 折线中圆形内部部颜色 | |
private int xyTextColor = 0xff0014FF; | |
// 折线图中折线的颜色 | |
private int lineColor = 0xffFD00FF; | |
//xy 坐标轴宽度 | |
private int xyLineWidth = dpToPx(1); | |
//xy 坐标轴文字大小 | |
private int xyTextSize = spToPx(12); | |
// x 轴各个坐标点水平间距 | |
private int interval = dpToPx(40); | |
// 背景颜色 | |
private int bgColor = 0xffffffff; | |
// 是否有起手时的滑动感 | |
private boolean isScroll = false; | |
// 提示框显示位置 | |
private int mShowPositionType = 3; | |
// 绘制 XY 轴坐标对应的画笔 | |
private Paint mXYPaint; | |
// 绘制 XY 轴的文本对应的画笔 | |
private Paint mXYTextPaint; | |
// 画折线对应的画笔 | |
private Paint mSpinnerLinePaint; | |
private int width; | |
private int height; | |
// x 轴的原点坐标 | |
private int mXOri; | |
// y 轴的原点坐标 | |
private int mYOri; | |
// 第一个点 X 的坐标 | |
private float mXInit; | |
// 第一个点对应的最大 X 坐标 | |
private float maxXInit; | |
// 第一个点对应的最小 X 坐标 | |
private float minXInit; | |
// x 轴坐标对应的数据 | |
private List mXData = new ArrayList(); | |
// y 轴坐标对应的数据 | |
private List mYData = new ArrayList(); | |
// 折线对应的数据 | |
private Map mSpinnerValue = new HashMap(); | |
// 点击的点对应的 X 轴的第几个点,默认 1 | |
private int selectIndex = 1; | |
// X 轴刻度文本对应的最大矩形,为了选中时,在 x 轴文本画的框框大小一致,获取从数据中得到的 x 轴数据,获得最长数据 | |
private Rect xValueRect; | |
// 速度检测器 | |
private VelocityTracker mTracker; | |
// 是否为短距离滑动 | |
private boolean isShortSlide = false; | |
// 获取尺寸的的中间 | |
private int mSelectMiddle = 0; | |
// 曲线切率 | |
private float mLineSmoothness = 0.18f; | |
public ChartView(Context context) {this(context, null); | |
} | |
public ChartView(Context context, AttributeSet attrs) {this(context, attrs, 0); | |
} | |
public ChartView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr); | |
init(context, attrs, defStyleAttr); | |
initPaint();} | |
// 设置切率 | |
public void setLineSmoothness(float lineSmoothness) {if (lineSmoothness != this.mLineSmoothness) {this.mLineSmoothness = lineSmoothness;} | |
} | |
/** | |
* 初始化 | |
*/ | |
private void initPaint() {mXYPaint = new Paint(); | |
mXYPaint.setAntiAlias(true); | |
mXYPaint.setStrokeWidth(xyLineWidth); | |
mXYPaint.setStrokeJoin(Paint.Join.ROUND); | |
mXYPaint.setColor(xyLineColor); | |
mXYTextPaint = new Paint(); | |
mXYTextPaint.setAntiAlias(true); | |
mXYTextPaint.setTextSize(xyTextSize); | |
mXYTextPaint.setStrokeJoin(Paint.Join.ROUND); | |
mXYTextPaint.setColor(xyTextColor); | |
mXYTextPaint.setStyle(Paint.Style.STROKE); | |
mSpinnerLinePaint = new Paint(); | |
mSpinnerLinePaint.setAntiAlias(true); | |
mSpinnerLinePaint.setStrokeWidth(xyLineWidth); | |
mSpinnerLinePaint.setColor(lineColor); | |
mSpinnerLinePaint.setStyle(Paint.Style.STROKE); | |
mSpinnerLinePaint.setStrokeJoin(Paint.Join.ROUND); | |
} | |
/** | |
* 初始化 | |
* | |
* @param context | |
* @param attrs | |
* @param defStyleAttr | |
*/ | |
private void init(Context context, AttributeSet attrs, int defStyleAttr) {TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ChartView, defStyleAttr, 0); | |
int count = array.getIndexCount(); | |
for (int i = 0; i textYWdith) | |
textYWdith = temp; | |
} | |
int dp2 = dpToPx(2); | |
int dp3 = dpToPx(3); | |
mXOri = (int) (dp2 + textYWdith + dp2 + xyLineWidth); | |
// 获取 x 轴的最长文本的宽度所占的矩形 | |
xValueRect = getTextBounds(mXData.get(getListItemMaxIndex(mXData)), mXYTextPaint); | |
// X 轴文本高度 | |
float textXHeight = xValueRect.height(); | |
for (int i = 0; i textXHeight) | |
textXHeight = rect.height(); | |
if (rect.width() > xValueRect.width()) | |
xValueRect = rect; | |
} | |
mYOri = (int) (height - dp2 - textXHeight - dp3 - xyLineWidth); | |
mXInit = mXOri + xValueRect.width() / 2 + dpToPx(5); | |
minXInit = width - (width - mXOri) * 0.1f - interval * (mXData.size() - 1); | |
maxXInit = mXInit; | |
} | |
selectIndex = getSelectIndexFromShowType(mShowPositionType); | |
super.onLayout(changed, left, top, right, bottom); | |
} | |
protected void onDraw(Canvas canvas) {canvas.drawColor(bgColor); | |
drawXY(canvas); | |
drawBrokenLineAndPoint(canvas); | |
} | |
/** | |
* 绘制交点处对应的点 | |
*/ | |
private void drawBrokenLineAndPoint(Canvas canvas) {if (mXData.size() 0) {previousPointX = getSpinnerPoint(i - 1).x; | |
previousPointY = getSpinnerPoint(i - 1).y; | |
} else { | |
// 用当前点表示上一个点 | |
previousPointX = currentPointX; | |
previousPointY = currentPointY; | |
} | |
} | |
if (Float.isNaN(prePreviousPointX)) { | |
// 是前两个点? | |
if (i > 1) {prePreviousPointX = getSpinnerPoint(i - 2).x; | |
prePreviousPointY = getSpinnerPoint(i - 2).y; | |
} else { | |
// 当前点表示上上个点 | |
prePreviousPointX = previousPointX; | |
prePreviousPointY = previousPointY; | |
} | |
} | |
// 判断是不是最后一个点了 | |
if (i = mXOri) {// 只绘制从原点开始的区域 | |
mXYTextPaint.setColor(xyTextColor); | |
canvas.drawLine(x, mYOri, x, mYOri - length, mXYPaint); | |
// 绘制 X 轴文本 | |
String text = mXData.get(i); | |
Rect rect = getTextBounds(text, mXYTextPaint); | |
if (i == selectIndex - 1) {mXYTextPaint.setColor(lineColor); | |
canvas.drawText(text, 0, text.length(), x - rect.width() / 2, mYOri + xyLineWidth + dpToPx(2) + rect.height(), mXYTextPaint); | |
canvas.drawRoundRect(x - xValueRect.width() / 2 - dpToPx(3), mYOri + xyLineWidth + dpToPx(1), x + xValueRect.width() / 2 + dpToPx(3), mYOri + xyLineWidth + dpToPx(2) + xValueRect.height() + dpToPx(2), dpToPx(2), dpToPx(2), mXYTextPaint); | |
} else {canvas.drawText(text, 0, text.length(), x - rect.width() / 2, mYOri + xyLineWidth + dpToPx(2) + rect.height(), mXYTextPaint); | |
} | |
} | |
} | |
} | |
private float startX; | |
private float startx; | |
public boolean onTouchEvent(MotionEvent event) {if (isScrolling) | |
return super.onTouchEvent(event); | |
// 当该 view 获得点击事件,就请求父控件不拦截事件 | |
this.getParent().requestDisallowInterceptTouchEvent(true); | |
obtainVelocityTracker(event); | |
switch (event.getAction()) { | |
case MotionEvent.ACTION_DOWN: | |
startX = event.getX(); | |
startx = event.getX(); | |
Log.e("XXXX", "down:" + startX + ""); | |
break; | |
case MotionEvent.ACTION_MOVE: | |
// 滑动距离小于等于 8 的时候任务为短距离滑动 | |
// 当前 x 轴的尺寸与设置的 x 轴间隔的距离之乘积大于 屏幕中的显示布局宽度与 x 轴七点之差时,开始移动 | |
if (interval * mXData.size() > width - mXOri) { | |
// 获取滑动的距离 | |
float dis = event.getX() - startX; | |
// 重新赋值给 startX | |
startX = event.getX(); | |
// 当前 x 原点距离与左右滑动的距离之和没有最小值大,则将当前 x 距离赋值为最小,以下相似 | |
if (mXInit + dis maxXInit) {mXInit = maxXInit;} else {mXInit = mXInit + dis;} | |
invalidate();} | |
break; | |
case MotionEvent.ACTION_UP: | |
isShortSlide = Math.abs(event.getX() - startx) minXInit) {// 向左滑动 | |
if (mXInit - value 0 && mXInit = maxXInit) | |
mXInit = maxXInit; | |
else | |
mXInit = mXInit + value; | |
} | |
invalidate();} | |
}); | |
animator.addListener(new Animator.AnimatorListener() { | |
public void onAnimationStart(Animator animator) {isScrolling = true;} | |
public void onAnimationEnd(Animator animator) {isScrolling = false;} | |
public void onAnimationCancel(Animator animator) {isScrolling = false;} | |
public void onAnimationRepeat(Animator animator) {}}); | |
animator.start();} | |
/** | |
* 获取速度 | |
* | |
* @return | |
*/ | |
private float getVelocity() {if (mTracker != null) {mTracker.computeCurrentVelocity(1000); | |
return mTracker.getXVelocity();} | |
return 0; | |
} | |
/** | |
* 点击 X 轴坐标或者折线节点 | |
* */ | |
// 44 142 139 | |
private void clickAction(MotionEvent event) {int dp8 = dpToPx(8); | |
float eventX = event.getX(); | |
float eventY = event.getY(); | |
if (!isShortSlide) {for (int i = 0; i = start + (mSelectMiddle - 1) * interval && x = x - dp8 && eventX = y - dp8 && eventY = x - rect.width() / 2 - dp8 && eventX = y - dp8 && eventY value) { | |
this.mSpinnerValue = value; | |
invalidate();} | |
public void setValue(Map value, List xValue, List yValue) { | |
this.mSpinnerValue = value; | |
this.mXData = xValue; | |
this.mYData = yValue; | |
invalidate();} | |
public Map getValue() {return mSpinnerValue;} | |
/** | |
* 获取丈量文本的矩形 | |
* | |
* @param text | |
* @param paint | |
* @return | |
*/ | |
private Rect getTextBounds(String text, Paint paint) {Rect rect = new Rect(); | |
paint.getTextBounds(text, 0, text.length(), rect); | |
return rect; | |
} | |
/** | |
* dp 转化成为 px | |
* | |
* @param dp | |
* @return | |
*/ | |
private int dpToPx(int dp) {float density = getContext().getResources().getDisplayMetrics().density; | |
return (int) (dp * density + 0.5f * (dp >= 0 ? 1 : -1)); | |
} | |
/** | |
* sp 转化为 px | |
* | |
* @param sp | |
* @return | |
*/ | |
private int spToPx(int sp) {float scaledDensity = getContext().getResources().getDisplayMetrics().scaledDensity; | |
return (int) (scaledDensity * sp + 0.5f * (sp >= 0 ? 1 : -1)); | |
} | |
/** | |
* 获取集合中最长的 index | |
*/ | |
private static final int NULL_INDEX = -1; | |
public int getListItemMaxIndex(List> data) {if (data == null || data.size() max) {return i;} | |
} | |
return NULL_INDEX; | |
} | |
// 获得在滑动结束的时候在屏幕内的点 | |
private int middleIndex(int size) {if (size % 2 == 0) {return size / 2;} else {return size / 2 + 1;} | |
} | |
/** | |
* 根据两点坐标获取中间某个点 | |
* | |
* @param from 坐标 1 | |
* @param to 坐标 2 | |
*/ | |
// 获取已知点的斜率 y = kx+b | |
private float getSlope(Point from, Point to) {float k = (to.y - from.y) / (to.x - from.x); | |
Log.e("Point", "参数 b:" + k); | |
return k; | |
} | |
// 获取参数 b | |
private float getParams(Point from, Point to) {float b = from.y - (getSlope(from, to) * from.x); | |
Log.e("Point", "参数 b:" + b); | |
return b; | |
} | |
// 根据两点间的坐标获取 x 轴的任意一个坐标 x 值, | |
private float getArbitrarilyX(Point from, Point to, int grade, int needGrade) { | |
// 获得输入的新坐标 | |
float x = ((to.x - from.x) * needGrade) / grade + from.x; | |
Log.e("Point", "x 坐标值:" + x); | |
return x; | |
} | |
// 获取坐标值 | |
private Point getPoint(Point from, Point to, int grade, int needGrade) {Point point = new Point(); | |
point.setX(getArbitrarilyX(from, to, grade, needGrade)); | |
float slope = getSlope(from, to); | |
point.setY(slope * point.x + getParams(from, to)); | |
return point; | |
} | |
// 获取绘制折线的点 | |
private Point getSpinnerPoint(int valueIndex) {float x = mXInit + interval * (valueIndex); | |
float y = mYOri - mYOri * (1 - 0.1f) * mSpinnerValue.get(mXData.get(valueIndex)) / mYData.get(mYData.size() - 1); | |
return new Point(x, y); | |
} | |
private class Point { | |
float x; | |
float y; | |
public Point() {} | |
public float getX() {return x;} | |
public void setX(float x) {this.x = x;} | |
public float getY() {return y;} | |
public void setY(float y) {this.y = y;} | |
public Point(float x, float y) { | |
this.x = x; | |
this.y = y; | |
} | |
public String toString() { | |
return "Point{" + | |
"x=" + x + | |
", y=" + y + | |
'}'; | |
} | |
} | |
} |
以上就是本文的全部内容,希望对大家的学习有所帮助。
正文完
星哥玩云-微信公众号
