电影级TextView特效源码解析

MatchView source code analyse

Posted by Roger on July 23, 2015

今天解析一个电影级TextView特效的源码..其实是在吃老本~八个月前就扔这个上github了~唉.

上图先:

MatchView

源码地址:https://github.com/Rogero0o/MatchView

包结构是这样的:

matchview

一共六个类..真是短小精悍~

我们从 MatchTextView.java 开始,源码如下:

public class MatchTextView extends MatchView {

    /**
     * 内容
     */
    String mContent;
    float mTextSize;
    int mTextColor;

    public MatchTextView(Context context) {
        super(context);
        init();
    }

    public MatchTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initAttrs(attrs);
    }

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

    void initAttrs(AttributeSet attrs) {
        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.match);
        //获取尺寸属性值,默认大小为:25
        mTextSize = a.getDimension(R.styleable.match_textSize, 25);
        //获取颜色属性值,默认颜色为:Color.WHITE
        mTextColor = a.getColor(R.styleable.match_textColor, Color.WHITE);
        //获取内容
        mContent = a.getString(R.styleable.match_text);
        init();
    }

    void init() {
        this.setBackgroundColor(Color.TRANSPARENT);
        if (!TextUtils.isEmpty(mContent)) {
            setTextColor(mTextColor);
            setTextSize(mTextSize);
            initWithString(mContent);
            show();
        }
    }


    public void setText(String text) {
        this.mContent = text;
        init();
    }

}

这里面没什么东西,只是简单的读取一些属性然后设置,其主要工作都是再其父类 MatchView.java 中完成的。 看到方法init()

OK,让我们打开MatchView.java 和它的show()方法看看:

public class MatchView extends View {


    /**
     * 加载状态 1、划入 2、划出
     */
    private int STATE = ;

    private MatchInListener mMatchInListener;
    private MatchOutListener mMatchOutListener;


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

    private void initView() {
        this.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
        Utils.init(getContext());
        mLineWidth = Utils.dp2px(1);
        mDropHeight = Utils.dp2px(40);
        horizontalRandomness = Utils.SCREEN_WIDTH_PIXELS / 2;

        setPadding(, Utils.dp2px(mPaddingTop), , Utils.dp2px(mPaddingTop));

        mHandler = new Handler() {
            @Override
            public void dispatchMessage(Message msg) {
                super.dispatchMessage(msg);
                if (STATE == 1) {//划入
                    if (progress < 100) {
                        progress++;
                        setProgress((progress * 1f / (100)));
                        mHandler.sendEmptyMessageDelayed(, (long) (mInTime * 10));
                    } else {
                        STATE = 2;
                        if (mMatchInListener != null) {
                            mMatchInListener.onFinish();
                        }
                    }
                } else if (STATE == 2) {//划出
                    if (mIsInLoading) {
                        lightFinish();
                    }
                    if (progress > ) {
                        progress--;
                        setProgress((progress * 1f / (100)));
                        mHandler.sendEmptyMessageDelayed(, (long) (mOutTime * 10));
                    } else {
                        progress = ;
                        if (mMatchOutListener != null) {
                            mMatchOutListener.onFinish();
                        }
                        STATE = 1;
                    }
                }
            }
        };
    }
    protected void show() {
        if (mItemList.size() == ) {
            return;
        }
        STATE = 1;
        mHandler.sendEmptyMessage();
        if (mMatchInListener != null) {
            mMatchInListener.onBegin();
        }
    }

    public void hide() {
        if (mMatchOutListener != null) {
            mMatchOutListener.onBegin();
        }
        mHandler.sendEmptyMessage();
    }

    public void setProgress(float progress) {
        if (mMatchInListener != null && STATE == 1) {
            mMatchInListener.onProgressUpdate(progress);
        } else if (mMatchOutListener != null && STATE == 2) {
            mMatchOutListener.onProgressUpdate(progress);
        }

        if (progress == 1) {
            if (isBeginLight) {
                beginLight();
            }
        } else if (mIsInLoading) {
            lightFinish();
        }
        mProgress = progress;
        postInvalidate();
    }



    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int height = getTopOffset() + mDrawZoneHeight + getBottomOffset();
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        mOffsetX = (getMeasuredWidth() - mDrawZoneWidth) / 2;
        mOffsetY = getTopOffset();
        mDropHeight = getTopOffset();
    }

    private int getTopOffset() {
        return getPaddingTop() + Utils.dp2px(10);
    }

    private int getBottomOffset() {
        return getPaddingBottom() + Utils.dp2px(10);
    }

    public void initWithString(String str) {
        initWithString(str, mTextSize);
    }

    public void initWithString(String str, float fontSize) {
        ArrayList<float[]> pointList = MatchPath.getPath(str, fontSize * 0.01f, 14);
        initWithPointList(pointList);
    }

    public void initWithStringArray(int id) {
        String[] points = getResources().getStringArray(id);
        ArrayList<float[]> pointList = new ArrayList<float[]>();
        for (int i = ; i < points.length; i++) {
            String[] x = points[i].split(",");
            float[] f = new float[4];
            for (int j = ; j < 4; j++) {
                f[j] = Float.parseFloat(x[j]);
            }
            pointList.add(f);
        }
        initWithPointList(pointList);
    }

    public float getScale() {
        return mScale;
    }

    public void setScale(float scale) {
        mScale = scale;
    }

 public void initWithPointList(ArrayList<float[]> pointList) {

        float drawWidth = ;
        float drawHeight = ;
        boolean shouldLayout = mItemList.size() > ;
        mItemList.clear();
        for (int i = ; i < pointList.size(); i++) {
            float[] line = pointList.get(i);
            PointF startPoint = new PointF(Utils.dp2px(line[]) * mScale, Utils.dp2px(line[1]) * mScale);
            PointF endPoint = new PointF(Utils.dp2px(line[2]) * mScale, Utils.dp2px(line[3]) * mScale);

            drawWidth = Math.max(drawWidth, startPoint.x);
            drawWidth = Math.max(drawWidth, endPoint.x);

            drawHeight = Math.max(drawHeight, startPoint.y);
            drawHeight = Math.max(drawHeight, endPoint.y);

            MatchItem item = new MatchItem(i, startPoint, endPoint, mTextColor, mLineWidth);
            item.resetPosition(horizontalRandomness);
            mItemList.add(item);
        }
        mDrawZoneWidth = (int) Math.ceil(drawWidth);
        mDrawZoneHeight = (int) Math.ceil(drawHeight);
        if (shouldLayout) {
            requestLayout();
        }
    }

    public void beginLight() {
        mIsInLoading = true;
        mAniController.start();
        invalidate();
    }

    public void lightFinish() {
        mIsInLoading = false;
        mAniController.stop();
    }

 @Override
    public void onDraw(Canvas canvas) {
         super.onDraw(canvas);
        float progress = mProgress;
        int c1 = canvas.save();
        int len = mItemList.size();
        for (int i = ; i < mItemList.size(); i++) {
            canvas.save();
            MatchItem LoadingViewItem = mItemList.get(i);
            float offsetX = mOffsetX + LoadingViewItem.midPoint.x;
            float offsetY = mOffsetY + LoadingViewItem.midPoint.y;

            if (mIsInLoading) {
                LoadingViewItem.getTransformation(getDrawingTime(), mTransformation);
                canvas.translate(offsetX, offsetY);
            } else {

                if (progress == ) {
                    LoadingViewItem.resetPosition(horizontalRandomness);
                    continue;
                }

                float startPadding = (1 - internalAnimationFactor) * i / len;
                float endPadding = 1 - internalAnimationFactor - startPadding;

                // done
                if (progress == 1 || progress >= 1 - endPadding) {
                    canvas.translate(offsetX, offsetY);
                    LoadingViewItem.setAlpha(mBarDarkAlpha);
                } else {
                    float realProgress;
                    if (progress <= startPadding) {
                        realProgress = ;
                    } else {
                        realProgress = Math.min(1, (progress - startPadding) / internalAnimationFactor);
                    }
                    offsetX += LoadingViewItem.translationX * (1 - realProgress);
                    offsetY += -mDropHeight * (1 - realProgress);
                    Matrix matrix = new Matrix();
                    matrix.postRotate(360 * realProgress);
                    matrix.postScale(realProgress, realProgress);
                    matrix.postTranslate(offsetX, offsetY);
                    LoadingViewItem.setAlpha(mBarDarkAlpha * realProgress);
                    canvas.conca t(matrix);
                }
            }
            LoadingViewItem.draw(canvas);
            canvas.restore();
        }
        if (mIsInLoading) {
            invalidate();
        }
        canvas.restoreToCount(c1);
    }
 private class AniController implements Runnable {

        private int mTick = ;
        private int mCountPerSeg = ;
        private int mSegCount = ;
        private int mInterval = ;
        private boolean mRunning = true;

        private void start() {
            mRunning = true;
            mTick = ;

            mInterval = mLoadingAniDuration / mItemList.size();
            mCountPerSeg = mLoadingAniSegDuration / mInterval;
            mSegCount = mItemList.size() / mCountPerSeg + 1;
            run();
        }

         @Override
        public void run() {

            int pos = mTick % mCountPerSeg;
            for (int i = ; i < mSegCount; i++) {

                int index = i * mCountPerSeg + pos;
                if (index > mTick) {
                    continue;
                }

                index = index % mItemList.size();
                MatchItem item = mItemList.get(index);

                item.setFillAfter(false);
                item.setFillEnabled(true);
                item.setFillBefore(false);
                item.setDuration(mLoadingAniItemDuration);
                item.start(mFromAlpha, mToAlpha);
            }

            mTick++;
            if (mRunning) {
                postDelayed(this, mInterval);
            }
        }
        private void stop() {
            mRunning = false;
            removeCallbacks(this);
        }
    }

    public interface MatchInListener {
        public void onBegin();

        public void onProgressUpdate(float progress);

        public void onFinish();
    }

    public interface MatchOutListener {
        public void onBegin();

        public void onProgressUpdate(float progress);

        public void onFinish();
    }
}

看到 show 方法:

protected void show() {
        if (mItemList.size() == ) {
            return;
        }
        STATE = 1;
        mHandler.sendEmptyMessage();
        if (mMatchInListener != null) {
            mMatchInListener.onBegin();
        }
    }

STATE = 1; 将加载状态设置为划入状态,然后调用 mHandler.sendEmptyMessage(0); 开始动画,看看这个mHandler干了什么。

mHandler = new Handler() {
            @Override
            public void dispatchMessage(Message msg) {
                super.dispatchMessage(msg);
                if (STATE == 1) {//划入
                    if (progress < 100) {
                        progress++;
                        setProgress((progress * 1f / (100)));
                        mHandler.sendEmptyMessageDelayed(, (long) (mInTime * 10));
                    } else {
                        STATE = 2;
                        if (mMatchInListener != null) {
                            mMatchInListener.onFinish();
                        }
                    }
                } else if (STATE == 2) {//划出
                    if (mIsInLoading) {
                        lightFinish();
                    }
                    if (progress > ) {
                        progress--;
                        setProgress((progress * 1f / (100)));
                        mHandler.sendEmptyMessageDelayed(, (long) (mOutTime * 10));
                    } else {
                        progress = ;
                        if (mMatchOutListener != null) {
                            mMatchOutListener.onFinish();
                        }
                        STATE = 1;
                    }
                }
            }
        };

判断STATE==1,然后progress小于100则一直 +1 并调用 setProgress((progress * 1f / (100))); 直到 pregress>=100 则将STATE设置为2并调用mMatchInListener.onFinish();回调,看来是 setProgress((progress * 1f / (100))); 这个方法使动画一直变幻,而且每个progress的值都对应一个动画的状态,所以才要持续不断的调用,OK,再跟进setProgress中看看:

public void setProgress(float progress) {
        if (mMatchInListener != null && STATE == 1) {
            mMatchInListener.onProgressUpdate(progress);
        } else if (mMatchOutListener != null && STATE == 2) {
            mMatchOutListener.onProgressUpdate(progress);
        }
        if (progress == 1) {
            if (isBeginLight) {
                beginLight();
            }
        } else if (mIsInLoading) {
            lightFinish();
        }
        mProgress = progress;
        postInvalidate();
    }

看过来可能有点失望,这个方法只是处理了一些回调,在progress=1的时候调用闪亮的动画,将 progress赋值给 mProgress ,然后调用了 postInvalidate(); ,大家都知道这是重绘View的意思,看来我们只能去onDraw中一看究竟了。

 public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        float progress = mProgress;
        int c1 = canvas.save();
        int len = mItemList.size();
        for (int i = ; i < mItemList.size(); i++) {
            canvas.save();
            MatchItem LoadingViewItem = mItemList.get(i);
            float offsetX = mOffsetX + LoadingViewItem.midPoint.x;
            float offsetY = mOffsetY + LoadingViewItem.midPoint.y;

            if (mIsInLoading) {
                LoadingViewItem.getTransformation(getDrawingTime(), mTransformation);
                canvas.translate(offsetX, offsetY);
            } else {

                if (progress == ) {
                    LoadingViewItem.resetPosition(horizontalRandomness);
                    continue;
                }

                float startPadding = (1 - internalAnimationFactor) * i / len;
                float endPadding = 1 - internalAnimationFactor - startPadding;

                // done
                if (progress == 1 || progress >= 1 - endPadding) {
                    canvas.translate(offsetX, offsetY);
                    LoadingViewItem.setAlpha(mBarDarkAlpha);
                } else {
                    float realProgress;
                    if (progress <= startPadding) {
                        realProgress = ;
                    } else {
                        realProgress = Math.min(1, (progress - startPadding) / internalAnimationFactor);
                    }
                    offsetX += LoadingViewItem.translationX * (1 - realProgress);
                    offsetY += -mDropHeight * (1 - realProgress);
                    Matrix matrix = new Matrix();
                    matrix.postRotate(360 * realProgress);
                    matrix.postScale(realProgress, realProgress);
                    matrix.postTranslate(offsetX, offsetY);
                    LoadingViewItem.setAlpha(mBarDarkAlpha * realProgress);
                    canvas.conca t(matrix);
                }
            }
            LoadingViewItem.draw(canvas);
            canvas.restore();
        }
        if (mIsInLoading) {
            invalidate();
        }
        canvas.restoreToCount(c1);
    }

这个mItemList里面保存的是个什么东西呢?它其实是将我们初始化的文字都转换为代码里设置好的一系列PATH,看到MatchTextView.java的init方法中,有一个initWithString(mContent);,跟进去是什么呢?

最终我们来到:

 public void initWithString(String str, float fontSize) {
        ArrayList<float[]> pointList = MatchPath.getPath(str, fontSize * 0.01f, 14);
        initWithPointList(pointList);
    }

MatchPath.getPath将文本内容转换为float[],这个float[]存储的就是组成每个字母火柴棒(每一笔)的坐标,具体类在MatchPath中~若有疑问请直接进去看看,就不赘述了.

然后 initWithPointList(pointList); 这个方法再将 float[] 转换成 ArrayList ,也就是 mItemList , 一个 MatchItem 其实就是一跟火柴棒(或者说是横平竖直的一笔).

public void initWithPointList(ArrayList<float[]> pointList) {

        float drawWidth = ;
        float drawHeight = ;
        boolean shouldLayout = mItemList.size() > ;
        mItemList.clear();
        for (int i = ; i < pointList.size(); i++) {
            float[] line = pointList.get(i);
            PointF startPoint = new PointF(Utils.dp2px(line[]) * mScale, Utils.dp2px(line[1]) * mScale);
            PointF endPoint = new PointF(Utils.dp2px(line[2]) * mScale, Utils.dp2px(line[3]) * mScale);

            drawWidth = Math.max(drawWidth, startPoint.x);
            drawWidth = Math.max(drawWidth, endPoint.x);

            drawHeight = Math.max(drawHeight, startPoint.y);
            drawHeight = Math.max(drawHeight, endPoint.y);

            MatchItem item = new MatchItem(i, startPoint, endPoint, mTextColor, mLineWidth);
            item.resetPosition(horizontalRandomness);
            mItemList.add(item);
        }
        mDrawZoneWidth = (int) Math.ceil(drawWidth);
        mDrawZoneHeight = (int) Math.ceil(drawHeight);
        if (shouldLayout) {
            requestLayout();
        }
    }

看懂了吧,在初始化MatchTextView的时候,已经将文本内容转换为每一根火柴棒的坐标保存在这个 mItemList 中!所以在onDraw中要做的就只是根据progress的不同,将 mItemList 画出来即可了!

好的,让我们再回到onDraw中,为了方便我再将代码贴过来:

public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        float progress = mProgress;
        int c1 = canvas.save();
        int len = mItemList.size();
       for (int i = ; i < mItemList.size(); i++) {
            canvas.save();
            MatchItem LoadingViewItem = mItemList.get(i);
            float offsetX = mOffsetX + LoadingViewItem.midPoint.x;
            float offsetY = mOffsetY + LoadingViewItem.midPoint.y;

            if (mIsInLoading) {
                LoadingViewItem.getTransformation(getDrawingTime(), mTransformation);
                canvas.translate(offsetX, offsetY);
            } else {

                if (progress == ) {
                    LoadingViewItem.resetPosition(horizontalRandomness);
                    continue;
                }

                float startPadding = (1 - internalAnimationFactor) * i / len;
                float endPadding = 1 - internalAnimationFactor - startPadding;

                // done
                if (progress == 1 || progress >= 1 - endPadding) {
                    canvas.translate(offsetX, offsetY);
                    LoadingViewItem.setAlpha(mBarDarkAlpha);
                } else {
                    float realProgress;
                    if (progress <= startPadding) {
                        realProgress = ;
                    } else {
                        realProgress = Math.min(1, (progress - startPadding) / internalAnimationFactor);
                    }
                    offsetX += LoadingViewItem.translationX * (1 - realProgress);
                    offsetY += -mDropHeight * (1 - realProgress);
                    Matrix matrix = new Matrix();
                    matrix.postRotate(360 * realProgress);
                    matrix.postScale(realProgress, realProgress);
                    matrix.postTranslate(offsetX, offsetY);
                    LoadingViewItem.setAlpha(mBarDarkAlpha * realProgress);
                    canvas.conca t(matrix);
                }
            }
            LoadingViewItem.draw(canvas);
            canvas.restore();
        }
        if (mIsInLoading) {
            invalidate();
        }
        canvas.restoreToCount(c1);
    }

仔细观察动画的同学会发现,每一个火柴棒飞入的过程是边旋转边减小透明度的,这两个过程都在onDraw中完成。

mIsInLoading 判断是否已经飞入完成,且调用了beginLight,若完成则保持位置不变。

否则来到下面的判断中,经过一系列状态判断,看到

     Matrix matrix = new Matrix();
     matrix.postRotate(360 * realProgress);
     matrix.postScale(realProgress, realProgress);
     matrix.postTranslate(offsetX, offsetY);
     LoadingViewItem.setAlpha(mBarDarkAlpha * realProgress);

就是这四句,根据progress设置了每根火柴棒的角度和透明度~再将每一根火柴棒都onDraw出来,完成了酷炫的动画效果~

最后再来看看飞入后每根火柴棒的闪动是怎么做到的,看到前面的,setProgress方法中,若progress=1则调用 beginLight();,在该方法中我们看到一个 mAniController.start(); 来看看这个mAniController的代码,其实在上面已经有了:

private class AniController implements Runnable {

        private int mTick = ;
        private int mCountPerSeg = ;
        private int mSegCount = ;
        private int mInterval = ;
        private boolean mRunning = true;

        private void start() {
            mRunning = true;
            mTick = ;

            mInterval = mLoadingAniDuration / mItemList.size();
            mCountPerSeg = mLoadingAniSegDuration / mInterval;
            mSegCount = mItemList.size() / mCountPerSeg + 1;
            run();
        }

        @Override
        public void run() {

            int pos = mTick % mCountPerSeg;
            for (int i = ; i < mSegCount; i++) {

                int index = i * mCountPerSeg + pos;
                if (index > mTick) {
                    continue;
                }

                index = index % mItemList.size();
                MatchItem item = mItemList.get(index);

                item.setFillAfter(false);
                item.setFillEnabled(true);
                item.setFillBefore(false);
                item.setDuration(mLoadingAniItemDuration);
                item.start(mFromAlpha, mToAlpha);
            }

            mTick++;
            if (mRunning) {
                postDelayed(this, mInterval);
            }
        }

        private void stop() {
            mRunning = false;
            removeCallbacks(this);
        }
    }

调用start方法后走到run中,然后给每一个 MatchItem 设置了透明度变换的动画,为什么 MatchItem 能设置动画呢?相信你已经猜到了~因为 MatchItem 继承的就是Animation!所以最后的闪亮的动画不过是每根火柴棒轮流变换透明度的把戏而已!~~

好久没写了,一个月起码写一篇这是保底消费~~..不写点东西就说明这段时间都白费了,唉~都在吃老本..不进则退啊,要警惕了..

欢迎大家拍砖指正