此间仅仅是技术实现.,这里的一个网站可以在线模拟钢笔工具的使用

电池背景

因为电池内部有少数个部分,所以本例用了一个Grid来做背景,用Clip属性剪切出一个电池的概略,这样不光映现出一个电池的概况,还可以够避免水波和气泡跑呈现Grid的外面.

Clip的其中,是一个Path形状.具体画法就不多说了,从前写过.有趣味的校友看这里:http://www.cnblogs.com/tsliwei/p/5609035.html

图片 1

这多少个意义来自于三星S5的充电界面,版权归三星所有,这里唯有是技巧实现.

源代码

此次的任课代码已经全部上传到Github :

https://github.com/xuyisheng/BezierArt

欢迎我们提issue。

先上效果图

表示电量的液体效果

所有液体分两有的,上边是波浪,下面是矩形.进度值实际决定的是矩形的中度.六个控件放到StackPanel中,让下边的矩形往上顶.最后给波浪底部Margin值为-1,使其看起来没有间隙.

图片 2

波浪是用贝塞尔曲线实现的,首先介绍下贝塞尔曲线

图片 3

贝塞尔曲线有4个点,起源终点和五个控制点.(此括号里的可以不看:上画画的并不确切,因为控制点并不一定在曲线上).通过多少个控制点决定曲线的路径.

眼看上图这本身就是个波浪形.使用点动画PointAnimation控制几个点光景移动就有了波浪的动态效果.注意多个卡通时间毫无一样,否则看起来动画太假.多少个时刻错开一点点就好了.

图片 4

波浪部分宽度是50,中度是5

图片 5

圆滑绘图

当在屏幕上绘制路径时,例如手写板,最核心的不二法门是透过Path.lineTo将逐条触点连接起来,而这种模式在众多时候会发觉,六个点的连接是分外生硬的,因为它究竟是经过直线来连续的,尽管通过二阶贝塞尔曲线来将相继触点连接,就会圆滑的多,不会产出太多的生硬连接。

先来看下代码,分外简单的绘图路径代码:

package com.xys.animationart.views;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

/**
 * 圆滑路径
 * <p/>
 * Created by xuyisheng on 16/7/19.
 */
public class DrawPadBezier extends View {

    private float mX;
    private float mY;
    private float offset = ViewConfiguration.get(getContext()).getScaledTouchSlop();

    private Paint mPaint;
    private Path mPath;

    public DrawPadBezier(Context context) {
        super(context);
    }

    public DrawPadBezier(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPath = new Path();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);
        mPaint.setColor(Color.RED);
    }

    public DrawPadBezier(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mPath.reset();
                float x = event.getX();
                float y = event.getY();
                mX = x;
                mY = y;
                mPath.moveTo(x, y);
                break;
            case MotionEvent.ACTION_MOVE:
                float x1 = event.getX();
                float y1 = event.getY();
                float preX = mX;
                float preY = mY;
                float dx = Math.abs(x1 - preX);
                float dy = Math.abs(y1 - preY);
                if (dx >= offset || dy >= offset) {
                    // 贝塞尔曲线的控制点为起点和终点的中点
                    float cX = (x1 + preX) / 2;
                    float cY = (y1 + preY) / 2;
//                    mPath.quadTo(preX, preY, cX, cY);
                    mPath.lineTo(x1, y1);
                    mX = x1;
                    mY = y1;
                }
        }
        invalidate();
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(mPath, mPaint);
    }
}

先来看下通过mPath.lineTo来落实的绘图,效果如下所示:

18.png

图形中的拐点有显然的锯齿效果,即通过直线的总是,再来看下通过贝塞尔曲线来连续的效果,平日情况下,贝塞尔曲线的操纵点取六个连续点的中心:

mPath.quadTo(preX, preY, cX, cY);

通过二阶贝塞尔曲线的连天效果如图所示:

19.png

可以肯定的发现,曲线变得尤为圆滑了。

图片 6

图片 7

三阶模拟

三阶贝塞尔曲线在Android中的API为:cubicTo()和rCubicTo(),这多少个API在常理上是可以相互转换的——quadTo是按照相对坐标,而rCubicTo是按照相对坐标,所未来面我都只以内部一个来展开教学。

有了二阶的基本功,再来模拟三阶就分外简单了,无非是充实了一个控制点而已,先看下效果图:

4.gif

代码只需要在二阶的功底上添加一些协助点即可,下边只交付一些要害代码,详细代码请参考Github:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        mPath.moveTo(mStartPointX, mStartPointY);
        // 辅助点
        canvas.drawPoint(mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliary);
        canvas.drawText("控制点1", mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliaryText);
        canvas.drawText("控制点2", mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliaryText);
        canvas.drawText("起始点", mStartPointX, mStartPointY, mPaintAuxiliaryText);
        canvas.drawText("终止点", mEndPointX, mEndPointY, mPaintAuxiliaryText);
        // 辅助线
        canvas.drawLine(mStartPointX, mStartPointY, mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliary);
        canvas.drawLine(mEndPointX, mEndPointY, mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliary);
        canvas.drawLine(mAuxiliaryOneX, mAuxiliaryOneY, mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliary);
        // 三阶贝塞尔曲线
        mPath.cubicTo(mAuxiliaryOneX, mAuxiliaryOneY, mAuxiliaryTwoX, mAuxiliaryTwoY, mEndPointX, mEndPointY);
        canvas.drawPath(mPath, mPaintBezier);
    }

气泡效果

此地的血泡效果就是个典型的粒子效果,而且是最简易的这种,并不涉及到怎么着复杂的公式总计.

简言之介绍下原理:这里的气泡可以用作是圆依据一定的快慢持续的进步(改变Y轴坐标).所以规定一个速率,规定一个离开,使用帧动画CompositionTarget.Rendering,在每一帧都在Y轴上加那么些速率在一帧移动的距离.然后判断又没达成规定的距离.如果达到,移除这些圈子,否则继续上升.

气泡可以分成两个部分:

1.电池内部的气泡.大小适宜,移动速度最慢,移动距离最短.

2.屏幕底边的大气泡,个头相比大,移动速度较慢,移动距离较短.

3.屏幕底边的小气泡,个头小小的,移动速度较快,移动距离较远.

新建一个Class,用来代表气泡音信

图片 8

里面多少个重大性质,一个是速率,一个是气泡需要活动的距离.这多少个特性决定了血泡的移动轨迹.第两个特性是用来判断气泡是不是做到了沉重,第两个属性是充分一个对气泡的引用,这样便于在后台控制气泡.

概念五个聚众,用来存放在三有些的血泡信息.

在帧渲染事件内,遍历多少个集合.让集合里的每个气泡都进化移动(Canvas.SetTop),判断气泡是不是早已移动了指定的距离,是的话就在页面移除气泡,集合也移除该气泡音信.判断集合的Count是不是自愧不如规定个个数,假使低于,就向页面添加气泡,集合添加气泡音讯.

GitHub地址:https://github.com/ptddqr/wpf-samsung-phone-s5-charging-ui/tree/master

计算

有了公式,只需要代码实现就OK了,我们先写两个公式:

package com.xys.animationart.util;

import android.graphics.PointF;

/**
 * 计算贝塞尔曲线上的点坐标
 * <p/>
 * Created by xuyisheng on 16/7/13.
 */
public class BezierUtil {

    /**
     * B(t) = (1 - t)^2 * P0 + 2t * (1 - t) * P1 + t^2 * P2, t ∈ [0,1]
     *
     * @param t  曲线长度比例
     * @param p0 起始点
     * @param p1 控制点
     * @param p2 终止点
     * @return t对应的点
     */
    public static PointF CalculateBezierPointForQuadratic(float t, PointF p0, PointF p1, PointF p2) {
        PointF point = new PointF();
        float temp = 1 - t;
        point.x = temp * temp * p0.x + 2 * t * temp * p1.x + t * t * p2.x;
        point.y = temp * temp * p0.y + 2 * t * temp * p1.y + t * t * p2.y;
        return point;
    }

    /**
     * B(t) = P0 * (1-t)^3 + 3 * P1 * t * (1-t)^2 + 3 * P2 * t^2 * (1-t) + P3 * t^3, t ∈ [0,1]
     *
     * @param t  曲线长度比例
     * @param p0 起始点
     * @param p1 控制点1
     * @param p2 控制点2
     * @param p3 终止点
     * @return t对应的点
     */
    public static PointF CalculateBezierPointForCubic(float t, PointF p0, PointF p1, PointF p2, PointF p3) {
        PointF point = new PointF();
        float temp = 1 - t;
        point.x = p0.x * temp * temp * temp + 3 * p1.x * t * temp * temp + 3 * p2.x * t * t * temp + p3.x * t * t * t;
        point.y = p0.y * temp * temp * temp + 3 * p1.y * t * temp * temp + 3 * p2.y * t * t * temp + p3.y * t * t * t;
        return point;
    }
}

俺们来将路径绘制到View中,看是否科学:

package com.xys.animationart.views;

import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.View;

import com.xys.animationart.util.BezierUtil;

/**
 * 通过计算模拟二阶、三阶贝塞尔曲线
 * <p/>
 * Created by xuyisheng on 16/7/13.
 */
public class CalculateBezierPointView extends View implements View.OnClickListener {

    private Paint mPaint;
    private ValueAnimator mAnimatorQuadratic;
    private ValueAnimator mAnimatorCubic;
    private PointF mPointQuadratic;
    private PointF mPointCubic;

    public CalculateBezierPointView(Context context) {
        super(context);
    }

    public CalculateBezierPointView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public CalculateBezierPointView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mAnimatorQuadratic = ValueAnimator.ofFloat(0, 1);
        mAnimatorQuadratic.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                PointF point = BezierUtil.CalculateBezierPointForQuadratic(valueAnimator.getAnimatedFraction(),
                        new PointF(100, 100), new PointF(500, 100), new PointF(500, 500));
                mPointQuadratic.x = point.x;
                mPointQuadratic.y = point.y;
                invalidate();
            }
        });

        mAnimatorCubic = ValueAnimator.ofFloat(0, 1);
        mAnimatorCubic.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                PointF point = BezierUtil.CalculateBezierPointForCubic(valueAnimator.getAnimatedFraction(),
                        new PointF(100, 600), new PointF(100, 1100), new PointF(500, 1000), new PointF(500, 600));
                mPointCubic.x = point.x;
                mPointCubic.y = point.y;
                invalidate();
            }
        });

        mPointQuadratic = new PointF();
        mPointQuadratic.x = 100;
        mPointQuadratic.y = 100;

        mPointCubic = new PointF();
        mPointCubic.x = 100;
        mPointCubic.y = 600;

        setOnClickListener(this);
    }

    @Override
    protected void onDraw(final Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(mPointQuadratic.x, mPointQuadratic.y, 10, mPaint);
        canvas.drawCircle(mPointCubic.x, mPointCubic.y, 10, mPaint);
    }

    @Override
    public void onClick(View view) {
        AnimatorSet set = new AnimatorSet();
        set.playTogether(mAnimatorQuadratic, mAnimatorCubic);
        set.setDuration(2000);
        set.start();
    }
}

本次我们并从未经过API提供的贝塞尔曲线绘制方法来绘制二阶、三阶贝塞尔曲线,而是经过时间t和最先点来计量一条贝塞尔曲线上的所有点,可以发现,通过算法统计出来的点,与经过API所绘制出来的点,是一点一滴合乎的。

GitHub地址:https://github.com/ptddqr/wpf-samsung-phone-s5-charging-ui/tree/master

画气泡

为了赏心悦目,我自己画了个气泡的模子,用在了大气泡上.小气泡直接用的椭圆,因为即选拔模型,因为太小,也看不出来.实际上大气泡也稍微看得出来.不过既然写了,依然介绍下吧.

图片 9

先是这么些气泡就是个ViewBox.方便缩放.

轮廓是个正圆,Fill给了个渐变画刷,向外不断深化,在最外侧0.85-1的有些是最深的.五个点的R都是20,B都是10,紫色部分G依次减小,分别是240,150,100.

图片 10

右下面的月牙是个Path,给了个半径是10的模糊效果.Fill是半透明的白色.月牙的画法就是五个弧线,起源和终端相同,半径不同.

图片 11

左上角的长处就是四个椭圆,和月牙一样.半径是10的模糊效果.Fill是半透明的白色.

 

2016-12-19更新:

发布到GitHub,地址:https://github.com/ptddqr/wpf-samsung-phone-s5-charging-ui/tree/master

源码下载: 三星手机电池充电效果.rar

贝塞尔曲线模拟

在Android中,一般的话,开发者只考虑二阶贝塞尔曲线和三阶贝塞尔曲线,SDK也只提供了二阶和三阶的API调用。对于再高阶的贝塞尔曲线,平常可以将曲线拆分成三个低阶的贝塞尔曲线,也就是所谓的降阶操作。下边将透过代码来效仿二阶和三阶的贝塞尔曲线是咋样绘制和操纵的。

贝塞尔曲线的一个相比好的动态演示如下所示:

http://myst729.github.io/bezier-curve/

20.png

这个效率来自于三星S5的充电界面,版权归三星所有,这里仅仅是技术实现.

先上效果图

途径动画

贝塞尔曲线的另一个丰盛常用的功用,就是作为动画的移动轨迹,让动画片目标可以沿曲线平滑的贯彻活动动画,也就是让实体沿着贝塞尔曲线运动,而不是教条主义的直线,本例实现效益如下所示:

7.gif

package com.xys.animationart.views;

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;

import com.xys.animationart.evaluator.BezierEvaluator;

/**
 * 贝塞尔路径动画
 * <p/>
 * Created by xuyisheng on 16/7/12.
 */
public class PathBezier extends View implements View.OnClickListener {

    private Paint mPathPaint;
    private Paint mCirclePaint;

    private int mStartPointX;
    private int mStartPointY;
    private int mEndPointX;
    private int mEndPointY;

    private int mMovePointX;
    private int mMovePointY;

    private int mControlPointX;
    private int mControlPointY;

    private Path mPath;

    public PathBezier(Context context) {
        super(context);
    }

    public PathBezier(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPathPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPathPaint.setStyle(Paint.Style.STROKE);
        mPathPaint.setStrokeWidth(5);
        mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        mStartPointX = 100;
        mStartPointY = 100;
        mEndPointX = 600;
        mEndPointY = 600;
        mMovePointX = mStartPointX;
        mMovePointY = mStartPointY;
        mControlPointX = 500;
        mControlPointY = 0;
        mPath = new Path();
        setOnClickListener(this);
    }

    public PathBezier(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        canvas.drawCircle(mStartPointX, mStartPointY, 30, mCirclePaint);
        canvas.drawCircle(mEndPointX, mEndPointY, 30, mCirclePaint);
        mPath.moveTo(mStartPointX, mStartPointY);
        mPath.quadTo(mControlPointX, mControlPointY, mEndPointX, mEndPointY);
        canvas.drawPath(mPath, mPathPaint);
        canvas.drawCircle(mMovePointX, mMovePointY, 30, mCirclePaint);
    }

    @Override
    public void onClick(View view) {
        BezierEvaluator bezierEvaluator = new BezierEvaluator(new PointF(mControlPointX, mControlPointY));
        ValueAnimator anim = ValueAnimator.ofObject(bezierEvaluator,
                new PointF(mStartPointX, mStartPointY),
                new PointF(mEndPointX, mEndPointY));
        anim.setDuration(600);
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                PointF point = (PointF) valueAnimator.getAnimatedValue();
                mMovePointX = (int) point.x;
                mMovePointY = (int) point.y;
                invalidate();
            }
        });
        anim.setInterpolator(new AccelerateDecelerateInterpolator());
        anim.start();
    }
}

里面,用于转移运动点坐标的重中之重evaluator如下所示:

package com.xys.animationart.evaluator;

import android.animation.TypeEvaluator;
import android.graphics.PointF;

import com.xys.animationart.util.BezierUtil;

public class BezierEvaluator implements TypeEvaluator<PointF> {

    private PointF mControlPoint;

    public BezierEvaluator(PointF controlPoint) {
        this.mControlPoint = controlPoint;
    }

    @Override
    public PointF evaluate(float t, PointF startValue, PointF endValue) {
        return BezierUtil.CalculateBezierPointForQuadratic(t, startValue, mControlPoint, endValue);
    }
}

此处的Type伊娃(Eva)luator统计用到了统计贝塞尔曲线上点的盘算算法,这一个会在前面继续助教。

代表电量的液体效果

整套液体分两有些,下面是波浪,下边是矩形.进度值实际决定的是矩形的高度.六个控件放到StackPanel中,让下边的矩形往上顶.最终给波浪底部Margin值为-1,使其看起来没有间隙.

图片 12

波浪是用贝塞尔曲线实现的,首先介绍下贝塞尔曲线

图片 13

贝塞尔曲线有4个点,起点终点和三个控制点.(此括号里的可以不看:上画画的并不标准,因为控制点并不一定在曲线上).通过六个控制点决定曲线的路径.

明确上图这我就是个波浪形.使用点动画PointAnimation控制多少个点左右移动就有了波浪的动态效果.注意三个卡通时间不要一样,否则看起来动画太假.六个时间错开一点点就好了.

图片 14

波浪部分宽度是50,低度是5

图片 15

电池背景

因为电池内部有好几个部分,所以本例用了一个Grid来做背景,用Clip属性剪切出一个电池的概貌,这样不但呈现出一个电池的大概,还是可以够制止水波和气泡跑呈现Grid的外面.

Clip的中间,是一个Path形状.具体画法就不多说了,以前写过.有趣味的同室看这里:http://www.cnblogs.com/tsliwei/p/5609035.html

图片 16

圆的拟合

贝塞尔曲线做动画,很多时候都需要采用到圆的特效,而经过二阶、三阶贝塞尔曲线来拟合圆,也不是一个卓殊简单的政工,所以,我从来把结论拿出去了,具体的算法地址如下所示:

http://spencermortensen.com/articles/bezier-circle/

http://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves

14.png

15.png

有了贝塞尔曲线的控制点,再对其落实动画,就分外简单了,与事先的动画片没有太大的区别。

画气泡

为了雅观,我自己画了个气泡的模子,用在了大气泡上.小气泡直接用的椭圆,因为即采纳模型,因为太小,也看不出来.实际上大气泡也稍微看得出来.然而既然写了,依然介绍下吧.

图片 17

先是这些气泡就是个ViewBox.方便缩放.

大概是个正圆,Fill给了个渐变画刷,向外不断深化,在最外面0.85-1的有些是最深的.六个点的R都是20,B都是10,青色部分G依次减小,分别是240,150,100.

图片 18

右下面的月牙是个Path,给了个半径是10的混淆效果.Fill是半晶莹剔透的白色.月牙的画法就是两个弧线,起源和终极相同,半径不同.

图片 19

左上角的优点就是多少个椭圆,和月牙一样.半径是10的歪曲效果.Fill是半晶莹剔透的白色.

 

2016-12-19更新:

发布到GitHub,地址:https://github.com/ptddqr/wpf-samsung-phone-s5-charging-ui/tree/master

源码下载: 三星手机电池充电效果.rar

气泡效果

此地的气泡效果就是个典型的粒子效果,而且是最简易的这种,并不涉及到哪些复杂的公式总括.

简言之介绍下原理:这里的气泡可以作为是圆遵照一定的进度持续的提高(改变Y轴坐标).所以规定一个速率,规定一个离开,使用帧动画CompositionTarget.Rendering,在每一帧都在Y轴上加这些速率在一帧移动的距离.然后判断又没达到规定的距离.假若达到,移除这一个圈子,否则继续上升.

气泡可以分成六个部分:

1.电池内部的气泡.大小适当,移动速度最慢,移动距离最短.

2.屏幕底边的大气泡,个头相比较大,移动速度较慢,移动距离较短.

3.屏幕底边的小气泡,个头不大,移动速度较快,移动距离较远.

新建一个Class,用来表示气泡音信

图片 20

中间五个重点性质,一个是速率,一个是气泡需要活动的距离.这多少个特性决定了血泡的运动轨迹.第多少个属性是用来判定气泡是不是水到渠成了重任,第五个特性是加上一个对气泡的引用,那样方便在后台控制气泡.

概念多少个会聚,用来存放三局部的血泡音信.

在帧渲染事件内,遍历五个集合.让集合里的各类气泡都向上移动(Canvas.SetTop),判断气泡是不是曾经移动了点名的离开,是的话就在页面移除气泡,集合也移除该气泡新闻.判断集合的Count是不是低于规定个个数,假若低于,就向页面添加气泡,集合添加气泡音信.

矩形拟合

俺们来看一下拟合的法则,实际上就是经过贝塞尔曲线来连接四个圆上的两个点,当我们调整下画笔的填充形式,并绘制一些襄助线,我们来看现实是如何举办拟合的,如图所示:

9.png

可以窥见,控制点为两圆圆心连线的中央,连接线为图中的这样一个矩形,当圆相比时辰,这种经过矩形来拟合的办法几乎是未曾问题的,但我们把圆放大,再来看下这种拟合,如图所示:

10.png

当圆的半径扩张之后,就足以相当引人注目标发现拟合的连接点与圆有必然相交的区域,这样的拟合效果就欠好了,大家将画笔格局调整回来,如图所示:

11.png

故而,简单的矩形拟合,在圆半径小的时候,是足以的,但当圆半径变大之后,就需要更进一步严刻的拟合了。

这边大家先来上课下,如何总计矩形拟合的几个关键点。

此前方那张线图可以见到,标红的多少个角是相等的,而以此角能够由此六个圆心的坐标来算出,有了这样一个角度,通过R
x cos和 R x
sin来测算矩形的一个终极的坐标,类似的,其余坐标可求,关键代码如下所示:

private void metaBallVersion1(Canvas canvas) {
        float x = mCircleTwoX;
        float y = mCircleTwoY;
        float startX = mCircleOneX;
        float startY = mCircleOneY;

        float dx = x - startX;
        float dy = y - startY;
        double a = Math.atan(dx / dy);
        float offsetX = (float) (mCircleOneRadius * Math.cos(a));
        float offsetY = (float) (mCircleOneRadius * Math.sin(a));

        float x1 = startX + offsetX;
        float y1 = startY - offsetY;

        float x2 = x + offsetX;
        float y2 = y - offsetY;

        float x3 = x - offsetX;
        float y3 = y + offsetY;

        float x4 = startX - offsetX;
        float y4 = startY + offsetY;

        float controlX = (startX + x) / 2;
        float controlY = (startY + y) / 2;

        mPath.reset();
        mPath.moveTo(x1, y1);
        mPath.quadTo(controlX, controlY, x2, y2);
        mPath.lineTo(x3, y3);
        mPath.quadTo(controlX, controlY, x4, y4);
        mPath.lineTo(x1, y1);

        // 辅助线
        canvas.drawLine(mCircleOneX, mCircleOneY, mCircleTwoX, mCircleTwoY, mPaint);
        canvas.drawLine(0, mCircleOneY, mCircleOneX + mRadiusNormal + 400, mCircleOneY, mPaint);
        canvas.drawLine(mCircleOneX, 0, mCircleOneX, mCircleOneY + mRadiusNormal + 50, mPaint);
        canvas.drawLine(x1, y1, x2, y2, mPaint);
        canvas.drawLine(x3, y3, x4, y4, mPaint);
        canvas.drawCircle(controlX, controlY, 5, mPaint);
        canvas.drawLine(mCircleTwoX, mCircleTwoY, mCircleTwoX, 0, mPaint);
        canvas.drawLine(x1, y1, x1, mCircleOneY, mPaint);

        canvas.drawPath(mPath, mPaint);
    }

贝塞尔曲线拟合总计

贝塞尔曲线有一个非凡常用的卡通效果——MetaBall算法。相信广大开发者都见过类似的动画,例如QQ的小红点消除,UC浏览器的下拉刷新loading等等。要做好这一个动画,实际上最重要的就是经过贝塞尔曲线来拟合多少个图形。

效用如图所示:

8.png

贝塞尔曲线开发的办法

一句话概括贝塞尔曲线:将轻易一条曲线转化为规范的数学公式。

不少绘制工具中的钢笔工具,就是第一级的贝塞尔曲线的利用,这里的一个网站可以在线模拟钢笔工具的施用:

http://bezier.method.ac/

2.png

贝塞尔曲线中有局部相比紧要的名词,解释如下:

  • 数据点:平常指一条途径的开端点和终止点
  • 控制点:控制点决定了一条途径的曲折轨迹,按照控制点的个数,贝塞尔曲线被分为一阶贝塞尔曲线(0个控制点)、二阶贝塞尔曲线(1个控制点)、三阶贝塞尔曲线(2个控制点)等等。

要想对贝塞尔曲线有一个相比好的认识,可以参见WIKI上的链接:

https://en.wikipedia.org/wiki/B%C3%A9zier\_curve

1.png

曲线变形

经过决定贝塞尔曲线的控制点,就可以实现对一条路线的改动。所以,利用贝塞尔曲线,可以兑现广大的途径动画,例如:

5.gif

package com.xys.animationart;

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.BounceInterpolator;

/**
 * 曲线变形
 * <p/>
 * Created by xuyisheng on 16/7/11.
 */
public class PathMorphBezier extends View implements View.OnClickListener{

    private Paint mPaintBezier;
    private Paint mPaintAuxiliary;
    private Paint mPaintAuxiliaryText;

    private float mAuxiliaryOneX;
    private float mAuxiliaryOneY;
    private float mAuxiliaryTwoX;
    private float mAuxiliaryTwoY;

    private float mStartPointX;
    private float mStartPointY;

    private float mEndPointX;
    private float mEndPointY;

    private Path mPath = new Path();
    private ValueAnimator mAnimator;

    public PathMorphBezier(Context context) {
        super(context);
    }

    public PathMorphBezier(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaintBezier = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintBezier.setStyle(Paint.Style.STROKE);
        mPaintBezier.setStrokeWidth(8);

        mPaintAuxiliary = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintAuxiliary.setStyle(Paint.Style.STROKE);
        mPaintAuxiliary.setStrokeWidth(2);

        mPaintAuxiliaryText = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintAuxiliaryText.setStyle(Paint.Style.STROKE);
        mPaintAuxiliaryText.setTextSize(20);
        setOnClickListener(this);
    }

    public PathMorphBezier(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        mStartPointX = w / 4;
        mStartPointY = h / 2 - 200;

        mEndPointX = w / 4 * 3;
        mEndPointY = h / 2 - 200;

        mAuxiliaryOneX = mStartPointX;
        mAuxiliaryOneY = mStartPointY;
        mAuxiliaryTwoX = mEndPointX;
        mAuxiliaryTwoY = mEndPointY;

        mAnimator = ValueAnimator.ofFloat(mStartPointY, (float) h);
        mAnimator.setInterpolator(new BounceInterpolator());
        mAnimator.setDuration(1000);
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mAuxiliaryOneY = (float) valueAnimator.getAnimatedValue();
                mAuxiliaryTwoY = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        mPath.moveTo(mStartPointX, mStartPointY);
        // 辅助点
        canvas.drawPoint(mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliary);
        canvas.drawText("辅助点1", mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliaryText);
        canvas.drawText("辅助点2", mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliaryText);
        canvas.drawText("起始点", mStartPointX, mStartPointY, mPaintAuxiliaryText);
        canvas.drawText("终止点", mEndPointX, mEndPointY, mPaintAuxiliaryText);
        // 辅助线
        canvas.drawLine(mStartPointX, mStartPointY, mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliary);
        canvas.drawLine(mEndPointX, mEndPointY, mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliary);
        canvas.drawLine(mAuxiliaryOneX, mAuxiliaryOneY, mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliary);
        // 三阶贝塞尔曲线
        mPath.cubicTo(mAuxiliaryOneX, mAuxiliaryOneY, mAuxiliaryTwoX, mAuxiliaryTwoY, mEndPointX, mEndPointY);
        canvas.drawPath(mPath, mPaintBezier);
    }

    @Override
    public void onClick(View view) {
        mAnimator.start();
    }
}

那边就是简单的变动二阶贝塞尔曲线的控制点来贯彻曲线的变形。

网上一些相比复杂的变形动画效果,也是依照这种实现模式,其规律都是通过变更控制点的地点,从而达成对图纸的转换,例如圆形到心形的浮动、圆形到五角星的转移,等等。

模仿网页

如下所示的网页,模拟了三阶贝塞尔曲线的绘图,可以通过拖动曲线来得到两个控制点的坐标,而开始点分别是(0,0)和(1,1)。

http://cubic-bezier.com/

16.png

通过这么些网页,也可以比较有利的收获三阶贝塞尔曲线的决定点坐标。

贝塞尔曲线进阶

二阶模拟

二阶贝塞尔曲线在Android中的API为:quadTo()和rQuadTo(),这多少个API在常理上是可以相互转换的——quadTo是基于相对坐标,而rQuadTo是按照相对坐标,所未来面我都只以中间一个来举办教学。

先来看下最后的效用:

3.gif

在此以前面的介绍可以领悟,二阶贝塞尔曲线有几个数据点和一个控制点,只需要在代码中绘制出这一个帮助点和襄助线即可,同时,控制点可以由此onTouch伊芙nt来展开传递。

package com.xys.animationart.views;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

/**
 * 二阶贝塞尔曲线
 * <p/>
 * Created by xuyisheng on 16/7/11.
 */
public class SecondOrderBezier extends View {

    private Paint mPaintBezier;
    private Paint mPaintAuxiliary;
    private Paint mPaintAuxiliaryText;

    private float mAuxiliaryX;
    private float mAuxiliaryY;

    private float mStartPointX;
    private float mStartPointY;

    private float mEndPointX;
    private float mEndPointY;

    private Path mPath = new Path();

    public SecondOrderBezier(Context context) {
        super(context);
    }

    public SecondOrderBezier(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaintBezier = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintBezier.setStyle(Paint.Style.STROKE);
        mPaintBezier.setStrokeWidth(8);

        mPaintAuxiliary = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintAuxiliary.setStyle(Paint.Style.STROKE);
        mPaintAuxiliary.setStrokeWidth(2);

        mPaintAuxiliaryText = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintAuxiliaryText.setStyle(Paint.Style.STROKE);
        mPaintAuxiliaryText.setTextSize(20);
    }

    public SecondOrderBezier(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mStartPointX = w / 4;
        mStartPointY = h / 2 - 200;

        mEndPointX = w / 4 * 3;
        mEndPointY = h / 2 - 200;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        mPath.moveTo(mStartPointX, mStartPointY);
        // 辅助点
        canvas.drawPoint(mAuxiliaryX, mAuxiliaryY, mPaintAuxiliary);
        canvas.drawText("控制点", mAuxiliaryX, mAuxiliaryY, mPaintAuxiliaryText);
        canvas.drawText("起始点", mStartPointX, mStartPointY, mPaintAuxiliaryText);
        canvas.drawText("终止点", mEndPointX, mEndPointY, mPaintAuxiliaryText);
        // 辅助线
        canvas.drawLine(mStartPointX, mStartPointY, mAuxiliaryX, mAuxiliaryY, mPaintAuxiliary);
        canvas.drawLine(mEndPointX, mEndPointY, mAuxiliaryX, mAuxiliaryY, mPaintAuxiliary);
        // 二阶贝塞尔曲线
        mPath.quadTo(mAuxiliaryX, mAuxiliaryY, mEndPointX, mEndPointY);
        canvas.drawPath(mPath, mPaintBezier);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                mAuxiliaryX = event.getX();
                mAuxiliaryY = event.getY();
                invalidate();
        }
        return true;
    }
}

贝塞尔曲线应用

求贝塞尔曲线上随意一点的坐标

求贝塞尔曲线上自由一点的坐标,这一进程,就是采纳了De Casteljau算法。

http://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/de-casteljau.html

7.png

使用这一算法,有开发者开发了一个演示多阶贝塞尔曲线的功能的App,其规律就是通过绘制贝塞尔曲线上的点来举办绘图的,地址如下所示:

https://github.com/venshine/BezierMaker

下边这篇著作就详细的上书了该算法的采用,我的代码也从这里提取而来:

http://devmag.org.za/2011/04/05/bzier-curves-a-tutorial/

切线拟合

如前方所说,矩形拟合在半径较小的气象下,是可以实现宏观拟合的,而当半径变大后,就会油不过生贝塞尔曲线与圆相交的情状,导致拟合战败。

那么如何来贯彻宏观的拟合呢?实际上,也就是说贝塞尔曲线与圆的连接点到贝塞尔曲线的控制点的连线,一定是圆的切线,这样的话,无论圆的半径怎么着转移,贝塞尔曲线一定是与圆拟合的,具体职能如图所示:

12.png

此时我们把画笔形式调整回来看下填充效果,如图所示:

13.png

这么拟合是非凡系数的。那么要如何来测算这个拟合的关键点呢?在前方的线图中,我标记出了五个角,这五个角分别可以求出,相减,就足以博得切点与圆心的夹角了,这样,通过R
x cos和R x sin就足以求出切点的坐标了。

里头,小的角可以经过六个圆心的坐标来求出,而大的角,可以透过直角三角形(圆心、切点、控制点)来求出,即控制点到圆心的相距/半径。

首要代码如下所示:

private void metaBallVersion2(Canvas canvas) {
        float x = mCircleTwoX;
        float y = mCircleTwoY;
        float startX = mCircleOneX;
        float startY = mCircleOneY;
        float controlX = (startX + x) / 2;
        float controlY = (startY + y) / 2;

        float distance = (float) Math.sqrt((controlX - startX) * (controlX - startX) + (controlY - startY) * (controlY - startY));
        double a = Math.acos(mRadiusNormal / distance);

        double b = Math.acos((controlX - startX) / distance);
        float offsetX1 = (float) (mRadiusNormal * Math.cos(a - b));
        float offsetY1 = (float) (mRadiusNormal * Math.sin(a - b));
        float tanX1 = startX + offsetX1;
        float tanY1 = startY - offsetY1;

        double c = Math.acos((controlY - startY) / distance);
        float offsetX2 = (float) (mRadiusNormal * Math.sin(a - c));
        float offsetY2 = (float) (mRadiusNormal * Math.cos(a - c));
        float tanX2 = startX - offsetX2;
        float tanY2 = startY + offsetY2;

        double d = Math.acos((y - controlY) / distance);
        float offsetX3 = (float) (mRadiusNormal * Math.sin(a - d));
        float offsetY3 = (float) (mRadiusNormal * Math.cos(a - d));
        float tanX3 = x + offsetX3;
        float tanY3 = y - offsetY3;

        double e = Math.acos((x - controlX) / distance);
        float offsetX4 = (float) (mRadiusNormal * Math.cos(a - e));
        float offsetY4 = (float) (mRadiusNormal * Math.sin(a - e));
        float tanX4 = x - offsetX4;
        float tanY4 = y + offsetY4;

        mPath.reset();
        mPath.moveTo(tanX1, tanY1);
        mPath.quadTo(controlX, controlY, tanX3, tanY3);
        mPath.lineTo(tanX4, tanY4);
        mPath.quadTo(controlX, controlY, tanX2, tanY2);
        canvas.drawPath(mPath, mPaint);

        // 辅助线
        canvas.drawCircle(tanX1, tanY1, 5, mPaint);
        canvas.drawCircle(tanX2, tanY2, 5, mPaint);
        canvas.drawCircle(tanX3, tanY3, 5, mPaint);
        canvas.drawCircle(tanX4, tanY4, 5, mPaint);
        canvas.drawLine(mCircleOneX, mCircleOneY, mCircleTwoX, mCircleTwoY, mPaint);
        canvas.drawLine(0, mCircleOneY, mCircleOneX + mRadiusNormal + 400, mCircleOneY, mPaint);
        canvas.drawLine(mCircleOneX, 0, mCircleOneX, mCircleOneY + mRadiusNormal + 50, mPaint);
        canvas.drawLine(mCircleTwoX, mCircleTwoY, mCircleTwoX, 0, mPaint);
        canvas.drawCircle(controlX, controlY, 5, mPaint);
        canvas.drawLine(startX, startY, tanX1, tanY1, mPaint);
        canvas.drawLine(tanX1, tanY1, controlX, controlY, mPaint);
    }

波浪效果

波浪的绘图是贝塞尔曲线一个相当简单的利用,而让波浪举行波动,其实并不需要对控制点进行更改,而是可以通过位移来实现,这里大家是倚重贝塞尔曲线来实现波浪的绘图效果,效果如图所示:

6.gif

package com.xys.animationart.views;

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.LinearInterpolator;

/**
 * 波浪图形
 * <p/>
 * Created by xuyisheng on 16/7/11.
 */
public class WaveBezier extends View implements View.OnClickListener {

    private Paint mPaint;
    private Path mPath;
    private int mWaveLength = 1000;
    private int mOffset;
    private int mScreenHeight;
    private int mScreenWidth;
    private int mWaveCount;
    private int mCenterY;

    public WaveBezier(Context context) {
        super(context);
    }

    public WaveBezier(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public WaveBezier(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPath = new Path();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.LTGRAY);
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        setOnClickListener(this);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mScreenHeight = h;
        mScreenWidth = w;
        mWaveCount = (int) Math.round(mScreenWidth / mWaveLength + 1.5);
        mCenterY = mScreenHeight / 2;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        mPath.moveTo(-mWaveLength + mOffset, mCenterY);
        for (int i = 0; i < mWaveCount; i++) {
            // + (i * mWaveLength)
            // + mOffset
            mPath.quadTo((-mWaveLength * 3 / 4) + (i * mWaveLength) + mOffset, mCenterY + 60, (-mWaveLength / 2) + (i * mWaveLength) + mOffset, mCenterY);
            mPath.quadTo((-mWaveLength / 4) + (i * mWaveLength) + mOffset, mCenterY - 60, i * mWaveLength + mOffset, mCenterY);
        }
        mPath.lineTo(mScreenWidth, mScreenHeight);
        mPath.lineTo(0, mScreenHeight);
        mPath.close();
        canvas.drawPath(mPath, mPaint);
    }

    @Override
    public void onClick(View view) {
        ValueAnimator animator = ValueAnimator.ofInt(0, mWaveLength);
        animator.setDuration(1000);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mOffset = (int) animation.getAnimatedValue();
                postInvalidate();
            }
        });
        animator.start();
    }
}

波浪动画实际上并不复杂,但三角函数确实对部分开发者比较坚苦,开发者可以因而下边的这多少个网站来效仿三角函数图像的绘图:

https://www.desmos.com/calculator

17.png

相关文章