Hands with you to explain a complex dynamic efficiency of custom drawing

Primer

Custom View is an important part of senior android UI knowledge. Also distinguish watershed senior developers. Senior developers, knowledge system, the language whenever possible to describe the effects, they can always give solutions. The mid-level developers due to the limited field of vision, often encounter complex needs can not start.
Some seemingly complex special effects, in fact, android has provided a solution for us, which is a must Advanced Intermediate advanced knowledge.
This paper presents the complete Raiders, ensure a soul into. = =!

( Pulled left GitHub link, you need access to relevant content such as interviews can find their own )
https://github.com/xiangjiana/Android-MS

Renderings

The following figure you can see, first we saw a heart-shaped, then there are waves in the beating, and finally fill the whole heart-shaped green
Hands with you to explain a complex dynamic efficiency of custom drawing

At first glance

Eh? Heart is how to draw? Eh? Wave is how drawn out, but also how to move up? Eh? Character is showing how the two colors of the same time?

Do not know if someone has such doubts `` `` please continue to read.

The effect of dismantling

Get a complex special effects, the first thing do not panic, first a closer look, this effect inside which specific details can be split out. Things are complicated by the simple combination of details from.

Start dismantling

1, is a heart-shaped drawing region
2, the wave from the bottom, gradually filling the entire heart shaped green
3, middle text "a wolf", and during the growth of the waves, there is a period of time text the two parts of different color states.

The case used knowledge:

1、 canvas.clipPath 画布裁剪
2、 canvas.save 画布状态保存
3、 canvas.restore 恢复
4、 canvas.translate 画布平移
5、 path.rCubicTo 构建三阶贝塞尔曲线(相当于上一个点位置)
6、属性动画 ValueAnimator / AnimatorSet

开始撸码
第 1步:构建一个心形区域
当一个复杂图形摆在我们面前,而且还是不规则图形,我们首先应该想到的,就是 android.graphics.Path 类,它可以记录复杂图形的全部点组成的路径。关键代码:

/**
     * 构建心形
     * <p>
     * 注意,它这个是以 矩形区域中心点为基准的图形,所以绘制的时候,必须先把坐标轴移动到 区域中心
     */
     private void initHeartPath(Path path) {
        List<PointF> pointList = new ArrayList<>();
        pointList.add(new PointF(0,Utils.dp2px(-38)));
        pointList.add(new PointF(Utils.dp2px(50),Utils.dp2px(-103)));
        pointList.add(new PointF(Utils.dp2px(112),Utils.dp2px(-61)));
        pointList.add(new PointF(Utils.dp2px(112),Utils.dp2px(-12)));
        pointList.add(new PointF(Utils.dp2px(112),Utils.dp2px(37)));
        pointList.add(new PointF(Utils.dp2px(51),Utils.dp2px(90)));
        pointList.add(new PointF(0,Utils.dp2px(129)));
        pointList.add(new PointF(Utils.dp2px(-51),Utils.dp2px(90)));
        pointList.add(new PointF(Utils.dp2px(-112),Utils.dp2px(37)));
        pointList.add(new PointF(Utils.dp2px(-112), Utils.dp2px(-12)));
        pointList.add(new PointF(Utils.dp2px(-112),Utils.dp2px(-61)));
        pointList.add(new PointF(Utils.dp2px(-50),Utils.dp2px(-103)));

        path.reset();
        for(int i =0; i <4; i++) {
            if (i ==0) {
                path.moveTo(pointList.get(i *3).x, pointList.get(i *3).y);
            } else {
                path.lineTo(pointList.get(i * 3).x, pointList.get(i *3).y);
            }

            int endPointIndex;
            if (i ==3) {
                endPointIndex = 0;
            } else {
                endPointIndex = i *3+3;
            }

            path.cubicTo(pointList.get(i *3+1).x, pointList.get(i *3+1).y,
                    pointList.get(i *3+2).x, pointList.get(i *3+2).y,
                    pointList.get(endPointIndex).x, pointList.get(endPointIndex).y);
                   //你的心形就是用贝塞尔曲线来画的吗
        }
        path.close();
        path.computeBounds(mHeartRect,false);
       //把path所占据的最小矩形区域,返回出去
    }

传入一个 Path引用,然后在方法内部对 path进行各种 api调用改变其属性. 这里需要提及一个重点:最后一行代码 path.computeBounds(mHeartRect,false);意思是,无论什么样的 path,它都会占据一个最小矩形区域, computeBounds方法可以获取这个矩形区域,设置给入参 mHeartRect.

第 2步:将心形区域裁剪出来, 裁剪之后,后续的绘制都只会显示在这个区域之内
(为了作图方便,我们通常先把坐标轴原点移动到 绘制区域的正中央)

 @Override
  protected void onDraw(Canvas canvas) {

        int width = getWidth();
        int height = getHeight();
        canvas.translate(width / 2, height /2);
        //为了作图方便,我们通常先把坐标轴原点移动到 绘制区域的正中央
        ...省略无关代码

        canvas.clipPath(mMainPath);
        //裁剪心形区域
        canvas.save();
       //保存画布状态

        ...省略无关代码

    }

第 3步:绘制波浪区域
这里有两点细节

1)波浪区域分为两块, top和 bottom 上下两块
2) 整个波浪区域的长度为 心形矩形范围宽度的 2倍 ( ?为什么是2倍?因为上面的波浪动画,其实是整个波浪区域平移造成的视觉效果,为了让这个动画可以无限执行,设计两倍宽度,当一半的宽度向右移动刚好触及心形矩形区域的右边框的时候,让它还原到原始位置,这样就能无缝衔接。)

关键代码1 - 波浪path的构建

 /**
     * @param ifTop   是否是上部分; 上下部分的封口位置不一样
     * @param r       心形的矩形区域
     * @param process 当前进度值
     */
    private void resetWavePath(boolean ifTop,RectF r,float process,Pathpath) {
        final float width = r.width();
        final float height = r.width();

        path.reset();

        if( ifTop) {
            path.moveTo(r.left - width, r.top);
        } else {
            path.moveTo(r.left - width, r.bottom);
            //下部,初始位置点在 下
       }

        float waveHeight = height /8f;//波动的最大幅度

        //找到矩形区域的左边线中点
        path.lineTo(r.left - width,r.bottom - height * process);

        //做两个周期的贝塞尔曲线
        for (int i =0; i < 2; i++) {
            float px1, py1, px2, py2, px3, py3;

            px1 = width /4;
            py1 = -waveHeight;

            px2 = width /4*3;
            py2 = waveHeight;

            px3 = width;
            py3 = 0;

            path.rCubicTo(px1, py1, px2, py2, px3, py3);
        }
        if (ifTop) {
            path.lineTo(r.right, r.top);
        } else {
            path.lineTo(r.right, r.bottom);
        }
        path.close();

    }

关键代码2- 属性动画改变两个全局变量波浪的向上增长系数以及横向波浪动画系数:

    AnimatorSet animatorSet;
    // 动起来
    public void startAnimator() {

        if(animatorSet == null) {
            animatorSet = new AnimatorSet();
            ValueAnimator growAnimator = ValueAnimator.ofFloat(0f, 1f);
            growAnimator.addUpdateListener(animation -> growProcess =(float) animation.getAnimatedValue());
            growAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    animatorSet.cancel();
                }
            });
            growAnimator.setInterpolator(new DecelerateInterpolator());
            growAnimator.setDuration((long)(4000/ animatorSpeedCoefficient));

            ValueAnimator waveAnimator = ValueAnimator.ofFloat(0f,1f);
            waveAnimator.setRepeatCount(ValueAnimator.INFINITE);
            waveAnimator.setRepeatMode(ValueAnimator.RESTART);
            waveAnimator.addUpdateListener(animation -> {
                waveProcess = (float) animation.getAnimatedValue();
                invalidate();
            });
            waveAnimator.setInterpolator(new LinearInterpolator());
            waveAnimator.setDuration((long)(1000/ animatorSpeedCoefficient));

            animatorSet.playTogether(growAnimator, waveAnimator);
            animatorSet.start();
        } else {
            animatorSet.cancel();
            animatorSet.start();
        }
    }

关键代码3- 利用属性动画改变的全局变量,构建动态效果

  @Override
   protected void onDraw(Canvas canvas) {

        int width = getWidth();
        int height = getHeight();
        canvas.translate(width /2, height /2);
        //为了作图方便,我们通常先把坐标轴原点移动到 绘制区域的正中央
        curXOffset = waveProcess * mHeartRect.width();
       //当前X轴方向上 波浪偏移量

        canvas.clipPath(mMainPath);
        canvas.save();

        mainRect = new Rect();
        ...省略无关代码

        // 上波浪区域
        resetWavePath(true, mHeartRect, growProcess, topWavePath);
        canvas.translate(curXOffset,0);
        canvas.clipPath(topWavePath);
        canvas.drawPath(topWavePath, mTopPaint);
        ...省略无关代码

        //下波浪区域
        resetWavePath(false, mHeartRect, growProcess, bottomWavePath);
        canvas.restore();
        canvas.translate(curXOffset,0);
        canvas.clipPath(bottomWavePath);
        canvas.drawPath(bottomWavePath, mBottomPaint);
       ...省略无关代码

    }

第 4步:绘制“一条大灰狼” 到心形中央,并且达成双色效果
这里有两个细节:

  1. canvas.drawText, 就算你把paint 设置了 .setTextAlign(Paint.Align.CENTER); 它也未必会在你给的 x,y为中心 绘制。原因就不解释了,谷歌大佬就是这么设计的。解决方法:利用 paint.getTextBounds,获得文字的矩形区域。然后在真正 canvas.drawText,计算y的时候考虑这个矩形区域,就像下面这样如下
    mainRect = new Rect();
    textBottomPaint.getTextBounds(text,0, text.length(), mainRect);
  2. 由于之前波浪的横向移动,坐标轴产生了平移,所以我绘制文字,要将平移的距离减去,再绘制,保证居中,且文字位置不随着波浪的横向移动而变化。

完整代码如下(此步骤的关键代码已经标红):
Hands with you to explain a complex dynamic efficiency of custom drawing

结语

来解答 乍一看里面提出的3个问题:

诶?心形是怎么绘制的?答:构建Path,然后 canvas.clipPath裁剪画布,裁剪之后,所有的作图效果就只在这个心形区域内可见

诶?波浪是怎么画出来的,又是如何动起来的?答:波浪,或者说波浪区域,也是 Path构建,主要由一根波浪线以及三根直线组成,是一个封闭区域. 让波浪动起来,其实就是 canvas平移操作,利用属性动画+双倍宽度的波浪区域,形成无缝无限循环动画.

诶? 文字是怎么呈现出同一时刻的两种颜色的?答:在两个相邻的波浪区域,使用不一样的颜色绘制两次文字。视觉效果上还是一串文字,但是实际上是两次绘制的组合效果。神奇吗?神奇个屁,其实就是 同一位置绘制两次文字,后面的覆盖前面的......话粗理不粗- -!

话题延伸

要想随心所欲地掌控自定义View,需要有完整的知识体系。

Tree conceptual structure view of
measurement, layout, drawing process
event distribution / conflict sliding core principle
CanvasPaintPath draw common API
Bitmap bitmap
attribute animation
If the interaction occurs with certain View system, and the system may require you to know the source
but to arbitrary use a custom View, just so is not enough, you also need: a good mathematical foundation

Because most of the irregular shapes, may need assistance thoughts mathematical formula, such as:

Construction of a heart-shaped path
infinite wave design ideas
Bezier subsequent article will be referred to

We are inseparable from the years when the math class to develop mathematical thinking, if the mathematical foundation is bad, start these effects tend to be more difficult.

( Pulled left GitHub link, you need access to relevant content such as interviews can find their own )
https://github.com/xiangjiana/Android-MS

Guess you like

Origin blog.51cto.com/14541311/2458922