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

作者: Anonymous___ | 来源:发表于2017-08-23 16:18 被阅读4485次

    这次不是十分钟动画,今天要分享的是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分享的光能使者阵。

    相关文章

      网友评论

      • AWeiLoveAndroid:给博主点赞,求关注
      • SheepYang:暴露年纪了
        Anonymous___:@SheepYang 今年18岁的我,怕什么暴露年纪
      • 疾风紫狼:这样就很容易实现那种路径导航的动态效果了,比如选取开始位置,结束位置,然后动态给出一个小人移动的效果。
        Anonymous___:@疾风紫狼 哈哈,快动手写个 demo
      • 我的小侯子:学习了
        不过在19及以下pathMeasure.getSegment会有问题
        需要在getSegment之前加个rLineTo(0,0);

        drawPath.reset();
        drawPath.rLineTo(0,0);
        pathMeasure.getSegment();

        <p>On {@link android.os.Build.VERSION_CODES#KITKAT} and earlier
        * releases, the resulting path may not display on a hardware-accelerated
        * Canvas. A simple workaround is to add a single operation to this path,
        * such as <code>dst.rLineTo(0, 0)</code>.</p>
        Anonymous___:@我的小侯子 好的,谢谢哥指点,回头我试一下。
      • GordenNee:厉害了
      • 5906055820d4:6666,厉害了
      • e658f399d4a1:下哈拉多库西多😏
      • 225f963f19a7:好帅啊,能召唤出光能使者就厉害了
      • d0373c94e1af:只有我一个人注意到Demo的名字嘛
        Anonymous___:@格格不入嘿 :cry:
      • Benhero:又酷炫又中二,哈哈哈哈哈!!!
        好好笑!
        效果又做得好棒棒!为你点赞!:+1:
      • idioitcbear:更新到下一个阶段为什么需要使用到Handler,我试着直接在end里面写switch语句开启下一个阶段的动画又不执行,大佬求教!!
        idioitcbear:@Anonymous___ 好嘞,谢谢楼主
        Anonymous___:@想你0开心 哈哈,这个你得去看ValueAnimator源码的mStared的值。
      • 紫豪:不关注你我都不好意思
        Anonymous___:@紫豪 哈哈,这是我听到最动听的告白
      • b29d2d093017:666
        Anonymous___:@毛衡 999
      • 扎克Zach:66666, 我的童年, 我能说 中, 日, 韩三文版我小时候都全看了一遍么..(捂脸)
        Anonymous___:@黑暗料理菌 勾起了你内心深处的回忆:wink:
        扎克Zach:@Anonymous___ 当时我记得央视还是辽宁卫视放中文配音版, 家里电视台放日语配音版, 然后小学同学家, 因为是朝鲜族家, 所以有从韩国带回来的韩语版录像带.....比较下来, 中文版 OP 最带感, 韩文版 OP 相当羞耻.....是一群小孩子唱, 而且抑扬顿挫外加改的略羞耻的歌词...现在听听浑身鸡皮疙瘩,,,,,:smile:
        Anonymous___:@黑暗料理菌 32个赞全给你好不好
      • 696e9de18e1a:你这个线看上去带光影效果?
        Anonymous___:@不正经吖不正经 这是一条带有魔力的线,咒语是:paint.setShadowLayer(15, 0, 0, Color.WHITE);
      • Rc在努力:暴露年纪啦,老铁
        Anonymous___:@Rc在努力 94年,不怕:wink:
      • 夜航星osmo:看来工作还是太轻松了
        夜航星osmo:@Anonymous___ 黄片拍多了,养养身体
        Anonymous___:@Aeggainety 拍黄片的同学怎么跑到安卓来了:smirk:
      • asAnotherJack:我要看卡布达启动超级变换形态:grin:
        Anonymous___:@嗨__你的益达 那不是机器人3D变身嘛:sleepy:
      • X1a0Yu_:真tm带劲
      • 冰冰的冻结:大神好厉害 :stuck_out_tongue_winking_eye:
      • ab72c065186a:很棒,有想法。我一开始是想到用SVG··· 楼主不妨再来个百变小樱的动画吧~:grin:
        Anonymous___:@凶残的程序员 gay里gay气的,我不看小樱:sleepy:
      • 沉默寡言若有所思:这也是我的童年啊。。
        Anonymous___:@海阔天空_a29c 哈哈,握爪
      • 仰望星空的张拭心:酷酷的
        Anonymous___:@仰望星空的张拭心 骚年,搞基么

      本文标题:召唤,光能使者--玩转PathMeasure

      本文链接:https://www.haomeiwen.com/subject/uttjdxtx.html