这篇文章上次修改于 2447 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
从需求到API
例如,做一个股票趋势图
首先想到的应该是一条线,一条曲折的线,线就是路径,及对应Android的Path类,简单说一下Path类,它可以画线,可以画图形,还可以做为一条路径,按该路径的走势写字或画图,所以Path有个Direction的内部类,定义了方向。但是上面说的这些能力并不是Path类的,而是通过Path类才能实现的,因为Path类只代表路径。
Path类指明了路径,还需要一支笔,Paint类。笔是有多种多样的,就按需求来说定义一支笔出来
Paint paint = new Paint();
paint.setStyle(Paint.Style.STROKE);//设置画笔的样式为STROKE(描边),样式还有FILL(填充)和FILL_AND_STROKE(填充+描边)
paint.setColor(Color.RED);//设置画笔颜色
paint.setStrokeWidth(3);//设置画笔粗细
有画笔还是不够的,还需要画板才能作画,大多数情况下,我们是不需要自己创建画板的,因为大多数情况下,使用Android绘图都是在自定义View的时候,继承View重写的onDraw方法的入参就是一个现成的Canvas(画板)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
现在就差数据了,有了数据才能定义出路径来,有了路径,画笔才能在画板上按路径STROKE(描边)。
造数据,可以用个随机数来生成,在Y轴上一个范围内随机,在X轴上按一定刻度前进,应该就是想像中的趋势图了[完美]
Path path = new Path();
Random random = new Random();
for (int i = 0;i < 50;i++) {
int x = i * 20;
int y = random.nextInt(300);
if (i == 0)
path.moveTo(x,y);
else
path.lineTo(x,y);
}
然后在画板上画出来
canvas.drawPath(path,paint);
效果图
感觉太空了,加点鲜艳的颜色,给画板加个底色
canvas.drawColor(Color.GREEN);
真是惊艳[滑稽]
还差点什么,作为趋势图,就应该动起来,效果才好,怎么动呢,可以有好多方式,可以每隔一段时间调用一次onDraw方法,要用计时器吗?其实有更好的选择,View类有 invalidate() 方法,当被调用时,会触发onDraw方法,如果把invalidate()放在onDraw方法中,这就成了一个循环。那么,把path路径的绘制放到onDraw中,每次调用就绘制一段,再调用invalidate()方法,循环下去就成了动图,搞定。
回过头来,还要处理一个棘手的问题,上面的相互调用是个死循环,需要在onDraw方法中加入一些限制条件,以控制循环次数,有什么做条件呢?可以选择控制path的Y轴,当Y轴大于自定义的View的最右边界时,停止循环,问题解决。
但是,为了多聊一个API,这里采用另一种方式
canvas.clipRect(left,top,right,bottom);
canvas有好多clipxxx的方法,都是裁剪成xxx的形状,让画板的大小慢慢变大,使path能显示在画板上的区域越来越大,好像趋势图在动,在前进。这个方法要注意的一点是裁剪只会影响这个方法调用之后的内容,之前canvas有多大,上面画了什么,都不会受到clip的影响,通过这一点可以把画板的底色设置在调用clip之前,保持底色大小不受裁剪的影响。
最终效果
附上代码
public class StockView extends View {
Path path;
Paint paint;
int right;
private int defaultSize = 400;
public StockView(Context context){
super(context);
}
public StockView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
path = new Path();
Random random = new Random();
for (int i = 0;i < 50;i++) {
int x = i * 20;
int y = random.nextInt(300);
if (i == 0)
path.moveTo(x,y);
else
path.lineTo(x,y);
}
paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.RED);
paint.setStrokeWidth(3);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSize,defaultSize);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GREEN);
canvas.clipRect(0,0,right,getBottom());
canvas.drawPath(path,paint);
if (right < getRight()){
right+=2;
invalidate();
}
}
}
不是股票趋势图
画完才发现,这不是股票图吧,应该是那种吧,那种一个条,两头尖的,应该叫K线图。
研究一下,这个名叫蜡烛的东西,需要4个值,开盘,收盘等…姑且用一个数组表示吧,1234。
数据特点:
- 开盘和收盘不会高于最高
- 开盘和收盘不会低于最低
- 收盘高于开盘时为红色
- 开盘高于收盘时为绿色
根据这些特点,可以造出正确的数据
private float[] createData() {
double open = Math.random() * defaultSize;
double close = Math.random() * defaultSize;
return new float[]{(float) open, (float) close,
(float) (Math.max(open, close) + Math.random() * (defaultSize - Math.max(open, close))),
(float) (Math.min(open, close) - Math.random() * (Math.min(open, close)))};
}
制造一个蜡烛,并画到画板上
private int kWidth = 18;//蜡烛的宽度
// data {开盘,收盘,最高,最低}
private void createK(Canvas canvas, int x, float[] data) {
Path path = new Path();
path.moveTo(x, transformYScale(data[2]));
float tempMaxY = transformYScale(Math.max(data[0], data[1]));
float tempMinY = transformYScale(Math.min(data[0], data[1]));
path.lineTo(x, tempMaxY);
path.addRect(x - kWidth / 2, tempMaxY, x + kWidth / 2, tempMinY, Path.Direction.CCW);
path.moveTo(x, tempMinY);
path.lineTo(x, transformYScale(data[3]));
//改变颜色
if (data[0] > data[1])
paint.setColor(Color.GREEN);
else
paint.setColor(Color.RED);
canvas.drawPath(path, paint);
}
//转换Y轴
private float transformYScale(float y) {
return defaultSize - y;
}
由于Android的坐标轴是从左上角为(0,0)点,Y轴和笛卡尔坐标系相反,所以就有了transformYScale函数用于转换。
这个方式实现的蜡烛不能使用之前用到过的invalidate()方法,因为这次的path并不是一个整体的,当再次触发onDraw方式时,画板重置,之前的path绘制就被清除了,所以可以采用循环来处理这个问题。
为什么不用一个整体的path?因为画笔(paint)的颜色在不断的变化。
由于需要画一个矩形,所以画笔(paint)的样式需要设置成填充+描边
paint.setStyle(Paint.Style.FILL_AND_STROKE);
最终效果
不够惊艳,加个黑色背景色,效果翻倍[开心]
确实很惊艳,也很惊讶,这股票趋势,能吓死人了吧[滑稽]。
附上代码
public class KLineView extends View {
Paint paint;
private int defaultSize = 400;
private int kWidth = 18;
private int x = 50;
public KLineView(Context context) {
super(context);
}
public KLineView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
paint = new Paint();
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setStrokeWidth(2);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, defaultSize);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.BLACK);
while (x < getRight()) {
createK(canvas, x, createData());
x += 50;
}
}
private float[] createData() {
double open = Math.random() * defaultSize;
double close = Math.random() * defaultSize;
return new float[]{(float) open, (float) close,
(float) (Math.max(open, close) + Math.random() * (defaultSize - Math.max(open, close))),
(float) (Math.min(open, close) - Math.random() * (Math.min(open, close)))};
}
// data {开盘,收盘,最高,最低}
private void createK(Canvas canvas, int x, float[] data) {
Path path = new Path();
path.moveTo(x, transformYScale(data[2]));
float tempMaxY = transformYScale(Math.max(data[0], data[1]));
float tempMinY = transformYScale(Math.min(data[0], data[1]));
path.lineTo(x, tempMaxY);
path.addRect(x - kWidth / 2, tempMaxY, x + kWidth / 2, tempMinY, Path.Direction.CCW);
path.moveTo(x, tempMinY);
path.lineTo(x, transformYScale(data[3]));
if (data[0] > data[1])
paint.setColor(Color.GREEN);
else
paint.setColor(Color.RED);
canvas.drawPath(path, paint);
}
private float transformYScale(float y) {
return defaultSize - y;
}
}
2018/3/15.
Dean.King
Beijing
没有评论