定义控件Android自定义控件Android开发

记一次意外的自定义控件

2016-03-02  本文已影响832人  Razerdp

有时候,意外也许就会造成一个不经意间的成功。

【注意:本文章前两节尽是吐槽,要看代码,实现方案什么的,请直接看第三节】
【注意:本文章前两节尽是吐槽,要看代码,实现方案什么的,请直接看第三节】
【注意:本文章前两节尽是吐槽,要看代码,实现方案什么的,请直接看第三节】
重要的话要说三遍。。。

咳咳,,,咱们不是专业写手,就不要那么装文艺了,还是逗比点好。
不如咱们先上个图?


效果图

咳咳,请忽略我竖屏录制了啦。。。。还有,请忽略为啥那条线会在屏幕边边走,在下不拘束它的自由←_←

起因

事情的起源是这样滴,因为某种需求,咱们需要撸一个这样子的控件(为了不泄露设计图,咱们就拿MPAndroidChart的图展示吧,反正需求都一样):

伪设计图

拿到设计图,第一想法,这有多难,直接上MP库呗,于是把库放到MethodsCount一查,哭了。。。2K多个方法欸,2K欸!!!!2K!!!!

方法统计

遂放弃,,,还是自己开干吧

看到曲线什么的,第一时间**贝塞尔曲线**走起~ 于是,最为一个面向搜索引擎编程的程序员,当然谷歌一下贝塞尔。。。

随便搜搜,于是就看到CSDN的一篇文章文章点我

啊~好细致,好赞啊!!!可惜在下没法短时间内理解啊TAT。然而,按照我平时的经验,还是撸个初步的东西出来吧。。。

OMG

OMG....这神马啊,这尖尖,都快能戳死人了好吗。。。。
于是,选择战略性撤退,休息一晚再开干。

意外

第二天,毫无疑问的继续一脸蒙逼。。。
这时候,一位老朋友叫我帮他抠个图,是的,你没看错,抠图。。。。如果有看过我的一起撸个朋友圈系列文章的人,或许会知道,在下也会AE这个视频后期软件。。。

抠就抠吧。。。。但!!!
意外就这么来了。。。。抠图的时候,为了边缘平滑,我经常调节锚点,使曲线更加的平滑,然后居然让我发现了一个规律0.0,大致原理如下吧:

AE

如图,如果多看几遍,也许你会发现,当两个控制点的x位置在前后两个坐标内,而y分别与前后两个坐标平齐的时候,转折点的衔接最为平滑,否则妥妥的出现尖尖(嗯。。。我还特地用鼠标绕了几圈标出尖尖位置)。

妈蛋,得来毫不费功夫啊。。。。真的想抱着我朋友亲几口,可惜在下不搞基- -

实现

既然找到了突破口,那妥妥的开干啊。

于是兴冲冲的继承View,开始我们的伟业:

public class TestView extends View {
    // 最大值
    private final float maxValue = 100f;
    // 测试数据
    private float[] testDatas = { 55f, 38f, 50f, 44f, 31f, 22f, 9f, 19f, 50f, 78f, 62f, 51f, 45f, 66f, 79f, 50f, 33f,
            24f, 26f, 58f };
    //private float[] testDatas = { 60f, 55f, 57f, 50f ,56f,70f};
    //private float[] testDatas = { 60f, 55f};

    // 点记录
    private List<Point> datas;

    private final int num = 12;

    // 路径
    private Path clicPath;
    // 渐变填充
    private Paint mPaint;
    
    // 辅助性画笔
    private Paint controllPaintA;
    private Paint controllPaintB;
    private Path linePath;

    
    private PathMeasure mPathMeasure;
    private float[] mCurrentPosition = new float[2];
    private float[] mPrePosition = new float[2];

    LinearGradient mGradient;

    int width;
    int height;
    int offSet;

...构造器初始化以上的东西

我们定义了一个最大值,和一组测试数据。这个最大值的作用是用来计算当前数据在屏幕的y位置,比如这样:最大值100,我们的数值15,但我们的屏幕是720*1280,那么当然不可以只画15像素了,这怎么看得到嘛,我们的y位置判定为:

屏幕高度*(1-(15/100))

为什么要用1减去百分比,因为原点不在左下角而在左上角,所以我们需要减掉。

接下来到measure初始化我们的点。

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
        height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
        offSet = width / testDatas.length;
        if (datas.size() == 0) {
            for (int i = 0; i < testDatas.length; i++) {
                float ratio = testDatas[i] / maxValue;
                Point point;
                if (i == 0) {
                    point = new Point(0, (int) (height * (1 - ratio)));
                }
                else if (i == testDatas.length - 1) {
                    point = new Point(width, (int) (height * (1 - ratio)));
                }
                else {
                    point = new Point(i * offSet, (int) (height * (1 - ratio)));
                }
                datas.add(point);
            }
        }
        if (mGradient == null) {
            mGradient = new LinearGradient(getMeasuredWidth() >> 1, getMeasuredHeight() >> 1, getMeasuredWidth() >> 1,
                    getMeasuredHeight(), Color.parseColor("#e0cab3"), Color.parseColor("#ffffff"),
                    Shader.TileMode.CLAMP);
            mPaint.setShader(mGradient);
        }
    }

其中我们的offSet是偏移量,其作用是使点在屏幕上的x位置是均分的,然后初始化一个线性渐变。

这时候我们的点是这样的(为了更方便查看,我们设定为横屏并给上线条):

点和点之间的x偏移都是一致的(最后一个除外)

然后我们在onDraw开始绘制():

 @Override
    protected void onDraw(Canvas canvas) {
        clicPath.reset();
        super.onDraw(canvas);
        //clicPath.moveTo(datas.get(0).x, datas.get(0).y);
        for (int i = 0; i < datas.size() - 1; i++) {
            Point startPoint = datas.get(i);
            Point endPoint = datas.get(i + 1);
            if (i == 0) clicPath.moveTo(startPoint.x, startPoint.y);

            int controllA_X = (startPoint.x + endPoint.x) >>1;
            int controllA_Y = startPoint.y;
            int controllB_X = (startPoint.x + endPoint.x) >>1;
            int controllB_Y = endPoint.y;
            clicPath.cubicTo(controllA_X, controllA_Y, controllB_X, controllB_Y, endPoint.x, endPoint.y);

            // 控制点展示
            canvas.drawCircle(controllA_X,controllA_Y,5,controllPaintA);
            canvas.drawCircle(controllB_X,controllB_Y,5,controllPaintB);


            canvas.drawCircle(startPoint.x,startPoint.y,5,mPaint);


            //控制点展示
            canvas.drawLine(startPoint.x,startPoint.y,controllA_X,controllA_Y,mPaint);
            canvas.drawLine(endPoint.x,endPoint.y,controllB_X,controllB_Y,mPaint);

        }
        clicPath.lineTo(datas.get(datas.size() - 1).x, height);
        clicPath.lineTo(datas.get(0).x, height);
        clicPath.lineTo(datas.get(0).x, datas.get(0).y);
        canvas.drawPath(clicPath, mPaint);
    }

这里解析一下:
当i==0,也就是画第一个点的时候,我们需要把画笔移到我们第一个点的位置,否则永远都会从0,0开始,以后就不需要移动了,因为画完一条线后,画笔位置会停留在最后一个点。

我们可以看到两个控制点的坐标,跟我们上面AE展示出来的是一样的,x位置都是取两个点的中间,y则是分别跟两边平齐,这样的曲线最为圆滑

clicPath.cubicTo这个方法,前面4个参数分别代表着控制点1的xy,控制点2的xy,最后一个参数则是结束点的xy,在下一次循环到来之时,最后一个参数则会作为下一次绘制的起点。

最后别忘了在循环外面将path封闭起来,我们不可以直接用path.close(),因为close方法是最后一个点与第一个点直接连一条直线的,但我们需要填充曲线下方。

为了方便展示,我们添加了参考点以及将线条设置为stroke,先不填充:

预览图

可以看到,我们的控制点都很好的分布在两点之间,曲线看起来十分平滑。

为了更清晰,我们将测试数据减少一点:

private float[] testDatas = { 60f, 30f, 57f, 41f ,88f,70f};
预览图2

现在看起来更加的清晰,然后我们填充一下并取消掉辅助线条和辅助点。

预览图3

现在初步达到我们的效果了。。

然而,程序员的冤家产品却说:哎,这太单调了,给个动画呗。。。。

妈蛋!!!!!

不过骂完还是得干啊-T-

于是这次我们需要借助PathMeasure这个类

这个类通常用于将某个path转换为一个具体的position,更多情况下是用作路径动画。

还记得我们之前定义的变量里面有些什么吗:

    private PathMeasure mPathMeasure;
    private float[] mCurrentPosition = new float[2];
    private float[] mPrePosition = new float[2];

根据命名,也很清楚是干啥的。

接下来继续开工:

首先定义一个公用方法给外部调用:

public void startAnima(long duration) {}

我们通过这个方法来绘制线条

然后我们利用ValueAnimator来动态获取我们path的坐标

 public void startAnima(long duration) {
        if (mPathMeasure == null) mPathMeasure = new PathMeasure(clicPath, true);
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());
        valueAnimator.setDuration(duration);
        // 减速插值器
        valueAnimator.setInterpolator(new DecelerateInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (Float) animation.getAnimatedValue();
                // 获取当前点坐标封装到mCurrentPosition
                mPathMeasure.getPosTan(value, mCurrentPosition, null);
                invalidate();
                if (value == mPathMeasure.getLength()) animaFirst = true;
            }
        });
        valueAnimator.start();
    }

为了防止onDraw里面多次绘制,我们定义一个animaFirst。

然后补充我们的onDraw方法:

  @Override
    protected void onDraw(Canvas canvas) {
    ...
        if (animaFirst) {
            linePath.moveTo(datas.get(0).x, datas.get(0).y);
            mPrePosition[0] = datas.get(0).x;
            mPrePosition[1] = datas.get(0).y;
            animaFirst = false;
        }
        else {
            int controllA_X = (int) ((mPrePosition[0] + mCurrentPosition[0]) /2);
            int controllA_Y = (int) mPrePosition[1];
            int controllB_X = (int) ((mPrePosition[0] + mCurrentPosition[0]) /2);
            int controllB_Y = (int) mCurrentPosition[1];
            linePath.cubicTo(controllA_X, controllA_Y, controllB_X, controllB_Y, mCurrentPosition[0],
                    mCurrentPosition[1]);
            mPrePosition[0] = mCurrentPosition[0];
            mPrePosition[1] = mCurrentPosition[1];
        }
        canvas.drawPath(linePath, controllPaintA);
    }

如果动画刚启动,我们就把点移到第一个点的位置,同时记录
如果动画已经启动了,我们就重复前面的步骤画出贝塞尔,当然,你也可以直接lineTo,然后将当前点付给前一个点。

最后,我们在onDetachedFromWindow清掉各种信息,毕竟那啥,内存还是挺珍贵的对吧-V-

   @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        
        datas.clear();
        clicPath=null;
        controllPaintA=null;
        controllPaintB=null;
        mPathMeasure=null;
        
    }

最终效果图(未修复到屏幕边边继续画的问题。。。,以及貌似有些地方有点偏差):

preview

【附】所有代码(可以直接copy使用,因为是测试demo,所以并没有封装什么的,同时measure那里也没有指定wrap_content时的大小,大家可以自行封装或修复或扩展哈哈-V-):

/**
 * Created by 大灯泡 on 2016/2/29.
 */
public class TestView extends View {
    // 最大值
    private final float maxValue = 100f;
    // 测试数据
    //private float[] testDatas = { 55f, 38f, 50f, 44f, 31f, 22f, 9f, 19f, 50f, 78f, 62f, 51f, 45f, 66f, 79f, 50f, 33f,
    //        24f, 26f, 58f };
    private float[] testDatas = { 60f, 30f, 57f, 41f, 88f, 70f };
    //private float[] testDatas = { 60f, 55f};

    // 点记录
    private List<Point> datas;
    // 路径
    private Path clicPath;
    // 渐变填充
    private Paint mPaint;
    // 辅助性画笔
    private Paint controllPaintA;
    private Paint controllPaintB;
    private Path linePath;

    private PathMeasure mPathMeasure;
    private float[] mCurrentPosition = new float[2];
    private float[] mPrePosition = new float[2];
    LinearGradient mGradient;
    int width;
    int height;
    int offSet;

    public TestView(Context context) {
        this(context, null);
    }

    public TestView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        clicPath = new Path();
        linePath = new Path();
        datas = new ArrayList<>();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        //mPaint.setStyle(Paint.Style.STROKE);
        controllPaintA = new Paint(Paint.ANTI_ALIAS_FLAG);
        controllPaintA.setStyle(Paint.Style.STROKE);
        controllPaintA.setStrokeWidth(5);
        controllPaintA.setColor(0xffff0000);

        controllPaintB = new Paint(Paint.ANTI_ALIAS_FLAG);
        controllPaintB.setStyle(Paint.Style.STROKE);
        controllPaintB.setColor(0xff00ff00);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
        height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
        offSet = width / testDatas.length;
        if (datas.size() == 0) {
            for (int i = 0; i < testDatas.length; i++) {
                float ratio = testDatas[i] / maxValue;
                Point point;
                if (i == 0) {
                    point = new Point(0, (int) (height * (1 - ratio)));
                }
                else if (i == testDatas.length - 1) {
                    point = new Point(width, (int) (height * (1 - ratio)));
                }
                else {
                    point = new Point(i * offSet, (int) (height * (1 - ratio)));
                }
                datas.add(point);
            }
        }
        if (mGradient == null) {
            mGradient = new LinearGradient(getMeasuredWidth() >> 1, getMeasuredHeight() >> 1, getMeasuredWidth() >> 1,
                    getMeasuredHeight(), Color.parseColor("#e0cab3"), Color.parseColor("#ffffff"),
                    Shader.TileMode.CLAMP);
            mPaint.setShader(mGradient);
        }
    }

    private boolean animaFirst = true;
    @Override
    protected void onDraw(Canvas canvas) {
        clicPath.reset();
        super.onDraw(canvas);
        for (int i = 0; i < datas.size() - 1; i++) {
            Point startPoint = datas.get(i);
            Point endPoint = datas.get(i + 1);
            if (i == 0) clicPath.moveTo(startPoint.x, startPoint.y);

            int controllA_X = (startPoint.x + endPoint.x) >> 1;
            int controllA_Y = startPoint.y;
            int controllB_X = (startPoint.x + endPoint.x) >> 1;
            int controllB_Y = endPoint.y;
            clicPath.cubicTo(controllA_X, controllA_Y, controllB_X, controllB_Y, endPoint.x, endPoint.y);
            /**辅助点和线**/
            //canvas.drawCircle(controllA_X,controllA_Y,5,controllPaintA);
            //canvas.drawCircle(controllB_X,controllB_Y,5,controllPaintB);

            //canvas.drawCircle(startPoint.x,startPoint.y,5,mPaint);

            //canvas.drawLine(startPoint.x,startPoint.y,controllA_X,controllA_Y,mPaint);
            //canvas.drawLine(endPoint.x,endPoint.y,controllB_X,controllB_Y,mPaint);

        }
        clicPath.lineTo(datas.get(datas.size() - 1).x, height);
        clicPath.lineTo(datas.get(0).x, height);
        clicPath.lineTo(datas.get(0).x, datas.get(0).y);
        canvas.drawPath(clicPath, mPaint);

        if (animaFirst) {
            linePath.moveTo(datas.get(0).x, datas.get(0).y);
            mPrePosition[0] = datas.get(0).x;
            mPrePosition[1] = datas.get(0).y;
            animaFirst = false;
        }
        else {
            int controllA_X = (int) ((mPrePosition[0] + mCurrentPosition[0]) / 2);
            int controllA_Y = (int) mPrePosition[1];
            int controllB_X = (int) ((mPrePosition[0] + mCurrentPosition[0]) / 2);
            int controllB_Y = (int) mCurrentPosition[1];
            linePath.cubicTo(controllA_X, controllA_Y, controllB_X, controllB_Y, mCurrentPosition[0],
                    mCurrentPosition[1]);
            mPrePosition[0] = mCurrentPosition[0];
            mPrePosition[1] = mCurrentPosition[1];
        }
        canvas.drawPath(linePath, controllPaintA);
    }

    public void startAnima(long duration) {
        if (mPathMeasure == null) mPathMeasure = new PathMeasure(clicPath, true);
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());
        valueAnimator.setDuration(duration);
        // 减速插值器
        valueAnimator.setInterpolator(new DecelerateInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (Float) animation.getAnimatedValue();
                // 获取当前点坐标封装到mCurrentPosition
                mPathMeasure.getPosTan(value, mCurrentPosition, null);
                Log.d("curX",""+mCurrentPosition[0]);
                invalidate();
                if (value == mPathMeasure.getLength())
                    animaFirst = true;
            }
        });
        valueAnimator.start();
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        datas.clear();
        clicPath = null;
        controllPaintA = null;
        controllPaintB = null;
        mPathMeasure = null;
    }
}
上一篇下一篇

猜你喜欢

热点阅读