最近看到一个合适的开源库,是线性图表的,感觉以后可能会用到,这里就写下,因为作者写的直接用类,自己也好修改。给你们链接吧点击打开链接
一、简介首先适用于API14以上的(感觉这点很致命,不知道能不能规避)
静态属性 | 对应API | 说明 |
---|---|---|
xySize | setXySize | xy轴文字大小 |
xyColor | setXyColor | xy轴文字的颜色,包含轴线 |
lineType | setLineType | 指定line类型:CURVE / SEGMENT(曲线/线段) |
Style | setLineStyle | 指定line的风格:DASHED / SOLID(虚线/实线) |
needEdgeEffect | disableEdgeEffect | 关闭边缘效果,默认开启 |
colorEdgeEffect | setEdgeEffectColor | 指定边缘效果的颜色,默认为Color.GRAY |
needClickHint | disableClickHint | 关闭点击提示信息,默认开启 |
colorHint | setHintColor | 设置提示辅助线、文字颜色 |
maxOfVisible | / | 一组数据在可见区域中的最大可见点数,至少>=2 |
countOfY | / | y轴刻度数,至少>=1 |
/ | setLineSize | 设置line在非填充形态时的大小 |
/ | setLineForm | 设置line的形态:是否填充,默认为false |
对于一条line,可以直接调用feed或feedWithAnim方法:
List lines = new ArrayList<>();
for (int i = 0; i < 14; i++) {
lines.add(new Unit(new SecureRandom().nextInt(48), i + ""));
}
suitLines.feedWithAnim(lines);
如果是多条数据,则需要通过Builder来实现:
SuitLines.LineBuilder builder = new SuitLines.LineBuilder();
for (int j = 0; j < count; j++) {
List lines = new ArrayList<>();
for (int i = 0; i < 50; i++) {
lines.add(new Unit(new SecureRandom().nextInt(128), "" + i));
}
builder.add(lines, new int[]{...});//第一个参数是线,第二个参数是颜色
}
builder.build(suitLines, true);
代码public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private int[] color = {Color.RED, Color.GRAY, 0xFFF76055, 0xFF9B3655, 0xFFF7A055};
private SuitLines suitLines;
private int count;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
suitLines = (SuitLines) findViewById(R.id.suitlines);
findViewById(R.id.btn_add).setOnClickListener(this);
findViewById(R.id.btn_sub).setOnClickListener(this);
findViewById(R.id.btn_type).setOnClickListener(this);
findViewById(R.id.btn_type2).setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.btn_add:
count++;
changData();
break;
case R.id.btn_sub:
count--;
changData();
break;
case R.id.btn_type:
suitLines.setLineType(suitLines.getLineType() == SuitLines.CURVE ? SuitLines.SEGMENT : SuitLines.CURVE);
break;
case R.id.btn_type2:
suitLines.setLineStyle(suitLines.isLineDashed()?SuitLines.SOLID:SuitLines.DASHED);
break;
}
}
public void changData(){
Log.e("count是:",count+"");
if (count <= 0) {
count = 0;
}
if (count == 1) {
List lines = new ArrayList<>();
for (int i = 0; i < 14; i++) {
lines.add(new Unit(new SecureRandom().nextInt(48), i + "d"));
}
suitLines.feedWithAnim(lines);
return;
}
SuitLines.LineBuilder builder = new SuitLines.LineBuilder();
for (int j = 0; j < count; j++) {
List lines = new ArrayList<>();
for (int i = 0; i < 50; i++) {
lines.add(new Unit(new SecureRandom().nextInt(128), "" + i));
}
builder.add(lines, new int[]{color[new SecureRandom().nextInt(4)], color[new SecureRandom().nextInt(4)], color[new SecureRandom().nextInt(4)]});
}
builder.build(suitLines, true);
}
}
Util
class Util {
static int getCeil5(float num) {
return ((int) (num + 4.9f)) / 5 * 5;
}
static float calcTextSuitBaseY(RectF rectF, Paint paint) {
return rectF.top + rectF.height() / 2 -
(paint.getFontMetrics().ascent + paint.getFontMetrics().descent) / 2;
}
static float size2sp(float sp, Context context) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
sp, context.getResources().getDisplayMetrics());
}
static int dip2px(float dipValue) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
static float getTextHeight(Paint textPaint) {
return -textPaint.ascent() - textPaint.descent();
}
static void trySetColorForEdgeEffect(EdgeEffect edgeEffect, int color) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
edgeEffect.setColor(color);
return;
}
try {
Field edgeField = EdgeEffect.class.getDeclaredField("mEdge");
edgeField.setAccessible(true);
Drawable mEdge = (Drawable) edgeField.get(edgeEffect);
mEdge.setColorFilter(color, PorterDuff.Mode.SRC_IN);
mEdge.setCallback(null);
Field glowField = EdgeEffect.class.getDeclaredField("mGlow");
glowField.setAccessible(true);
Drawable mGlow = (Drawable) glowField.get(edgeEffect);
mGlow.setColorFilter(color, PorterDuff.Mode.SRC_IN);
mGlow.setCallback(null);
} catch (Exception ignored) {
}
}
}
Unit
public class Unit implements Comparable, Cloneable {
static long DURATION = 800;
private ValueAnimator VALUEANIMATOR = ValueAnimator.ofFloat(0, 1);
/**
* 当前点的值
*/
private float value;
// 当前点的额外信息(可选,x轴)
private String extX;
/**
* 当前点的坐标信息,都是相对的canvas而不是linesArea
*/
private PointF xy;
/**
* 当前点的动画进度,
* 默认为1表示无动画
*/
private float percent = 1f;
public Unit(float value) {
this.value = value;
}
public Unit(float value, String extX) {
this.value = value;
this.extX = extX;
}
public float getValue() {
return value;
}
void setXY(PointF xy) {
this.xy = xy;
}
PointF getXY() {
return xy;
}
void setPercent(float percent) {
this.percent = percent;
}
float getPercent() {
return percent;
}
public void setExtX(String extX) {
this.extX = extX;
}
String getExtX() {
return extX;
}
void cancelToEndAnim() {
if (VALUEANIMATOR.isRunning()) {
VALUEANIMATOR.cancel();
}
percent = 1f;
}
void startAnim(TimeInterpolator value) {
// 如果value小于一定阈值就不开启动画
if (VALUEANIMATOR.isRunning() || (int)this.value <= 1) {
return;
}
VALUEANIMATOR.setFloatValues(0, 1);
VALUEANIMATOR.setDuration(DURATION);
VALUEANIMATOR.setInterpolator(value);
VALUEANIMATOR.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
percent = (float) animation.getAnimatedValue();
}
});
VALUEANIMATOR.start();
}
@Override
public int compareTo(Unit o) {
if (value == o.value) {
return 0;
} else if (value > o.value) {
return 1;
} else {
return -1;
}
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Unit)) return false;
Unit unit = (Unit) obj;
return value == unit.value
&& (extX == unit.extX) || (extX != null && extX.equals(unit.extX));
}
@Override
public String toString() {
return "Unit{" +
"xy=" + xy +
'}';
}
@Override
protected Unit clone() {// 转化为深度拷贝,防止在集合中排序时的引用问题
try {
return (Unit) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
SuitLines
public class SuitLines extends View {
public static final String TAG = SuitLines.class.getSimpleName();
public SuitLines(Context context) {
this(context, null);
}
public SuitLines(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SuitLines(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initOptionalState(context, attrs);
basePadding = Util.dip2px(basePadding);
maxVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
clickSlop = ViewConfiguration.get(context).getScaledEdgeSlop();
scroller = new Scroller(context);
edgeEffectLeft = new EdgeEffect(context);
edgeEffectRight = new EdgeEffect(context);
setEdgeEffectColor(edgeEffectColor);
basePaint.setColor(defaultLineColor[0]);
basePaint.setStyle(Paint.Style.STROKE);
basePaint.setStrokeWidth(4);
setLineStyle(SOLID);
xyPaint.setTextSize(Util.size2sp(defaultXySize, getContext()));
xyPaint.setColor(defaultXyColor);
hintPaint.setTextSize(Util.size2sp(12, getContext()));
hintPaint.setColor(hintColor);
hintPaint.setStyle(Paint.Style.STROKE);
hintPaint.setStrokeWidth(2);
hintPaint.setTextAlign(Paint.Align.CENTER);
}
private void initOptionalState(Context ctx, AttributeSet attrs) {
TypedArray ta = ctx.obtainStyledAttributes(attrs, R.styleable.suitlines);
defaultXySize = ta.getFloat(R.styleable.suitlines_xySize, defaultXySize);
defaultXyColor = ta.getColor(R.styleable.suitlines_xyColor, defaultXyColor);
lineType = ta.getInt(R.styleable.suitlines_lineType, CURVE);
lineStyle = ta.getInt(R.styleable.suitlines_lineStyle, SOLID);
needEdgeEffect = ta.getBoolean(R.styleable.suitlines_needEdgeEffect, needEdgeEffect);
edgeEffectColor = ta.getColor(R.styleable.suitlines_colorEdgeEffect, edgeEffectColor);
needShowHint = ta.getBoolean(R.styleable.suitlines_needClickHint, needShowHint);
hintColor = ta.getColor(R.styleable.suitlines_colorHint, hintColor);
maxOfVisible = ta.getInt(R.styleable.suitlines_maxOfVisible, maxOfVisible);
countOfY = ta.getInt(R.styleable.suitlines_countOfY, countOfY);
ta.recycle();
}
// 创建自己的Handler,与ViewRootImpl的Handler隔离,方便detach时remove。
private Handler handler = new Handler(Looper.getMainLooper());
// 遍历线上点的动画插值器
private TimeInterpolator linearInterpolator = new LinearInterpolator();
// 每个数据点的动画插值
private TimeInterpolator pointInterpolator = new OvershootInterpolator(3);
private RectF linesArea, xArea, yArea, hintArea;
/**
* 默认画笔
*/
private Paint basePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/**
* x,y轴对应的画笔
*/
private Paint xyPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/**
* 点击提示的画笔
*/
private Paint hintPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/**
* 默认画笔的颜色,索引0位置为画笔颜色,整个数组为shader颜色
*/
private int[] defaultLineColor = {Color.RED, 0xFFF35E54, Color.YELLOW};
private int hintColor = Color.RED;
/**
* xy轴文字颜色和大小
*/
private int defaultXyColor = Color.GRAY;
private float defaultXySize = 8;
/**
* 每根画笔对应一条线
*/
private List paints = new ArrayList<>();
private List paths = new ArrayList<>();
/**
* 约定:如果需要实现多组数据,那么每组数据的长度必须相同!
* 多组数据的数据池;
* Key:一组数据的唯一标识,注意:要求连续且从0开始
* value:一组数据
*/
private Map> datas = new HashMap<>();
/**
* 所有数据集的动画
*/
private List animators = new ArrayList<>();
/**
* line的点击效果
*/
private ValueAnimator clickHintAnimator;
/**
* 当前正在动画的那组数据
*/
private int curAnimLine;
/**
* 整体动画的起始时间
*/
private long startTimeOfAnim;
/**
* 是否正在整体动画中
*/
private boolean isAniming;
/**
* 两个点之间的动画启动间隔,大于0时仅当总数据点<可见点数时有效
*/
private long intervalOfAnimCost = 100;
/**
* 可见区域中,将一组数据遍历完总共花费的最大时间
*/
private long maxOfAnimCost = 1000;
/**
* 一组数据在可见区域中的最大可见点数,至少>=2
*/
private int maxOfVisible = 7;
/**
* 文本之间/图表之间的间距
*/
private int basePadding = 4;
/**
* y轴刻度数,至少>=1
*/
private int countOfY = 5;
/**
* y轴的缓存,提高移动效率
*/
private Bitmap yAreaBuffer;
/**
* y轴的最大刻度值,保留一位小数
*/
private float maxValueOfY;
/**
* 根据可见点数计算出的两点之间的距离
*/
private float realBetween;
/**
* 手指/fling的上次位置
*/
private float lastX;
/**
* 滚动当前偏移量
*/
private float offset;
/**
* 滚动上一次的偏移量
*/
private float lastOffset;
/**
* 滚动偏移量的边界
*/
private float maxOffset;
/**
* fling最大速度
*/
private int maxVelocity;
// 点击y的误差
private int clickSlop;
/**
* 判断左/右方向,当在边缘就不触发fling,以优化性能
*/
float orientationX;
private VelocityTracker velocityTracker;
private Scroller scroller;
private EdgeEffect edgeEffectLeft, edgeEffectRight;
// 对于fling,仅吸收到达边缘时的速度
private boolean hasAbsorbLeft, hasAbsorbRight;
/**
* 是否需要边缘反馈效果
*/
private boolean needEdgeEffect = true;
private int edgeEffectColor = Color.GRAY;
/**
* 点击是否弹出额外信息
*/
private boolean needShowHint = true;
/**
* 实际的点击位置,0为x索引,1为某条line
*/
private int[] clickIndexs;
private float firstX, firstY;
/**
* 控制是否强制重新生成path,当改变lineType/paint时需要
*/
private boolean forceToDraw;
/**
* lines在当前可见区域的边缘点
*/
private int[] suitEdge;
// 曲线、线段
public static final int CURVE = 0;
public static final int SEGMENT = 1;
private int lineType = CURVE;
public static final int SOLID = 0;
public static final int DASHED = 1;
private int lineStyle = SOLID;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
calcAreas();
basePaint.setShader(buildPaintColor(defaultLineColor));
if (!datas.isEmpty()) {
calcUnitXY();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (datas.isEmpty() || isAniming) {
recycleVelocityTracker();
return false;
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
firstX = lastX = event.getX();
firstY = event.getY();
scroller.abortAnimation();
initOrResetVelocityTracker();
velocityTracker.addMovement(event);
super.onTouchEvent(event);
return true;
case MotionEvent.ACTION_POINTER_DOWN:
lastX = event.getX(0);
break;
case MotionEvent.ACTION_MOVE:
orientationX = event.getX() - lastX;
onScroll(orientationX);
lastX = event.getX();
velocityTracker.addMovement(event);
if (needEdgeEffect && datas.get(0).size() > maxOfVisible) {
if (isArriveAtLeftEdge()) {
edgeEffectLeft.onPull(Math.abs(orientationX) / linesArea.height());
} else if (isArriveAtRightEdge()) {
edgeEffectRight.onPull(Math.abs(orientationX) / linesArea.height());
}
}
break;
case MotionEvent.ACTION_POINTER_UP: // 计算出正确的追踪手指
int minID = event.getPointerId(0);
for (int i = 0; i < event.getPointerCount(); i++) {
if (event.getPointerId(i) <= minID) {
minID = event.getPointerId(i);
}
}
if (event.getPointerId(event.getActionIndex()) == minID) {
minID = event.getPointerId(event.getActionIndex() + 1);
}
lastX = event.getX(event.findPointerIndex(minID));
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (needShowHint && event.getAction() == MotionEvent.ACTION_UP) {
boolean canCallTap = Math.abs(event.getX() - firstX) < 2
&& Math.abs(event.getY() - firstY) < 2;
if (canCallTap) {
onTap(event.getX(), event.getY());
}
}
velocityTracker.addMovement(event);
velocityTracker.computeCurrentVelocity(1000, maxVelocity);
int initialVelocity = (int) velocityTracker.getXVelocity();
velocityTracker.clear();
if (!isArriveAtLeftEdge() && !isArriveAtRightEdge()) {
scroller.fling((int) event.getX(), (int) event.getY(), initialVelocity / 2,
0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
invalidate();
} else {
edgeEffectLeft.onRelease();
edgeEffectRight.onRelease();
}
lastX = event.getX();
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
onScroll(scroller.getCurrX() - lastX);
lastX = scroller.getCurrX();
if (needEdgeEffect) {
if (!hasAbsorbLeft && isArriveAtLeftEdge()) {
hasAbsorbLeft = true;
edgeEffectLeft.onAbsorb((int) scroller.getCurrVelocity());
} else if (!hasAbsorbRight && isArriveAtRightEdge()) {
hasAbsorbRight = true;
edgeEffectRight.onAbsorb((int) scroller.getCurrVelocity());
}
}
postInvalidate();
} else {
hasAbsorbLeft = false;
hasAbsorbRight = false;
}
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (datas.isEmpty()) return;
if (!needEdgeEffect) return;
if (!edgeEffectLeft.isFinished()) {
canvas.save();
canvas.rotate(-90);
canvas.translate(-linesArea.bottom, linesArea.left);
edgeEffectLeft.setSize((int) linesArea.height(), (int) linesArea.height());
if (edgeEffectLeft.draw(canvas)) {
postInvalidate();
}
canvas.restore();
}
if (!edgeEffectRight.isFinished()) {
canvas.save();
canvas.rotate(90);
canvas.translate(linesArea.top, -linesArea.right);
edgeEffectRight.setSize((int) linesArea.height(), (int) linesArea.height());
if (edgeEffectRight.draw(canvas)) {
postInvalidate();
}
canvas.restore();
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (datas.isEmpty()) return;
// lines
canvas.save();
canvas.clipRect(linesArea.left, linesArea.top, linesArea.right, linesArea.bottom+xArea.height());
canvas.translate(offset, 0);
// 当滑动到边缘 或 上次与本次结果相同 或 不需要计算边缘点 的时候就不再计算,直接draw已有的path
if (!paths.isEmpty() && !forceToDraw && !isAniming && (lastOffset == offset || noNeedCalcEdge(offset))) {
drawExsitDirectly(canvas);
// hint
if (clickIndexs != null) {
drawClickHint(canvas);
}
} else {
// 因为手指或fling计算出的offset不是连续按1px递增/减的,即无法准确地确定当前suitEdge和linesArea之间的相对位置
// 所以不适合直接加减suitEdge来划定数据区间
suitEdge = findSuitEdgeInVisual2();
drawLines(canvas, suitEdge[0], suitEdge[1]);
}
// x 蓝色会稍增加
drawX(canvas, suitEdge[0], suitEdge[1]);
if (lastOffset != offset) {
clickIndexs = null;
}
lastOffset = offset;
forceToDraw = false;
canvas.restore();
// y
drawY(canvas);
}
/**
* 边缘点在可见区域两侧时不需要重新计算
* 但是手指滑动越快,该分支的有效效果越差
* @param offset
* @return
*/
private boolean noNeedCalcEdge(float offset) {
return suitEdge != null
&& datas.get(0).get(suitEdge[0]).getXY().x <= linesArea.left - offset
&& datas.get(0).get(suitEdge[1]).getXY().x >= linesArea.right - offset;
}
/**
* 滑动方法,同时检测边缘条件
*
* @param deltaX
*/
private void onScroll(float deltaX) {
offset += deltaX;
offset = offset > 0 ? 0 : (Math.abs(offset) > maxOffset) ? -maxOffset : offset;
invalidate();
}
private void onTap(float upX, float upY) {
upX -= offset;
RectF bak = new RectF(linesArea);
bak.offset(-offset,0);
if (datas.isEmpty() || !bak.contains(upX, upY)) {
return;
}
float index = (upX - linesArea.left) / realBetween;
int realIndex = -1;
if ((index - (int) index) > 0.6f) {
realIndex = (int) index + 1;
} else if ((index - (int) index) < 0.4f) {
realIndex = (int) index;
}
if (realIndex != -1) {
int mostMatchY = -1;
for (int i = 0; i < datas.size(); i++) {
float cur = Math.abs(datas.get(i).get(realIndex).getXY().y - upY);
if (cur <= clickSlop) {
if (mostMatchY != -1) {
if (Math.abs(datas.get(mostMatchY).get(realIndex).getXY().y - upY) > cur) {
mostMatchY = i;
}
} else {
mostMatchY = i;
}
}
}
if (mostMatchY != -1) {
if (clickHintAnimator != null && clickHintAnimator.isRunning()) {
clickHintAnimator.removeAllUpdateListeners();
clickHintAnimator.cancel();
hintPaint.setAlpha(100);
clickIndexs = null;
invalidate();
}
clickIndexs = new int[]{realIndex, mostMatchY};
clickHintAnimator = ValueAnimator.ofInt(100, 30);
clickHintAnimator.setDuration(800);
clickHintAnimator.setInterpolator(linearInterpolator);
clickHintAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int cur = (Integer) animation.getAnimatedValue();
if (cur <= 30) {
hintPaint.setAlpha(100);
clickIndexs = null;
} else {
hintPaint.setAlpha(cur);
}
postInvalidate();
}
});
clickHintAnimator.start();
}
}
}
private void initOrResetVelocityTracker() {
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain();
} else {
velocityTracker.clear();
}
}
private void recycleVelocityTracker() {
if (velocityTracker != null) {
velocityTracker.recycle();
velocityTracker = null;
}
}
/**
* 是否滑动到了左边缘,注意,并非指可视区域的边缘,下同
*
* @return
*/
private boolean isArriveAtLeftEdge() {
return offset == 0 && orientationX > 0;
}
/**
* 是否滑动到了右边缘
*
* @return
*/
private boolean isArriveAtRightEdge() {
return Math.abs(offset) == Math.abs(maxOffset) && orientationX < 0;
}
/**
* 找到当前可见区间内合适的两个边缘点,注意如果边缘点不在可见区间的边缘,则需要包含下一个不可见的点
*
* @return
*/
private int[] findSuitEdgeInVisual() {
int startIndex = 0, endIndex = datas.get(0).size() - 1;
if (offset == 0) {// 不可滑动或当前位于最左边
startIndex = 0;
endIndex = Math.min(datas.get(0).size() - 1, maxOfVisible - 1);
} else if (Math.abs(offset) == maxOffset) {// 可滑动且当前位于最右边
endIndex = datas.get(0).size() - 1;
startIndex = endIndex - maxOfVisible + 1;
} else {
float startX = linesArea.left - offset;
float endX = linesArea.right - offset;
if (datas.get(0).size() > maxOfVisible) {
// 找到指定区间的第一个被发现的点
int suitKey = 0;
int low = 0;
int high = datas.get(0).size() - 1;
List i = datas.get(0);
while (low <= high) {
int mid = (low + high) >>> 1;
Unit midVal = i.get(mid);
if (midVal.getXY().x < startX) {
low = mid + 1;
} else if (midVal.getXY().x > endX) {
high = mid - 1;
} else {
suitKey = mid;
break;
}
}
int bakKey = suitKey;
// 先左边
while (suitKey >= 0) {
startIndex = suitKey;
if (datas.get(0).get(suitKey).getXY().x <= startX) {
break;
}
suitKey--;
}
suitKey = bakKey;
// 再右边
while (suitKey < datas.get(0).size()) {
endIndex = suitKey;
if (datas.get(0).get(suitKey).getXY().x >= endX) {
break;
}
suitKey++;
}
}
}
return new int[]{startIndex, endIndex};
}
/**
* 1. ax+b >= y
* 2. a(x+1)+b <= y
* 得到: (int)x = (y-b) / a
* 由于 y = b - offset
* 所以:(int)x = |offset| / a
* @return
*/
private int[] findSuitEdgeInVisual2() {
int startIndex, endIndex;
if (offset == 0) {// 不可滑动或当前位于最左边
startIndex = 0;
endIndex = Math.min(datas.get(0).size() - 1, maxOfVisible - 1);
} else if (Math.abs(offset) == maxOffset) {// 可滑动且当前位于最右边
endIndex = datas.get(0).size() - 1;
startIndex = endIndex - maxOfVisible + 1;
} else {
startIndex = (int) (Math.abs(offset) / realBetween);
endIndex = startIndex + maxOfVisible;
}
return new int[]{startIndex, endIndex};
}
/**
* 开始连接每条线的各个点
* 最耗费性能的地方:canvas.drawPath
* @param canvas
* @param startIndex
* @param endIndex
*/
private void drawLines(Canvas canvas, int startIndex, int endIndex) {
for (int i = 0; i < paths.size(); i++) {
paths.get(i).reset();
}
for (int i = startIndex; i <= endIndex; i++) {
for (int j = 0; j < datas.size(); j++) {
Unit current = datas.get(j).get(i);
float curY = linesArea.bottom - (linesArea.bottom - current.getXY().y) * current.getPercent();
if (i == startIndex) {
paths.get(j).moveTo(current.getXY().x, curY);
continue;
}
if (lineType == SEGMENT) {
paths.get(j).lineTo(current.getXY().x, curY);
} else if (lineType == CURVE) {
// 到这里肯定不是起始点,所以可以减1
Unit previous = datas.get(j).get(i - 1);
// 两个锚点的坐标x为中点的x,y分别是两个连接点的y
paths.get(j).cubicTo((previous.getXY().x + current.getXY().x) / 2,
linesArea.bottom - (linesArea.bottom - previous.getXY().y) * previous.getPercent(),
(previous.getXY().x + current.getXY().x) / 2, curY,
current.getXY().x, curY);
}
if (isLineFill() && i == endIndex) {
paths.get(j).lineTo(current.getXY().x, linesArea.bottom);
paths.get(j).lineTo(datas.get(j).get(startIndex).getXY().x, linesArea.bottom);
paths.get(j).close();
}
}
}
drawExsitDirectly(canvas);
}
/**
* 直接draw现成的
* @param canvas
*/
private void drawExsitDirectly(Canvas canvas) {
// TODO 需要优化
for (int j = 0; j < datas.size(); j++) {
canvas.drawPath(paths.get(j), paints.get(j));
}
// TODO 画点
}
/**
* 画提示文本和辅助线
* @param canvas
*/
private void drawClickHint(Canvas canvas) {
Unit cur = datas.get(clickIndexs[1]).get(clickIndexs[0]);
canvas.drawLine(datas.get(clickIndexs[1]).get(suitEdge[0]).getXY().x,cur.getXY().y,
datas.get(clickIndexs[1]).get(suitEdge[1]).getXY().x,cur.getXY().y, hintPaint);
canvas.drawLine(cur.getXY().x,linesArea.bottom,
cur.getXY().x,linesArea.top, hintPaint);
RectF bak = new RectF(hintArea);
bak.offset(-offset, 0);
hintPaint.setAlpha(100);
hintPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(bak, hintPaint);
hintPaint.setColor(Color.WHITE);
if (!TextUtils.isEmpty(cur.getExtX())) {
canvas.drawText("x : " + cur.getExtX(), bak.centerX(), bak.centerY() - 12, hintPaint);
}
canvas.drawText("y : " + cur.getValue(), bak.centerX(),
bak.centerY() + 12 + Util.getTextHeight(hintPaint), hintPaint);
hintPaint.setColor(hintColor);
}
/**
* 画x轴,默认取第一条线的值
* @param canvas
* @param startIndex
* @param endIndex
*/
private void drawX(Canvas canvas, int startIndex, int endIndex) {
canvas.drawLine(datas.get(0).get(startIndex).getXY().x, xArea.top,
datas.get(0).get(endIndex).getXY().x, xArea.top, xyPaint);
for (int i = startIndex; i <= endIndex; i++) {
String extX = datas.get(0).get(i).getExtX();
if (TextUtils.isEmpty(extX)) {
continue;
}
if (i == startIndex && startIndex == 0) {
xyPaint.setTextAlign(Paint.Align.LEFT);
} else if (i == endIndex && endIndex == datas.get(0).size()-1) {
xyPaint.setTextAlign(Paint.Align.RIGHT);
} else {
xyPaint.setTextAlign(Paint.Align.CENTER);
}
canvas.drawText(extX, datas.get(0).get(i).getXY().x, Util.calcTextSuitBaseY(xArea, xyPaint), xyPaint);
}
}
private void drawY(Canvas canvas) {
if (yAreaBuffer == null) {
yAreaBuffer = Bitmap.createBitmap((int)yArea.width(), (int)yArea.height(), Bitmap.Config.ARGB_8888);
Rect yRect = new Rect(0, 0, yAreaBuffer.getWidth(), yAreaBuffer.getHeight());
Canvas yCanvas = new Canvas(yAreaBuffer);
yCanvas.drawLine(yRect.right, yRect.bottom, yRect.right, yRect.top, xyPaint);
for (int i = 0; i < countOfY; i++) {
xyPaint.setTextAlign(Paint.Align.RIGHT);
float extY;
float y;
if (i == 0) {
extY = 0;
y = yRect.bottom;
} else if (i == countOfY - 1) {
extY = maxValueOfY;
y = yRect.top + Util.getTextHeight(xyPaint) + 3;
} else {
extY = maxValueOfY / (countOfY - 1) * i;
y = yRect.bottom - yRect.height() / (countOfY - 1) * i + Util.getTextHeight(xyPaint)/2;
}
yCanvas.drawText(new DecimalFormat("##.#").format(extY), yRect.right - basePadding, y, xyPaint);
}
}
canvas.drawBitmap(yAreaBuffer,yArea.left,yArea.top,null);
}
/**
*
* @param color 不能为null
* @return
*/
private LinearGradient buildPaintColor(int[] color) {
int[] bakColor = color;
if (color != null && color.length < 2) {
bakColor = new int[2];
bakColor[0] = color[0];
bakColor[1] = color[0];
}
return new LinearGradient(linesArea.left, linesArea.top,
linesArea.left, linesArea.bottom, bakColor, null, Shader.TileMode.CLAMP);
}
/**
* 基于orgPaint的clone
*
* @return
*/
private Paint buildNewPaint() {
Paint paint = new Paint();
paint.set(basePaint);
return paint;
}
private void feedInternal(Map> entry, List entryPaints, boolean needAnim) {
cancelAllAnims();
reset(); // 该方法调用了datas.clear();
if (entry.isEmpty()) {
invalidate();
return;
}
if (entry.size() != entryPaints.size()) {
throw new IllegalArgumentException("线的数量应该和画笔数量对应");
} else {
paints.clear();
paints.addAll(entryPaints);
}
if (entry.size() != paths.size()) {
paths.clear();
for (int i = 0; i < entry.size(); i++) {
paths.add(new Path());
}
}
datas.putAll(entry);
calcMaxUnit(datas);
calcAreas();
calcUnitXY();
if (needAnim) {
showWithAnims();
} else {
forceToDraw = true;
invalidate();
}
}
/**
* 得到maxValueOfY
* @param datas
*/
private void calcMaxUnit(Map> datas) {
// 先“扁平”
List allUnits = new ArrayList<>();
for (List line : datas.values()) {
allUnits.addAll(line);
}
// 再拷贝,防止引用问题
List bakUnits = new ArrayList<>();
for (int i = 0; i < allUnits.size(); i++) {
bakUnits.add(allUnits.get(i).clone());
}
// 最后排序,得到最大值
Collections.sort(bakUnits);
Unit maxUnit = bakUnits.get(bakUnits.size() - 1);
maxValueOfY = Util.getCeil5(maxUnit.getValue());
}
/**
* 重新计算三个区域的大小
*/
private void calcAreas() {
String baseY = "00";
if (maxValueOfY > 0) {
baseY = String.valueOf(maxValueOfY);
}
RectF validArea = new RectF(getPaddingLeft() + basePadding, getPaddingTop() + basePadding,
getMeasuredWidth() - getPaddingRight() - basePadding, getMeasuredHeight() - getPaddingBottom());
yArea = new RectF(validArea.left, validArea.top,
validArea.left + xyPaint.measureText(baseY) + basePadding,
validArea.bottom - Util.getTextHeight(xyPaint) - basePadding * 2);
xArea = new RectF(yArea.right, yArea.bottom, validArea.right, validArea.bottom);
linesArea = new RectF(yArea.right+1, yArea.top, xArea.right, yArea.bottom);
hintArea = new RectF(linesArea.right-linesArea.right/4,linesArea.top,
linesArea.right,linesArea.top + linesArea.height()/4);
}
/**
* 计算所有点的坐标
*
同时得到了realBetween,maxOffset
*/
private void calcUnitXY() {
int realNum = Math.min(datas.get(0).size(), maxOfVisible);
realBetween = linesArea.width() / (realNum - 1);
for (int i = 0; i < datas.get(0).size(); i++) {
for (int j = 0; j < datas.size(); j++) {
datas.get(j).get(i).setXY(new PointF(linesArea.left + realBetween * i,
linesArea.top + linesArea.height() * (1 - datas.get(j).get(i).getValue() / maxValueOfY)));
if (i == datas.get(0).size() - 1) {
maxOffset = Math.abs(datas.get(j).get(i).getXY().x) - linesArea.width() - linesArea.left;
}
}
}
}
/**
* 取消所有正在执行的动画,若存在的话;
* 在 重新填充数据 / dettach-view 时调用
*/
private void cancelAllAnims() {
// 不使用ViewRootImpl的getHandler(),否则影响其事件分发
handler.removeCallbacksAndMessages(null);
scroller.abortAnimation();
if (clickHintAnimator != null && clickHintAnimator.isRunning()) {
clickHintAnimator.removeAllUpdateListeners();
clickHintAnimator.cancel();
hintPaint.setAlpha(100);
clickHintAnimator = null;
}
if (!animators.isEmpty()) {
for (int i = 0; i < animators.size(); i++) {
animators.get(i).removeAllUpdateListeners();
if (animators.get(i).isRunning()) {
animators.get(i).cancel();
}
}
animators.clear();
}
if (!datas.isEmpty()) {
for (List line : datas.values()) {
for (int i = 0; i < line.size(); i++) {
line.get(i).cancelToEndAnim();
}
}
}
for (int i = 0; i < paths.size(); i++) {
paths.get(i).reset();
}
invalidate();
}
// 每个1/x启动下一条line的动画
private int percentOfStartNextLineAnim = 3;
/**
* 约定每间隔一组数据遍历总时间的一半就启动下一组数据的遍历
*
* @return 遍历时间+最后一组数据的等待时间+最后一个点的动画时间+缓冲时间
*/
private long calcTotalCost() {
if (datas.isEmpty() || datas.get(0).isEmpty()) return 0;
long oneLineCost = calcVisibleLineCost();
return oneLineCost + oneLineCost / percentOfStartNextLineAnim * (datas.size() - 1) + Unit.DURATION + 16;
}
/**
* 一条线遍历完的时间,
*
* @return
*/
private long calcVisibleLineCost() {
if (intervalOfAnimCost > 0) {
if (maxOfVisible < datas.get(0).size()) {
return maxOfAnimCost;
}
long oneLineCost = intervalOfAnimCost * (datas.get(0).size() - 1);
oneLineCost = Math.min(maxOfAnimCost, oneLineCost);
return oneLineCost;
} else {
return 0;
}
}
private void showWithAnims() {
if (datas.isEmpty()) return;
curAnimLine = 0;
startTimeOfAnim = System.currentTimeMillis();
int[] suitEdge = findSuitEdgeInVisual();
// 重置所有可见点的percent
for (int i = suitEdge[0]; i <= suitEdge[1]; i++) {
for (List item : datas.values()) {
item.get(i).setPercent(0);
}
}
startLinesAnimOrderly(suitEdge[0], suitEdge[1]);
autoInvalidate();
}
/**
* 开启自动刷新
*/
private void autoInvalidate() {
isAniming = true;
invalidate();
if (System.currentTimeMillis() - startTimeOfAnim > calcTotalCost()) {
isAniming = false;
return;
}
handler.postDelayed(new Runnable() {
@Override
public void run() {
autoInvalidate();
}
}, 16);
}
/**
* 间隔指定时间依次启动每条线
*/
private void startLinesAnimOrderly(final int startIndex, final int endIndex) {
startLineAnim(startIndex, endIndex);
if (curAnimLine >= datas.size() - 1) return;
handler.postDelayed(new Runnable() {
@Override
public void run() {
curAnimLine++;
startLinesAnimOrderly(startIndex, endIndex);
}
}, calcVisibleLineCost() / percentOfStartNextLineAnim);
}
/**
* 依次启动指定label的线的每个可见点的动画;
*
* @param startIndex
* @param endIndex
*/
private void startLineAnim(int startIndex, int endIndex) {
final List line = datas.get(curAnimLine);
long duration = calcVisibleLineCost();
if (duration > 0) {
ValueAnimator animator = ValueAnimator.ofInt(startIndex, endIndex);
animator.setDuration(duration);
animator.setInterpolator(linearInterpolator);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
line.get((Integer) animation.getAnimatedValue()).startAnim(pointInterpolator);
}
});
animator.start();
animators.add(animator);
} else {
for (int i = startIndex; i <= endIndex; i++) {
line.get(i).startAnim(pointInterpolator);
}
}
}
/**
* 重置相关状态
*/
private void reset() {
invaliateYBuffer();
offset = 0;
realBetween = 0;
suitEdge = null;
clickIndexs = null;
datas.clear();
}
private void invaliateYBuffer() {
if (yAreaBuffer != null) {
yAreaBuffer.recycle();
yAreaBuffer = null;
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
cancelAllAnims();
reset();
}
///APIs/
/**
* 设置默认一条line时的颜色
* @param colors 默认为defaultLineColor
*/
public void setDefaultOneLineColor(int...colors) {
if (colors == null || colors.length < 1) return;
defaultLineColor = colors;
basePaint.setColor(colors[0]);
basePaint.setShader(buildPaintColor(colors));
if (!datas.isEmpty() && datas.size() == 1) {
paints.get(0).set(basePaint);
postInvalidate();
}
}
/**
* 设置提示辅助线、文字颜色
* @param hintColor
*/
public void setHintColor(int hintColor) {
needShowHint = true;
this.hintColor = hintColor;
hintPaint.setColor(hintColor);
if (!datas.isEmpty()) {
if (clickIndexs != null) {
postInvalidate();
}
}
}
/**
* 设置xy轴文字的颜色
* @param color 默认为Color.GRAY
*/
public void setXyColor(int color) {
defaultXyColor = color;
xyPaint.setColor(defaultXyColor);
if (!datas.isEmpty()) {
invaliateYBuffer();
forceToDraw = true;
postInvalidate();
}
}
/**
* 设置xy轴文字大小
* @param sp
*/
public void setXySize(float sp) {
defaultXySize = sp;
xyPaint.setTextSize(Util.size2sp(defaultXySize, getContext()));
if (!datas.isEmpty()) {
invaliateYBuffer();
calcAreas();
calcUnitXY();
offset = 0;// fix bug.
forceToDraw = true;
postInvalidate();
}
}
/**
* 设置line的SEGMENT时的大小
* @param lineSize
*/
public void setLineSize(float lineSize) {
basePaint.setStyle(Paint.Style.STROKE);
basePaint.setStrokeWidth(lineSize);
// 同时更新当前已存在的paint
for (int i = 0; i < paints.size(); i++) {
forceToDraw = true;
paints.get(i).setStyle(basePaint.getStyle());
paints.get(i).setStrokeWidth(lineSize);
}
postInvalidate();
}
/**
* 指定line类型:CURVE / SEGMENT
* @param lineType 默认CURVE
*/
public void setLineType(int lineType) {
this.lineType = lineType;
forceToDraw = true;
postInvalidate();
}
public int getLineType() {
return lineType;
}
/**
* 设置line的形态:是否填充
* @param isFill 默认为false
*/
public void setLineForm(boolean isFill) {
if (isFill) {
basePaint.setStyle(Paint.Style.FILL);
} else {
basePaint.setStyle(Paint.Style.STROKE);
}
if (!datas.isEmpty()) {
// 同时更新当前已存在的paint
for (int i = 0; i < paints.size(); i++) {
forceToDraw = true;
paints.get(i).setStyle(basePaint.getStyle());
}
postInvalidate();
}
}
public boolean isLineFill() {
return basePaint.getStyle() == Paint.Style.FILL;
}
public void setLineStyle(int style) {
lineStyle = style;
basePaint.setPathEffect(lineStyle == DASHED ? new DashPathEffect(new float[]{Util.dip2px(3),Util.dip2px(6)},0) : null);
if (!datas.isEmpty()) {
// 同时更新当前已存在的paint
for (int i = 0; i < paints.size(); i++) {
forceToDraw = true;
paints.get(i).setPathEffect(basePaint.getPathEffect());
}
postInvalidate();
}
}
public boolean isLineDashed() {
return basePaint.getPathEffect() != null;
}
/**
* 关闭边缘效果,默认开启
*/
public void disableEdgeEffect() {
needEdgeEffect = false;
postInvalidate();
}
/**
* 关闭点击提示信息,默认开启
*/
public void disableClickHint() {
needShowHint = false;
}
/**
* 指定边缘效果的颜色
* @param color 默认为Color.GRAY
*/
public void setEdgeEffectColor(int color) {
needEdgeEffect = true;
edgeEffectColor = color;
Util.trySetColorForEdgeEffect(edgeEffectLeft, edgeEffectColor);
Util.trySetColorForEdgeEffect(edgeEffectRight, edgeEffectColor);
postInvalidate();
}
/**
* 本方式仅支持一条线,若需要支持多条线,请采用Builder方式
*
* @param line
*/
public void feedWithAnim(List line) {
if (line == null || line.isEmpty()) return;
final Map> entry = new HashMap<>();
entry.put(0, line);
handler.post(new Runnable() {
@Override
public void run() {
feedInternal(entry, Arrays.asList(buildNewPaint()), true);
}
});
}
/**
* 本方式仅支持一条线,若需要支持多条线,请采用Builder方式
*
* @param line
*/
public void feed(List line) {
if (line == null || line.isEmpty()) return;
final Map> entry = new HashMap<>();
entry.put(0, line);
handler.post(new Runnable() {
@Override
public void run() {
feedInternal(entry, Arrays.asList(buildNewPaint()), false);
}
});
}
public void anim() {
if (datas.isEmpty()) return;
handler.post(new Runnable() {
@Override
public void run() {
cancelAllAnims();
showWithAnims();
}
});
}
public void postAction(Runnable runnable) {
handler.post(runnable);
}
// 多条线的情况应该采用该构建方式
public static class LineBuilder {
private int curIndex;
private Map> datas;
private Map colors;
public LineBuilder() {
datas = new HashMap<>();
colors = new HashMap<>();
}
/**
* 该方式是用于构建多条line,单条line可使用lineGraph#feed
* @param data 单条line的数据集合
* @param color 指定当前line的颜色。默认取数组的第一个颜色;另外如果开启了填充,则整个数组颜色作为填充色的渐变。
* @return
*/
public LineBuilder add(List data, int... color) {
if (data == null || data.isEmpty() || color == null || color.length <= 0) {
throw new IllegalArgumentException("无效参数data或color");
}
int bakIndex = curIndex;
datas.put(bakIndex, data);
colors.put(bakIndex, color);
curIndex++;
return this;
}
/**
* 调用该方法开始填充数据
* @param suitLines 需要被填充的图表
* @param needAnim 是否需要动画
*/
public void build(final SuitLines suitLines, final boolean needAnim) {
final List tmpPaints = new ArrayList<>();
for (int i = 0; i < colors.size(); i++) {
Paint paint = suitLines.buildNewPaint();
paint.setColor(colors.get(0)[0]);
paint.setShader(suitLines.buildPaintColor(colors.get(i)));
tmpPaints.add(i, paint);
}
suitLines.postAction(new Runnable() {
@Override
public void run() {
suitLines.feedInternal(datas, tmpPaints, needAnim);
}
});
}
}
}