召唤,光能使者--玩转PathMeasure

这次不是十分钟动画,今天要分享的是PathMeasure的玩法。

首先我们来回顾一下童年吧~~90后满满的记忆

光能使者

小时候总是幻想着自己能变身,今天我们就来用代码实现变身的第一步吧,动画绘制一个魔法阵magic_circle~~

magic_circle.jpg

静态图片比较容易,我们用 Path 设置好路径,然后再 canvas.drawPath 即可,但是静态的也太 low 了一点,我们要让它动起来。

实现效果:

magic_circle1.gif

效果看完了,不会写的童鞋肯定已经懵逼了,会的童鞋可以出门左拐了,因为实现实在太简单。

好了,在开始撸代码之前,我们先来学习一个类 PathMeasure。我们的光能使者阵就是是基于这个类的两个方法撸出来的。

PathMeasure

这个类的 class 注释就一个版权说明,酱紫~

Copyright.png

踏马的,Google 工程师都偷懒了,注释都不写。。。幸好这个类只有一百多行代码,那我们就自己看吧

Public constructors

  • PathMeasure 创建一个空的 pathmeasure 对象
  • PathMeasure(Path path,boolean forceClosed)创建一个带 path 参数的 PathMeasure,forceClosed控制 path 是否自动闭合

Public methods

  • getLength() 返回当前 Path 的总长度。
  • getMatrix(float distance, Matrix matrix, int flags)
  • getPosTan(float distance, float[] pos, float[] tan)获取distance长度的 point 值给 pos,point 点的正切值给 tan。
  • getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) 获取 path 的一个片段,即startD到 stopD 的线段,辅助给 dst。
  • isClosed() 是否自动闭合
  • nextContour() 移动到下一条曲线。如果 path 中含有不连续的线条,getLength、getPosTan等方法之会在第一条线上运行,需要使用这个方法跳到第二条线
  • setPath(Path path, boolean forceClosed)

是不是很简单,就这么几个方法,现在去画光能使者阵有思路了么~~
接下来为了便于大家理解,我们再来简单回顾一下 path 的 api,因为静态的光能使者阵是需要 path 去绘制的。

Path

方法名 作用
moveTo 移动到指定点
setLastPoint 重新设置当前 Path 的最后一个点,如果当前 Path 为空,则等同上个方法
lineTo 添加当前点到一个指定点的直线
close 连接当前点到起点,形成闭合路径
addRect、addRoundRect、addOval、addCircle、addPath、addArc、arcTo 添加各种图形
isEmpty 是否为空
isRect 是否为矩形
set 用一个新的 path 替换
offset 对当前的路径进行偏移,不会影响后续操作
quadTo、cubicTo 贝塞尔曲线
rMoveTo、rLineTo、rQuaTo、rCubicTo 带 r 的是基于当前点的偏移量,不带 r 基于坐标原点
setFillType、getFillType 设置填充模式
transform 矩阵变换

就这样简单回顾一下吧,具体玩法可以参考 Hencoder 的 bolg【HenCoder Android 开发进阶: 自定义 View 1-1 绘制基础】

动画拆解

好了,准备工作完成,我们开始撸代码

第一步,绘制静态的光能使者阵

首先绘制两个圆,然后就是中间的六角星(其实仔细看就是两个三角形)。
都是很简单的方法,同学们动手去画的时候可能会遇到一个这样的问题,就是三角形的三个点不好取。其实很简单,直接在圆上取0,1/3,2/3长度的点即可,刚刚我们不是说了 PathMeasure 的方法么,用getPosTan就可以实现。

第二步,让光能使者阵动起来

这里我们把这个动画效果分成三个阶段吧。

  • 第一阶段,绘制两个圆
step_1.gif

如上图所示,这里两个圆是慢慢绘制出来的, 圆的 path 很容易绘制出来,这里我就不讲了,然后PathMeasure的getLength可以获取 path 总的长度,getSegment可以获取某个点到某个点的 Path。因此一个 ValueAnimator 就可以解决从0到100%长度的过程,具体实现看后面的代码。
然后问题来了,path画出来的圆的起点在哪里?怎么控制两个圆开始绘制的角度不一样。有同学可能想到了旋转90°再画第二个圆,当然这种方式是可以实现的,但是由于后面的三角形也需要旋转,这里我们就不用 path 画圆了,用 path 添加一个正方形 Rect 的圆弧也是一个圆,然后我们的圆弧可以控制开始的角度,弧度。
然后变成这样了

erroe_1.gif

WTF?角度怎么不对了,我明明设置的开始角度的呀

innerCircle.addArc(innerRect, 150, -360);
outerCircle.addArc(outerRect, 60, -360);

最后有个大牛说你的圆变成闭环了,PathMeasure 找不到开始点,用了默认的。你把360度改成359.9让他不是一个闭环的圆就行了。

  • 第二阶段,两个点在圆里面弹射
step_2.gif

看起来好像还要干什么碰撞反弹之类的事,一副高科技的样子,其实不是的。

轨迹就是两个三角形,怎么让两条线跟着三角形走呢,而且走的时候还要不段变化长度。

刚刚第一步我们用 ValueAnimator 来控制一个圆从0到100%的过程,

pathMeasure.getSegment(0, distance * pathMeasure.getLength(), drawPath, true);

不断截取起点到*%长度的 path 赋值给drawPath。

从里是从起点开始截取,那么我们不从起点开始截取,从当前点附近开始截取不就行了吗,哈哈哈哈~so easy

float stopD = distance * pathMeasure.getLength();
float startD = stopD - (0.5f - Math.abs(0.5f - distance)) * 200;
pathMeasure.getSegment(startD, stopD, drawPath, true);
酱紫~~

  • 第三阶段绘制两个三角形
step_3.gif

其实两个三角形就是第二步的运动轨迹,也是就是说直接用第阶段的 Path 即可,然后再用第一阶段一样的办法就可以实现效果。

代码实现

public class GranzortView extends View {

private Paint paint;

private Path innerCircle;//内圆 path
private Path outerCircle;//外圆 path
private Path trangle1;//第一个三角形的 Path
private Path trangle2;//第二个三角形的 Path
private Path drawPath;//用于截取路径的 Path

private PathMeasure pathMeasure;

private float mViewWidth;
private float mViewHeight;

private long duration = 3000;
private ValueAnimator valueAnimator;

private Handler mHanlder;

private float distance;//当前动画执行的百分比取值为0-1
private ValueAnimator.AnimatorUpdateListener animatorUpdateListener;
private Animator.AnimatorListener animatorListener;

private State mCurrentState = State.CIRCLE_STATE;

//三个阶段的枚举
private enum State {
    CIRCLE_STATE,
    TRANGLE_STATE,
    FINISH_STATE
}

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

public GranzortView(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
}

public GranzortView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
}

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

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawColor(getResources().getColor(R.color.colorPrimary));
    canvas.save();
    canvas.translate(mViewWidth / 2, mViewHeight / 2);
    switch (mCurrentState) {
        case CIRCLE_STATE:
            drawPath.reset();
            pathMeasure.setPath(innerCircle, false);
            pathMeasure.getSegment(0, distance * pathMeasure.getLength(), drawPath, true);
            canvas.drawPath(drawPath, paint);
            pathMeasure.setPath(outerCircle, false);
            drawPath.reset();
            pathMeasure.getSegment(0, distance * pathMeasure.getLength(), drawPath, true);
            canvas.drawPath(drawPath, paint);
            break;
        case TRANGLE_STATE:
            canvas.drawPath(innerCircle, paint);
            canvas.drawPath(outerCircle, paint);
            drawPath.reset();
            pathMeasure.setPath(trangle1, false);
            float stopD = distance * pathMeasure.getLength();
            float startD = stopD - (0.5f - Math.abs(0.5f - distance)) * 200;
            pathMeasure.getSegment(startD, stopD, drawPath, true);
            canvas.drawPath(drawPath, paint);
            drawPath.reset();
            pathMeasure.setPath(trangle2, false);
            pathMeasure.getSegment(startD, stopD, drawPath, true);
            canvas.drawPath(drawPath, paint);
            break;
        case FINISH_STATE:
            canvas.drawPath(innerCircle, paint);
            canvas.drawPath(outerCircle, paint);
            drawPath.reset();
            pathMeasure.setPath(trangle1, false);
            pathMeasure.getSegment(0, distance * pathMeasure.getLength(), drawPath, true);
            canvas.drawPath(drawPath, paint);
            drawPath.reset();
            pathMeasure.setPath(trangle2, false);
            pathMeasure.getSegment(0, distance * pathMeasure.getLength(), drawPath, true);
            canvas.drawPath(drawPath, paint);
            break;

    }

    canvas.restore();

}

private void init() {

    initPaint();

    initPath();

    initHandler();

    initAnimatorListener();

    initAnimator();

    mCurrentState = State.CIRCLE_STATE;
    valueAnimator.start();

}

private void initHandler() {
    mHanlder = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (mCurrentState) {
                case CIRCLE_STATE:
                    mCurrentState = State.TRANGLE_STATE;
                    valueAnimator.start();
                    break;
                case TRANGLE_STATE:
                    mCurrentState = State.FINISH_STATE;
                    valueAnimator.start();
                    break;
            }
        }
    };
}

private void initAnimatorListener() {
    animatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            distance = (float) animation.getAnimatedValue();
            invalidate();
        }
    };

    animatorListener = new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {
            Log.e("star:",mCurrentState+"_");
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            Log.e("end:",mCurrentState+"_");
            mHanlder.sendEmptyMessage(0);
        }

        @Override
        public void onAnimationCancel(Animator animation) {

        }

        @Override
        public void onAnimationRepeat(Animator animation) {

        }
    };
}

private void initAnimator() {
    valueAnimator = ValueAnimator.ofFloat(0, 1).setDuration(duration);

    valueAnimator.addUpdateListener(animatorUpdateListener);

    valueAnimator.addListener(animatorListener);
}

private void initPath() {
    innerCircle = new Path();
    outerCircle = new Path();
    trangle1 = new Path();
    trangle2 = new Path();
    drawPath = new Path();

    pathMeasure = new PathMeasure();

    RectF innerRect = new RectF(-220, -220, 220, 220);
    RectF outerRect = new RectF(-280, -280, 280, 280);
    innerCircle.addArc(innerRect, 150, -359.9F);     // 不能取360f,否则可能造成测量到的值不准确
    outerCircle.addArc(outerRect, 60, -359.9F);

    pathMeasure.setPath(innerCircle, false);

    float[] pos = new float[2];
    pathMeasure.getPosTan(0, pos, null);        // 获取开始位置的坐标
    trangle1.moveTo(pos[0], pos[1]);
    pathMeasure.getPosTan((1f / 3f) * pathMeasure.getLength(), pos, null);
    System.out.println("pos : " + pos[0] + "  " + pos[1]);

    trangle1.lineTo(pos[0], pos[1]);
    pathMeasure.getPosTan((2f / 3f) * pathMeasure.getLength(), pos, null);
    trangle1.lineTo(pos[0], pos[1]);
    trangle1.close();

    pathMeasure.getPosTan((2f / 3f) * pathMeasure.getLength(), pos, null);
    Matrix matrix = new Matrix();
    matrix.postRotate(-180);
    trangle1.transform(matrix, trangle2);
}

private void initPaint() {
    paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setColor(Color.WHITE);
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeWidth(10);
    paint.setStrokeCap(Paint.Cap.ROUND);
    paint.setStrokeJoin(Paint.Join.BEVEL);
    paint.setShadowLayer(15, 0, 0, Color.WHITE);//白色光影效果
}}

好,光能使者阵完成了,离变身成为光能使者只差喊口号和变身动作了,加油吧,有梦想的程序员。
最后,感觉小伙伴Moo_Night分享的光能使者阵。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容