这次是要实现一个至少有 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 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 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;
- }
- }
- }
在布局文件中使用。
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 等等。
来源: https://juejin.im/post/5a33153b6fb9a0450671a842