打造一个可定制的Path动画

前言

创建这个库并非是由于某个需求,而是以前在阅读OkHttp源码时深感设计的精妙,一直有一个模仿其责任链模式做一个自定义View(SimpleLineView)的想法,一是为了好玩- -,二是希望能够抛砖引玉。

对于View的path动画,PathAnimView甚至是Lottie等都可以作出十分复杂酷炫的path动画。如果你的动画很复杂很酷炫,这个库可能就不太适合了。

当然,SimpleLineView也有自己的优势:

1、可随意定制路径

2、路径可以随意组合

3、支持progress

效果图

      

使用
// 圆形
PixelPath circlePath = new PixelPath(10, 10, new int[]{1, 100});
CirclePainter ciclePainter = new RealCirclePainter(circlePath, 1000, -120, 360, false);
// 矩形
PixelPath squarePath = new PixelPath(2, 2, new int[]{1, 2, 4, 3});
Painter squarePainter = new SegmentPainter(squarePath, 1000, true);
// 添加路径
mView.addPainter(ciclePainter).addPainter(squarePainter);
// 启动
mView.start();
// 停止
mView.stop();
// 继续
mView.stick();
思路

1、自定义Painter(绘制相关接口),提供绘制功能。

2、RealChain实现了Chain接口并且维护了一个Painter的list,控制所有Painter依次执行绘制。

3、SimpleLineView维护了一个RealChain并且对外提供了方法,用于添加Painter以及控制动画的启动、停止和继续。在onDraw里调用当前Painter的onDraw方法实现真正的绘制。

整体架构图如下:


前期准备:PixelPath
// 横向像素
private int mHorizontal;
// 纵向像素
private int mVertical;
// 路径经过的像素序号
private int[] mPath;

SimpleLineView将View的长和宽分成若干个格子,格子数由图片的像素决定,并且规定了path的序号(从1开始,从左往右、从上到下依次递增1)。

例如,一张像素为4 * 4的图片


这里的mHorizontal(横向格子数)和mVertical(纵向格子数)都为4。如果mPath为{1, 13, 16, 4}, 则绘制的图形为依次连接1,13,16,4的矩形(是否封闭可设置对应参数)。

如果图形的形状比较复杂,可以用PS打开图片,依次获取像素点的x和y值(这里x和y值的单位可以是像素、厘米等,但是计算时要与图像大小的单位一致)。假设图像宽为w, 高为h, 则当前点的值为 w * ( y - 1) + x。

一、Painter接口

Painter接口主要提供了绘制的功能以及绘制时所需要的一些参数。

public interface Painter {
    // 获取Path
    PixelPath getPixelPath();
    // 时长
    int duration();
    // 路径是否闭合
    boolean close();
    // 设置Paint
    void setPaint(Paint paint);
    // 获取Paint
    Paint getPaint();
    // 动画是否正在进行
    boolean isRunning();
    // 开始动画
    void start(Chain chain, Action action);
    // 停止动画
    void stop();
    // 真正绘制的地方
    void onDraw(Canvas canvas);
    // 进行下一笔绘画时,完整画完当前笔
    void completeDraw(Canvas canvas);
}

onDraw方法和View的onDraw方法一样实现绘制。这里主要介绍一下completeDraw方法。由于每个Painter是依次绘制的,当下一个Painter进行绘制时,当前Pianter的形状也需要绘制,所以这里的completeDraw应该是当前Painter所要绘制的完整形状。这个方法会被当前Painter之后的每一个Painter调用。有点绕,看一下抽象类AbstractPainter的onDraw实现:

    @Override
    public void onDraw(Canvas canvas) {
        // 1.完成之前Painter的绘制
        drawPreviouse(canvas);
        // 2.绘制当前
        realDraw(canvas);
    }

drawPreviouse方法:

    /**
     * 开始当前绘制前,先完成之前的绘制
     * @param canvas
     */
    private void drawPreviouse(Canvas canvas) {
        if (chain != null) {
            // 当前Painter的index
            final int index = chain.index();
            final List<Painter> painters = chain.painters();
            // 遍历所有之前的Painter,调用completeDraw方法
            for (int i = 0; i < index - 1; i ++) {
                final Painter painter = painters.get(i);
                painter.completeDraw(canvas);
            }
        }
    }

为了方便说明,看一下AbstractPainter的一个子类RealCirclePainter的completeDraw和realDraw方法:

    @Override
    public void completeDraw(Canvas canvas) {
        canvas.drawArc(mRectF, mStartAngle, mSweepAngle, mUseCenter, paint);
    }

    @Override
    protected void realDraw(Canvas canvas) {
        canvas.drawArc(mRectF, mStartAngle, angle(), mUseCenter, paint);
    }

mSweepAngle是所需扫过的角度,angle()为当前的角度大小,这个角度会随着时间递增,如此也就有了动画。

当然必须得看一下AbstractPainter的start方法,这个才是每个Painter开始的地方。

   @Override
    public void start(Chain chain, Action action) {
        //是否正在执行
        mIsRunning = true;
        this.chain = chain;
        // 计算实际坐标点
        pointList = action.fetchCoordinate(this);
        // 执行绘制
        boolean isFinish = performDraw(action);
        mIsRunning = false;
        // 如果的确绘制完成,下一步
        if (isFinish) {
            chain.proceed();
        }
    }

fetchCoordinate方法为绘制提供了坐标点,实现下文会介绍。之后通过performDraw方法来开始View的绘制,看一下AbstractPainter另一个子类SegmentPainter的performDraw方法实现:

    @Override
    public boolean performDraw(Action action) {

        // 总路程
        float distance = Utils.calDistance(pointList, close());

        for (int i = 0; i < pointList.size(); i++) {

            // 省略若干代码          
            while (!current.isPathFinish()) {

                if (!isRunning()) {
                    return false;
                }
                // 省略计算代码
              
                // 更新界面
                action.update(this);
                try {
                    // 每更新一次的时长
                    Thread.sleep(INTERVAL);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return false;
                }
            }
            // 保证图像都绘制
            action.update(this);
        }
        return true;
    }

在performDraw方法中会遍历PixelPoint的list,每隔INTERVAL时间调用Action接口的update方法更新View一次。

二、Chain接口

Chain主要提供了调控的功能。

public interface Chain {
    // 执行
    void proceed();
    // 当前Painter的index
    int index();
    // 设置结束监听
    void setOnFinishListener(OnFinishListener listener);
    // 所有的Painter
    List<Painter> painters();
    // 结束接听接口
    interface OnFinishListener {
        void onFinish(int index);
    }
}

看一下唯一实现类RealChain的proceed方法

    @Override
    public void proceed() {

        if (mOnFinishListener != null && mIndex > 0) {
            mOnFinishListener.onFinish(mIndex - 1);
        }
        // 如果index等于size就返回结束了
        if (mIndex == mPainters.size()) {
            return;
        }
        // 设置progress时会不断调用该方法,为了避免不断创建RealChain对象,
        // 这里用了SparseArray保存所有已经创建的RealChain对象,key为index
        Chain next = mChainPool.get(mIndex);

        if (next == null) {
            next = new RealChain(mPainters, mIndex + 1, mAction);
            next.setOnFinishListener(mOnFinishListener);
            mChainPool.put(mIndex, next);
        }

        final Painter painter = mPainters.get(mIndex);
        painter.start(next, mAction);

    }

这里照搬了OkHttp,通过在RealChain的procced方法创建新的RealChain对象实现Painter的依次执行。由于Painter都是自定义的,所以当index等于所有Painter的size时return就好了,而OkHttp的最后一个Interceptor是没有创建Chain的。

三、Action接口

Action接口主要提供了计算当前path实际坐标点、通知更新View以及设置或者获取View的所需的参数的功能,这也是View需要实现的接口。

public interface Action {

    /**
     * 更新view,实际调用的是{@link SimpleLineView#postInvalidate()}方法
     * @param painter 该painter实现view的onDraw
     */
    void update(Painter painter);

    /**
     * 对外接口,设置progress后更新view
     * @param progress
     */
    void setProgress(int progress);

    /**
     * painter中通过调用该接口进行相应的绘制工作
     * @return
     */
    int getProgress();

    /**
     * 通过当前view执行的状态作出相应处理,可参考{@link com.robog.library.painter.TaskPainter#start(Chain, Action)}方法
     * @return
     */
    int getStatus();

    /**
     * 获得当前painter下所有点的实际坐标
     * @param painter
     * @return
     */
    List<PixelPoint> fetchCoordinate(Painter painter);
}

看一下fetchCoordinate方法:

    @Override
    public List<PixelPoint> fetchCoordinate(Painter painter) {
        // 同样的,这里避免频繁设置progress不断创建PixelPoint对象
        List<PixelPoint> pixelPoints = mPointPool.get(painter);

        if (pixelPoints != null) {
            return pixelPoints;
        }

        pixelPoints = new ArrayList<>();
        Utils.setPoint(painter, pixelPoints, mWidth, mHeight);
        mPointPool.put(painter, pixelPoints);
        return pixelPoints;
    }

Utils的setPoint方法:

    public static void setPoint(Painter painter, List<PixelPoint> pixelPoints, int width, int height) {
        // 先获取PixelPath
        final PixelPath pixelPath = painter.getPixelPath();
        final int[] path = pixelPath.getPath();
        final int horizontal = pixelPath.getHorizontal();
        final int vertical = pixelPath.getVertical();

        for (int target : path) {
            // 如果PixelPath中点的序号超过总数则抛出异常
            if (target > horizontal * vertical) {
                throw new IllegalArgumentException("Current coordinate [" + target + "] is invalid!");
            }
            // 商
            int quotient = target / horizontal;
            // 余数
            int remainder = target % horizontal;
            // 实际的x和y坐标
            float x;
            float y;
            // x和y坐标的系数
            float coefficientX;
            float coefficientY;
            if (remainder != 0) {
                // 余数不为0时,这里的0.5是让实际坐标点位于方格中心点
                coefficientX = remainder - 0.5f;
                coefficientY = quotient + 0.5f;
            } else {
                // 余数为0时
                coefficientX = horizontal - 0.5f;
                coefficientY = quotient - 0.5f;
            }
            // width / horizontal为每个方格的宽度
            // 每个方格的宽度乘系数即为x的坐标
            x = coefficientX *  width / horizontal;
            // 同理
            y = coefficientY *  height / vertical;

            PixelPoint pixelPoint = new PixelPoint(x, y);
            pixelPoints.add(pixelPoint);
        }
    }

用的小学数学,看一下注释就好了。接着看一下update和onDraw方法:

    @Override
    public void update(Painter painter) {
        mCurrentPainter = painter;
        postInvalidate();
    }
    @Override
    protected void onDraw(Canvas canvas) {
        if (mCurrentPainter != null) {
            mCurrentPainter.onDraw(canvas);
        }
    }

在update中设置当前的painter,由于之前的操作在线程中,这里调用postInvalidate通知绘制,在View的onDraw方法中调用Painter的onDraw实现绘制。

介绍完三个接口,整体的流程算是介绍完了,下面看一下两个功能型的Painter。

1、DelayPainter
    @Override
    public void start(Chain chain, Action action) {
        try {
            if (Looper.myLooper() == Looper.getMainLooper()) {
                throw new RuntimeException("Can't delay in the main thread!");
            }
            Thread.sleep(mTime);
            chain.proceed();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
在start方法通过Thread.sleep进行延时,如果当前方法在主线程执行就抛出异常。
2、TaskPainter
    @Override
    public void start(final Chain chain, final Action action) {
        mIsRunning = true;
        EXECUTOR.execute(new Runnable() {
            @Override
            public void run() {
                // 当status为start时重置point
                if (action.getStatus() == STATUS_START) {
                    Utils.resetPointStatus(mPainterPool);
                }
                chain.proceed();
                mIsRunning = false;
            }
        });
    }

当Painter有多个时,计算会耗费一定的时间,这里将chain的procced置于线程中,让后续的过程都在线程中执行以保证动画的流畅。并且,当start动画时,将PixelPoint的状态重置,保证下一次绘制是一个完整的过程。

不足

1、图形较为复杂时,通过PS获取计算坐标点较为繁琐。

2、由于计算在子线程,绘制在主线程,当SimpleLineView设置progress过快时,上一步onDraw可能未完成,画面可能会闪动,暂时的解决方法是将此过程放入主线程。使用代码如下:

mView.addPainter(mCicleProgressPainter).addPainter(mHookProgressPainter).onMain();
mView.setProgress(progress);

OK,基本上介绍完了,如果有什么不足或者有什么问题欢迎指正!

项目地址:https://github.com/XingdongYu/SimpleLineView

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值