テキストパスアニメーションコントロールTextPathView解析

30350 ワード

テキストパスアニメーションコントロールTextPathView解析
出典:炎の鎧csdnブログ:http://blog.csdn.net/totond 炎の鎧メールボックス:[email protected]本プロジェクトのGithubアドレス:https://github.com/totond/TextPathView この文章はオリジナルです。転載はこの出所を明記してください。この文章はWeChat公衆号Golin_に授権されました。ブログ(郭霖)独占発表
前言
このブログは主にTextPathViewの実現原理を紹介していますが、TextPathViewの利用はREADMEを参照することができます。
考え方の紹介
以下に書いたTextPathViewの構想の紹介は主に二つの部分があります。一部は文字経路の実現で、文字経路の取得、同期絵画と非同期絵画を含みます。一部は絵筆の特効で、各種の絵筆の特効の実現の構想を含みます。
テキストパス
テキストパスの実現はコア部分で、入力した文字をPathに変換して絵を描くのが主な仕事です。絵は二つの種類に分けられます。
  • の一つは同時絵画、つまり「画筆」だけに相当して、順番に各画数によって文字のPathを描きます。下の通りです。
  • の一つは非同期絵画で、つまり複数の「画筆」に相当します。各画(閉じた道)には1本があり、文字のPathを描きます。下のように:
  • の違いは、一つのスレッドが同期して絵を描き、複数の非同期画を描くようになります。もちろん、実際に実現されるのは全部メインスレッドの中で描かれています。具体的に実現されるのは以下の通りです。
    テキストパスの取得
    テキストパスの取得には、Paintの方法getTextPath(String text, int start, int end,float x, float y, Path path)が用いられており、この方法は、String全体のPath(すべての閉じたPathを含む)を取得し、その後、PathMeasreクラスに設定し、後に絵を描く時に経路を切り取ることができる。Sync TextPathViewの中のような:
        //       
        @Override
        protected void initTextPath(){
            //...
            mTextPaint.getTextPath(mText, 0, mText.length(), 0, mTextPaint.getTextSize(), mFontPath);
            mPathMeasure.setPath(mFontPath, false);
            mLengthSum = mPathMeasure.getLength();
            //          
            while (mPathMeasure.nextContour()) {
                mLengthSum += mPathMeasure.getLength();
            }
        }
    入力されたString値を設定するたびに、initTextPath()を呼び出してテキストパスを初期化する。
    PathMeasreはPathのサブクラスで、Pathを切り取り、Pathの上の点の座標、正接値などを得ることができます。
    テキストパスの同期
    同時に絵を描くということは、各画を順番に描くことです。このような描写はSync TextPathViewで実現されています。このような絵の描き方は複雑ではなく、入力の割合によって文字経路の表示割合を決定すればいいです。このように考えると、具体的に実現するかそれともコードを通すかについては、ここでまずグローバル属性を紹介します。
        //      、      、      
        protected Path mFontPath = new Path(), mDst = new Path(), mPaintPath = new Path();
        //    
        protected ValueAnimator mAnimator;
        //     
        protected float mAnimatorValue = 0;
        //      
        protected float mStop = 0;
        //      
        protected boolean showPainter = false, canShowPainter = false;
        //      
        protected float[] mCurPos = new float[2];
    前のinit時に取得した合計長mLengthSumと比例progressに基づいて、絵を描くテキストパスの部分の長さmStopを求め、そして一つのwhileを循環させてmPathMeasreを最後のセグメントに位置させ、その間に循環するセグメントを絵画のターゲットパスmDstに加え、最後のセグメントPathを残りの長さで切り取ります。セッション:
        /**
         *          
         * @param progress     ,0-1
         */
        @Override
        public void drawPath(float progress) {
            if (!isProgressValid(progress)){
                return;
            }
            mAnimatorValue = progress;
            mStop = mLengthSum * progress;
    
            //    
            mPathMeasure.setPath(mFontPath, false);
            mDst.reset();
            mPaintPath.reset();
    
            //        
            while (mStop > mPathMeasure.getLength()) {
                mStop = mStop - mPathMeasure.getLength();
                mPathMeasure.getSegment(0, mPathMeasure.getLength(), mDst, true);
                if (!mPathMeasure.nextContour()) {
                    break;
                }
            }
            mPathMeasure.getSegment(0, mStop, mDst, true);
    
            //      
            if (canShowPainter) {
                mPathMeasure.getPosTan(mStop, mCurPos, null);
                drawPaintPath(mCurPos[0], mCurPos[1], mPaintPath);
            }
    
            //    
            postInvalidate();
        }
    最後に呼び出したonDraw():
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            //...
    
            //      
            if (canShowPainter) {
                canvas.drawPath(mPaintPath, mPaint);
            }
            //      
            canvas.drawPath(mDst, mDrawPaint);
    
        }
    このようにプログレスに対応するテキストパスを描くことができます。
    テキストパスの非同期絵画
    非同期の絵画、つまり複数の「画筆」に相当します。各画数(閉じた道)には1本があります。文字のPathを描きます。このような描写はAync TextPathViewで実現します。このような画法も複雑ではありません。割合によって文字路の各画数を決定します。(閉じたパス)の表示スケールは大丈夫です。具体的には、whileを使って、すべての画数(閉じたパス)Pathを巡回します。循環中にprogressの割合から切り取り長さmStopを算出し、mDstに入れて最後に絵を描きます。ここでPaint.getTextPath()コードを与えたらいいです。
        /**
         *          
         * @param progress     ,0-1
         */
        @Override
        public void drawPath(float progress){
            if (!isProgressValid(progress)){
                return;
            }
            mAnimatorValue = progress;
    
            //    
            mPathMeasure.setPath(mFontPath,false);
            mDst.reset();
            mPaintPath.reset();
    
            //        
            while (mPathMeasure.nextContour()) {
                mLength = mPathMeasure.getLength();
                mStop = mLength * mAnimatorValue;
                mPathMeasure.getSegment(0, mStop, mDst, true);
    
                //      
                if (canShowPainter) {
                    mPathMeasure.getPosTan(mStop, mCurPos, null);
                    drawPaintPath(mCurPos[0],mCurPos[1],mPaintPath);
                }
            }
    
            //    
            postInvalidate();
        }
    
    これにより、各画数を一つの個体として、文字の経路を比例して表示することができます。
    ブラシの効果
    ブラシ効果の原理
    ブラシの効果とは、現在の絵の終点を基準として、Pathを少し増やして、全体の動画をより美しく見せることです。下記のスパーク効果のようです。
    具体的な原理は、PathMeassireクラスのdrawPath()方法を利用して、絵文字経路のたびにgetPosTan(float distance, float pos[], float tan[])を呼び出して近くのmPaintPathを描画し、drawPaintPath()で描けばいいということです。
        /**
         *          
         * @param progress     ,0-1
         */
        @Override
        public void drawPath(float progress) {
            //...
    
            //      
            if (canShowPainter) {
                mPathMeasure.getPosTan(mStop, mCurPos, null);
                drawPaintPath(mCurPos[0], mCurPos[1], mPaintPath);
            }
    
            //    
            postInvalidate();
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            //...
    
            //      
            if (canShowPainter) {
                canvas.drawPath(mPaintPath, mPaint);
            }
            //      
            canvas.drawPath(mDst, mDrawPaint);
    
        }
    ondraw()方法の実装は、Sync TextPathViewの例である。
        //    
        private SyncTextPainter mPainter;
    
        private void drawPaintPath(float x, float y, Path paintPath) {
            if (mPainter != null) {
                mPainter.onDrawPaintPath(x, y, paintPath);
            }
        }
    ここのブラシの特殊効果Painterは一つのインターフェースです。ユーザーにカスタマイズさせることができます。絵画の原理が違っています。Painterも二つに分けられます。
        public interface SyncTextPainter extends TextPainter {
            //         
            void onStartAnimation();
    
            /**
             *           
             * @param x      x  
             * @param y      y  
             * @param paintPath   Path  ,            
             */
            @Override
            void onDrawPaintPath(float x, float y, Path paintPath);
        }
    
        public interface AsyncTextPainter extends TextPainter{
            /**
             *           
             * @param x      x  
             * @param y      y  
             * @param paintPath   Path  ,            
             */
            @Override
            void onDrawPaintPath(float x, float y, Path paintPath);
        }
    TextPainterはもちろん、親インターフェースです。そして、ユーザーはset方式でTextPainterに伝えられます。
        //      
        public void setTextPainter(SyncTextPainter listener) {
            this.mPainter = listener;
        }
    以上がブラシの特効の原理であり、使用者はTextPainterインターフェイスを書き換えることによって特効を描きます。
    効果の実施例
    TextPathViewは3種類のオリジナルのブラシ効果を一時的に実現しました。
    
    //      ,                    ,       
    public class ArrowPainter implements SyncTextPathView.SyncTextPainter{}
    
    //        ,             
    public class PenPainter implements SyncTextPathView.SyncTextPainter,AsyncTextPathView.AsyncTextPainter {}
    
    //    ,          ,                         
    public class FireworksPainter implements SyncTextPathView.SyncTextPainter{}
    
    矢印と火花について説明します。ペンは簡単すぎます。もちろんコードを見ても分かります。そしてこの両方は速度を計算するタイプを使いました。
    /**
     * author : yany
     * e-mail : [email protected]
     * time   : 2018/02/08
     * desc   :                   
     */
    
    public class VelocityCalculator {
        private float mLastX = 0;
        private float mLastY = 0;
        private long mLastTime = 0;
        private boolean first = true;
    
        private float mVelocityX = 0;
        private float mVelocityY = 0;
    
        //  
        public void reset(){
            mLastX = 0;
            mLastY = 0;
            mLastTime = 0;
            first = true;
        }
    
        //    
        public void calculate(float x, float y){
            long time = System.currentTimeMillis();
            if (!first){
                //       ,        ,    deltaTime = 1,    
    //            float deltaTime = time - mLastTime;
    //            mVelocityX = (x - mLastX) / deltaTime;
    //            mVelocityY = (y - mLastY) / deltaTime;
                mVelocityX = x - mLastX;
                mVelocityY = y - mLastY;
            }else {
                first = false;
            }
    
            mLastX = x;
            mLastY = y;
            mLastTime = time;
    
        }
    
        public float getVelocityX() {
            return mVelocityX;
        }
    
        public float getVelocityY() {
            return mVelocityY;
        }
    }
  • 矢印特効:伝来した現在点と前の点との間の速度方向によって、矢印の方向をまっすぐにする。このPathは、前進速度の逆方向に、現在の画点を起点として、一定の角度で2本の直線を描くべきである。私たちは幾何学的数学的問題に転化できる。既知の矢印の長さはrであり、挟角はaである。現在のポイント座標(x,y)もあります。その速度はアングルで、矢印の二つの端の座標(字が下手で、これらの細部を気にしないでください。O(∩∩)O):
  • 上のこの簡単な高校の数学の問題は意外にも半日やりました。具体的にはAndroidのView座標系を使って描きませんでした。伝統的な数学座標系で描いていますので、計算すると毎回ずれがあります。この問題を意識したら簡単です。
    上記の導き出す過程によって、矢印の二つの端の座標を導き出すことができます。そしてコードによって表現されます。
    /**
     * author : yany
     * e-mail : [email protected]
     * time   : 2018/02/09
     * desc   :       ,                    ,       
     */
    
    public class ArrowPainter implements SyncTextPathView.SyncTextPainter {
        private VelocityCalculator mVelocityCalculator = new VelocityCalculator();
        //    
        private float radius = 60;
        //    
        private double angle = Math.PI / 8;
    
    //...
    
        @Override
        public void onDrawPaintPath(float x, float y, Path paintPath) {
            mVelocityCalculator.calculate(x, y);
            double angleV = Math.atan2(mVelocityCalculator.getVelocityY(), mVelocityCalculator.getVelocityX());
            double delta = angleV - angle;
            double sum = angleV + angle;
            double rr = radius / (2 * Math.cos(angle));
            float x1 = (float) (rr * Math.cos(sum));
            float y1 = (float) (rr * Math.sin(sum));
            float x2 = (float) (rr * Math.cos(delta));
            float y2 = (float) (rr * Math.sin(delta));
    
            paintPath.moveTo(x, y);
            paintPath.lineTo(x - x1, y - y1);
            paintPath.moveTo(x, y);
            paintPath.lineTo(x - x2, y - y2);
        }
    
        @Override
        public void onStartAnimation() {
            mVelocityCalculator.reset();
        }
    }
    
    //  set  ...
  • 火花の効果は矢印の効果の説明であり、矢印に基づいていくつかの角度をランダムにし、長さがランダムな矢印を加えて、矢印の線分をランダムな段数に切って、火花になります。
    /**
     * author : yany
     * e-mail : [email protected]
     * time   : 2018/02/11
     * desc   :     ,          ,                         
     */
    
    public class FireworksPainter implements SyncTextPathView.SyncTextPainter {
        private VelocityCalculator mVelocityCalculator = new VelocityCalculator();
        private Random random = new Random();
        //    
        private float radius = 100;
        //    
        private double angle = Math.PI / 8;
        //       
        private static final int arrowCount = 6;
        //       
        private static final int cutCount = 9;
    
    
        public FireworksPainter(){
        }
    
        public FireworksPainter(int radius,double angle){
            this.radius = radius;
            this.angle = angle;
        }
    
        @Override
        public void onDrawPaintPath(float x, float y, Path paintPath) {
            mVelocityCalculator.calculate(x, y);
    
            for (int i = 0; i < arrowCount; i++) {
                double angleV = Math.atan2(mVelocityCalculator.getVelocityY(), mVelocityCalculator.getVelocityX());
                double rAngle = (angle * random.nextDouble());
                double delta = angleV - rAngle;
                double sum = angleV + rAngle;
                double rr = radius * random.nextDouble() / (2 * Math.cos(rAngle));
                float x1 = (float) (rr * Math.cos(sum));
                float y1 = (float) (rr * Math.sin(sum));
                float x2 = (float) (rr * Math.cos(delta));
                float y2 = (float) (rr * Math.sin(delta));
    
                splitPath(x, y, x - x1, y - y1, paintPath, random.nextInt(cutCount) + 2);
                splitPath(x, y, x - x2, y - y2, paintPath, random.nextInt(cutCount) + 2);
            }
        }
    
        @Override
        public void onStartAnimation() {
            mVelocityCalculator.reset();
        }
    
        //  Path   
        //  count   0
        private void splitPath(float startX, float startY, float endX, float endY, Path path, int count) {
            float deltaX = (endX - startX) / count;
            float deltaY = (endY - startY) / count;
            for (int i = 0; i < count; i++) {
                if (i % 3 == 0) {
                    path.moveTo(startX, startY);
                    path.lineTo(startX + deltaX, startY + deltaY);
                }
                startX += deltaX;
                startY += deltaY;
            }
        }
    }
    全体構造
    上で紹介したのは全部部分的な細部の実現ですが、TextPathViewは一つのカスタムViewとして、一つの全体的な作業プロセスをカプセル化する必要があります。これによって、ユーザーが便利に使えるようになり、結合性を低下させます。
    親タイプTextPathView
    READMEを見たことがある人は、TextPathViewはユーザーに直接使用させるのではなく、ユーザーにサブタイプSync TextPathViewとAync TextPathViewを使用させて、同期絵画と非同期絵画の機能を実現しています。一方、親タイプTextPathViewはサブクラスに多重されるコードを書いています。
    仕事の流れ
    Sync TextPathViewとAync TextPathViewの作業過程は同じです。ここではSync TextPathViewを例にとって、作成から使用までの過程を紹介します。
  • まず作成するときは、drawPaintPath()方法を実行する必要があります。
  •     protected void init() {
    
            //     
            initPaint();
    
            //       
            initTextPath();
    
            //        
            if (mAutoStart) {
                startAnimation(0,1);
            }
    
            //                
            if (mShowInStart){
                drawPath(1);
            }
        }
    
        protected void initPaint(){
            mTextPaint = new Paint();
            mTextPaint.setTextSize(mTextSize);
    
            mDrawPaint = new Paint();
            mDrawPaint.setAntiAlias(true);
            mDrawPaint.setColor(mTextStrokeColor);
            mDrawPaint.setStrokeWidth(mTextStrokeWidth);
            mDrawPaint.setStyle(Paint.Style.STROKE);
            if (mTextInCenter){
                mDrawPaint.setTextAlign(Paint.Align.CENTER);
            }
    
            mPaint = new Paint();
            mPaint.setAntiAlias(true);
            mPaint.setColor(mPaintStrokeColor);
            mPaint.setStrokeWidth(mPaintStrokeWidth);
            mPaint.setStyle(Paint.Style.STROKE);
        }
    
    //   initTextPath() drawPath()     ,       ...
  • 測定過程に入るオンメスア:
  •     /**
         *   onMeasure    WRAP_CONTENT  
         */
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
            int hSpeSize = MeasureSpec.getSize(heightMeasureSpec);
    //        int hSpeMode = MeasureSpec.getMode(heightMeasureSpec);
            int wSpeSize = MeasureSpec.getSize(widthMeasureSpec);
    //        int wSpeMode = MeasureSpec.getMode(widthMeasureSpec);
            int width = wSpeSize;
            int height = hSpeSize;
    
            mTextWidth = TextUtil.getTextWidth(mTextPaint,mText);
            mTextHeight = mTextPaint.getFontSpacing() + 1;
    
            if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT){
                width = (int) mTextWidth;
            }
            if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT){
                height = (int) mTextHeight;
            }
            setMeasuredDimension(width,height);
        }
  • ユーザがinit()を呼び出し、テキストパスアニメーションの描画を開始する。
  •     /**
         *           
         * @param start     ,  0-1
         * @param end     ,  0-1
         */
        public void startAnimation(float start, float end) {
            if (!isProgressValid(start) || !isProgressValid(end)){
                return;
            }
            if (mAnimator != null) {
                mAnimator.cancel();
            }
            initAnimator(start, end);
            initTextPath();
            canShowPainter = showPainter;
            mAnimator.start();
            if (mPainter != null) {
                mPainter.onStartAnimation();
            }
        }
    以上はSync TextPathViewの簡単な仕事の流れです。コメントは全部はっきり書いてあるはずです。中にはまだ細かいところがあります。もし知りたいなら、ソースコードを調べてもいいです。
    更新
  • 2018/03/08 version 0.0.5:
  • は、色が塗りつぶされたすべてのテキストを直接表示するstartAnimation()方法を追加しました。
  • TextPathAnimatoListenerをTextPathViewの内部から解放して、前に使うのは面倒くさいです。
  • showFillColorText()属性を追加して、すべての時に絵筆効果が表示されるかどうかを設定します。アニメーションが終わったら、絵筆の効果が消えるはずですので、アニメーションを実行するたびに自動的にfalseに設定されます。したがって、アニメイトを持っている時に絵筆効果を表示しないようにします。
  • 後の話
    TextPathViewの原理紹介を完成しました。TextPathViewの今考えているアプリケーションシーンは簡単なオープニング動画や進捗表示です。元旦以降は仕事の合間を縫って書いています。ここ数ヶ月は仕事が忙しくて、生活の中で多くのことに出会いましたが、自分の好きなことをやり続けます。TextPathViewは守り続けます。新しいものを開発して、皆さんの好きな話をスターにあげてください。意見と提案があります。issueを提出してください。よろしくお願いします。
    最後に住所を貼り付けます。https://github.com/totond/TextPathView