Android鬼点子 举例说明自定义View性能优化

这次是要实现一个至少有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等等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值