这次是要实现一个至少有1000个点的折线图。大约在1000~2000个点之间,而且时间要求的很紧,没有美工图,完全自己发挥!!!(所以略丑,但这不重要) 我实现的最后效果:
我看到的原形图是这样的:
没错此图来自Excel。 很明显1000个点是需要滑动,这与我之前做过的一个曲线图很类似,
所以实现效果上没有什么难点,大约2个小时就可以搞定。
但是这个我比较担心的是性能问题,因为数据较多,而且需要适配的机型有很多3-4年之前的机器,性能较弱。我手上的测试机是红米note2,最后在此机器上流畅运行滑动无明显卡顿。恩,分享一下我的优化思路。
1.避免过度重绘
过度重绘的的意思就是屏幕上的像素点尽量不要绘制多次,能一次画好,就只画一次,不要多次覆盖绘制。没用的或者看不到的背景避免绘制。
在开发者选项里面有查看过度绘制选项,但是这个选项只对xml布局有效果,如果是自定义View的话没啥效果。但是在自定义View的时候也不要多次绘制同一个像素。这里就需要开发者自己注意了。
2.尽量减少或简化计算
在自定义View中,计算各个坐标,计算触摸事件、位置等等都占了很大比重。而且像是滑动这样的操作,每一帧都是通过实时计算的,所以减少或者简化计算是提高性能的有效方式。应该避免在for或while循环中做计算或者new对象,不要做无用计算。在合适的地方增加判断,跳出计算。尽可能的复用计算结果。没有数据,或者数据较少的时候应如何处理,没有事件需要响应的时候如何处理。注意这些细节,也会提高执行效率。
3.物尽其用,避免new对象
在我之前的博客中,总是有读者给我留言,说我不应该在ondraw中new 一个Paint对象。其实Paint类是提供了一个reset方法的。更要避免在循环中new对象,这是减少内存占用量的有效方法。一个对象尽量反复利用。
4.把握好I/O操作的时机
大家都知道I/O操作是十分耗时的,但是这些操作在自定义View中是不可避免的。比如读取属性,读取文件之类的操作。所以I/O操作的时机就十分重要,并且要避免重复读取。我个人的习惯是,如果不是十分强调通用性的话,我不会用到自定义属性,我会在代码开头声明好变量,做好注释,以后直接修改。对于像分辨率适配,而用到不用的value的时候,我在代码中尽量用到百分比(通过宽高计算)。
5.了解哪些效果会拖累性能
有很多视觉效果是很耗时,或者说占用很大资源的。应该事先了解,避免大量使用。比如画布剪切,渐变,Matrix变化,canvas移动等等。这里应该与设计师沟通好,或者寻找代替方案。
6.算法,其他技术也要考虑
一套效率更高的算法可能会成倍的提高效率。如果Java层实现效果不好的话,可以考虑NDK。代码是死的,程序猿是活的。
我一直认为,技术不能成为一款产品走向更好体验的绊脚石。
这次的代码,我觉得还有优化空间。欢迎各位提意见,随便贴一下吧。
package top.greendami.greendami;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import java.util.ArrayList;
import java.util.List;
/**
* 1000个点,震荡图
* Created by GreendaMi on 2017/5/8.
*/
public class ShakeMaps extends View {
Context mContext;
int max;
int min;
//两种线的颜色
int mColor1 = 0xff159461;
int mColor2 = 0xffeb2e28;
Paint mPaint;
int gap = 10;//点与点之间的间距
int startX = 10;
int borderTopAndBottom = 20;//上下留白
int botderLeft = 10;//左边留白
int botderLefttep = botderLeft;
int lastStartX = startX;//抬起手指后,当前控件最左边X的坐标
int mXDown;
int mLastX;
//最短滑动距离
int a = 0;
public void setmData(List<dataObj> mData, int max, int min) {
this.mData = mData;
this.max = max;
this.min = min;
postInvalidate();
}
public void initPaint() {
if (mPaint == null) {
mPaint = new Paint();
} else {
mPaint.reset();
}
mPaint.setAntiAlias(true);
//文字大小
mPaint.setTextSize(getWidth() / 32);
}
List<dataObj> mData = new ArrayList<>();
public ShakeMaps(Context context) {
super(context);
mContext = context;
a = DPUnitUtil.px2dip(context, ViewConfiguration.get(context).getScaledDoubleTapSlop());
setClickable(true);
initializeTheUnit();
initPaint();
}
public ShakeMaps(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mContext = context;
a = DPUnitUtil.px2dip(context, ViewConfiguration.get(context).getScaledDoubleTapSlop());
setClickable(true);
initializeTheUnit();
initPaint();
}
//初单位
public void initializeTheUnit() {
gap = DPUnitUtil.dip2px(mContext, 5);
startX = DPUnitUtil.dip2px(mContext, 5);
borderTopAndBottom = DPUnitUtil.dip2px(mContext, 10);
botderLeft = DPUnitUtil.dip2px(mContext, 10);
botderLefttep = botderLeft;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画背景
drawTheBackground(canvas);
setLayerType(LAYER_TYPE_SOFTWARE, null);
//画y轴
drawTheY(canvas);
//画虚线,关闭硬件加速
setLayerType(LAYER_TYPE_SOFTWARE, null);
//画x轴横线
drawTheX(canvas);
setLayerType(LAYER_TYPE_SOFTWARE, null);
//画数据
drawDatas(canvas);
setLayerType(LAYER_TYPE_SOFTWARE, null);
}
private void drawTheY(Canvas canvas) {
initPaint();
mPaint.setColor(0xff92dac4);
mPaint.setStrokeWidth(DPUnitUtil.dip2px(mContext, 1));
//留出文字距离
botderLeft = botderLefttep + (int) (mPaint.measureText(min + "") * 1.2f);
//画出纵坐标线
canvas.drawLine(botderLeft, borderTopAndBottom, botderLeft, getHeight() - borderTopAndBottom, mPaint);
}
private void drawDatas(Canvas canvas) {
if (mData == null || mData.size() == 0) {
return;
}
initPaint();
mPaint.setColor(mColor1);
mPaint.setStrokeWidth(DPUnitUtil.dip2px(mContext, 1));
//画y1的线
for (int i = 0; i < mData.size() - 1; i++) {
//超过屏幕范围,不再绘制
if (startX + botderLeft + gap * i < botderLeft) {
continue;
}
if (startX + botderLeft + gap * (i + 1) > getWidth()) {
break;
}
canvas.drawLine(startX + botderLeft + gap * i, getHByValue(mData.get(i).y1), startX + botderLeft + gap * (i + 1), getHByValue(mData.get(i + 1).y1), mPaint);
//画开始小球和结束小球
if (i == 0) {
canvas.drawCircle(startX + botderLeft + gap * i, getHByValue(mData.get(i).y1), 8, mPaint);
mPaint.setColor(0xffffffff);
canvas.drawCircle(startX + botderLeft + gap * i, getHByValue(mData.get(i).y1), 4, mPaint);
mPaint.setColor(mColor1);
}
if (i == mData.size() - 2) {
canvas.drawCircle(startX + botderLeft + gap * (i + 1), getHByValue(mData.get(i + 1).y1), 8, mPaint);
mPaint.setColor(0xffffffff);
canvas.drawCircle(startX + botderLeft + gap * (i + 1), getHByValue(mData.get(i + 1).y1), 4, mPaint);
mPaint.setColor(mColor1);
}
}
//画y2的线
initPaint();
mPaint.setColor(mColor2);
mPaint.setStrokeWidth(DPUnitUtil.dip2px(mContext, 1));
for (int i = 0; i < mData.size() - 1; i++) {
//超过屏幕范围,不再绘制
if (startX + botderLeft + gap * i < botderLeft) {
continue;
}
if (startX + botderLeft + gap * (i + 1) > getWidth()) {
break;
}
canvas.drawLine(startX + botderLeft + gap * i, getHByValue(mData.get(i).y2), startX + botderLeft + gap * (i + 1), getHByValue(mData.get(i + 1).y2), mPaint);
//画开始小球和结束小球
if (i == 0) {
canvas.drawCircle(startX + botderLeft + gap * i, getHByValue(mData.get(i).y2), 8, mPaint);
mPaint.setColor(0xffffffff);
canvas.drawCircle(startX + botderLeft + gap * i, getHByValue(mData.get(i).y2), 4, mPaint);
mPaint.setColor(mColor2);
}
if (i == mData.size() - 2) {
canvas.drawCircle(startX + botderLeft + gap * (i + 1), getHByValue(mData.get(i + 1).y2), 8, mPaint);
mPaint.setColor(0xffffffff);
canvas.drawCircle(startX + botderLeft + gap * (i + 1), getHByValue(mData.get(i + 1).y2), 4, mPaint);
mPaint.setColor(mColor2);
}
}
}
private void drawTheX(Canvas canvas) {
//画中间的线
initPaint();
//纵坐标文字距离Y轴线的距离
int textLeftBorder = DPUnitUtil.dip2px(mContext, 2);
mPaint.setColor(0xff92dac4);
//0度线
mPaint.setStrokeWidth(DPUnitUtil.dip2px(mContext, 1));
canvas.drawLine(botderLeft - textLeftBorder, getHeight() / 2, getWidth(), getHeight() / 2, mPaint);
mPaint.setColor(mContext.getResources().getColor(R.color.colorPrimary));
//每条横线的上下间隔
float step = (getHeight() / 2 - borderTopAndBottom) / 5;
int stepInt = (max - min) / 10;
//纵坐标文字大小
mPaint.setStrokeWidth(DPUnitUtil.dip2px(mContext, 1));
mPaint.setPathEffect(new DashPathEffect(new float[]{15, 10, 3, 10}, 0));
for (int i = 0; i < 11; i++) {
//写纵坐标文字
mPaint.setColor(0xff159461);
canvas.drawText(max - i * stepInt + "",
botderLeft - mPaint.measureText(max - i * stepInt + "") - textLeftBorder,
borderTopAndBottom + i * step + (mPaint.getFontMetrics().bottom - mPaint.getFontMetrics().top) / 2 - mPaint.getFontMetrics().bottom,
mPaint);
if (i == 5) {
continue;
}
mPaint.setColor(0xdd92dac4);
mPaint.setStrokeWidth(DPUnitUtil.dip2px(mContext, 0.5f));
canvas.drawLine(botderLeft, borderTopAndBottom + i * step, getWidth(), borderTopAndBottom + i * step, mPaint);
}
}
private void drawTheBackground(Canvas canvas) {
}
//触摸处理
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mData == null || mData.size() == 0) {
return super.onTouchEvent(event);
}
final int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
// 按下
mXDown = (int) event.getRawX();
break;
case MotionEvent.ACTION_MOVE:
// 移动
mLastX = (int) event.getRawX();
//1.5是加速滑动
int tempx = (int) (lastStartX + (mLastX - mXDown) * 1.5);
// if (Math.abs(lastStartX - mXDown) < a) {
// break;
// }
//滑动限制
if (tempx > botderLefttep) {
tempx = botderLefttep;
}
if (tempx < -((mData.size() + 1) * gap + botderLeft - getWidth())) {
tempx = -((mData.size() + 1) * gap + botderLeft - getWidth());
}
if(startX == tempx){
//说明已经绘制过,不再绘制
break;
}
//1.5是加速滑动
startX = tempx;
postInvalidate();
break;
case MotionEvent.ACTION_UP:
// 抬起
lastStartX = startX;
postInvalidate();
break;
default:
break;
}
return super.onTouchEvent(event);
}
//通过Y的值获取Y轴坐标
private float getHByValue(int y) {
return (((float) (max - y) / (float) (max - min))) * (getHeight() - borderTopAndBottom * 2f) + borderTopAndBottom;
}
public static class dataObj {
int x;
int y1;
int y2;
public void setX(int x) {
this.x = x;
}
public void setY1(int y1) {
this.y1 = y1;
}
public void setY2(int y2) {
this.y2 = y2;
}
}
}
复制代码
在布局文件中使用。
<top.greendami.greendami.ShakeMaps
android:layout_width="match_parent"
android:layout_height="500dp"
android:id="@+id/shakemaps"/>
复制代码
在Activity中添加数据
List<ShakeMaps.dataObj> mData = new ArrayList<>();
ShakeMaps.dataObj obj;
for(int i = 0;i < 1000 ; i++){
obj = new ShakeMaps.dataObj();
obj.setX(i);
obj.setY1((int)(Math.random()* -60) + 30);
obj.setY2((int)(Math.random()* 60) - 30);
mData.add(obj);
}
((ShakeMaps)findViewById(R.id.shakemaps)).setmData(mData,35,-35);
复制代码
可能用到的工具 Hierarchy Viewer,Monitors等等。