LW教你自定義安卓控件之LoadingView

項目下載地址:http://download.csdn.net/detail/qq_26331127/9418430

github地址 :https://github.com/LoveIsReal/LWang

先看效果:



題外話:寫這個動畫撫慰一下自己和可憐的廣大單身狗們(不包括我生氣)希望看着能開心    

寫這個的想法來自於一篇Android開發中文站 的文章    地址:

http://mp.weixin.qq.com/s?__biz=MzA4NDM2MjAwNw==&mid=400894996&idx=1&sn=586b373aad44c03f881ff688d8daae0f&scene=0#wechat_redirect

他那個動畫的最後是畫了一個勾,但是覺得很普通     於是改成了現在這樣    覺得很nice的自覺底下頂下大笑


既然是做動畫,不管什麼工具或方式,首先是設計動畫流程

我的動畫流程:

1 ,圓弧加載成整環,隨着進度值改變顏色  ,顏色區間是紅色到藍色。   這個並不是動畫,是通過一個線程不斷改變進度值從0 到 100  模擬了加載的過程

(所以當這個進度達到一百之後,程序後面的事就和加載無關了,純屬動畫)

2 ,圓環兩側平行於圓環圓心位置同時拋出方塊,運動到圓環正上方,此時距離圓環大小就是圓環半徑

3,將上一個動畫的兩個方塊 改爲一個圓球,並且改爲綠色,開始下落動畫 ,終點是圓心

4,同時開始嘴  和 眼睛的繪製的動畫。眼睛:圓心位置開始繪製兩個圓球  逐漸運動到最終位置 。  嘴:從左向右繪製圓弧


分析結束。。。。。。

代碼中最重要的知識點就是屬性動畫。

推薦看這篇文章  :http://blog.csdn.net/jdsjlzx/article/details/45558901

接下來的內容默認你看完上面那篇文章    知道什麼是 ValueAnimator    Interpolator   TypeEvaluator  

接下來開始裝逼 ,額,錯了,開始講解代碼。。。。。

並沒有提供自定義屬性  就不用什麼attr了  

繼承View   :

public class LoadingView extends View {

標準的三個參數構造方法  ,最後一個構造方法做各種Paint的初始化操作  


onMeasure() 、onLayout 方法 不需要重寫     因爲內部沒有子View xml 中設定的是固定寬高   。


看第一個流程 :

我們繪製圓環   繪製圓環用的是canvas.drawArc() 方法  

裏面需要一個Rectf 參數  就是一個矩形區域  我們設置成成員變量 ,在onSizeChanged (onMeasure 之後 onDraw之前調用的一個方法)方法中初始化它

看張圖:   藍色的圓環即爲所需



然後就是進度值的設定:我在 Activity 中設置按鈕的監聽,啓動一個 Thread 去模擬加載進度的過程 ,

Thread.sleep(20);

每隔20毫秒 加載1%  

View 類中 定義了一個   setProgress() 

 public void setProgress(int progress) {

        if (progress == 0) {
            //   由於ColorEvaluator 中的保存顏色值的靜態變量   所有每次重置進度 也需要重置這些變量
            status = 0;  //                         每次重新運行  改變動畫的運行標誌位
            currentPosition = ballRadius;
            ColorEvaluator.resetColor();
        }
        float fraction = 1.0f * (progress) / 100;   
        String color_str = ColorEvaluator.evaluate(fraction, "#0000ff", "#ff0000");
        int color = Color.parseColor(color_str);
        Log.e("wxy", "asdasdasd   " + fraction);
        circlePaint.setColor(color);
        this.progress = progress;
        postInvalidate();   //   之所有在這裏使用postInvalidate  因爲我是開了個線程去更新

        if (progress == 100) {   // 開始方塊拋出動畫
            status = drawFlyRect;    //  先改變運行狀態  因爲在下面的post線程會調用onDraw
            post(new Runnable() {     //    這裏的post方法     其實就是View中專門設計的方法
                //  Causes the Runnable to be added to the message queue.
                //   The runnable will be run on the user interface thread   這兩行英文是它的官方解釋
                // 這個post產生的Runnable將運行在主線程中         這就解釋了 爲什麼用了它  Mainactivity中就不用Looper.prepare()方法 ,
                //  而且也滿足了  這個ValueAnimator必須用在主線程中
                // 從源碼中看 View的 post  方法 開啓了一個mHandler.post(runnable);
                @Override
                public void run() {
                    animation_fly.start();
                }
            });
        }
    }
這個方法主要就是更新了成員變量progress     用postInvalidate()  方法去刷新View   ;

看下drawArc方法的源碼  :

     * @param oval       The bounds of oval used to define the shape and size
     *                   of the arc
     * @param startAngle Starting angle (in degrees) where the arc begins
     * @param sweepAngle Sweep angle (in degrees) measured clockwise
     * @param useCenter If true, include the center of the oval in the arc, and
                        close it if it is being stroked. This will draw a wedge
     * @param paint      The paint used to draw the arc
     */
    public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
            @NonNull Paint paint) {
        drawArc(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, useCenter,
                paint);
    }

startAngle  是起始的角度 ;  canvas的角度的規定是  X正半軸 爲0度 向下爲正 ,即順時針

sweepAngle 是圓弧掃過的角度 也有正負之分    正值表示順時針  負值表示逆時針


結束時是圓環重合   掃過的角度是 -360 , 起始角度是 -360   就是X軸正方向的角度        

轉的過程就是依據進度值更新圓環的起始角度和掃過的角度  :

canvas.drawArc(mRectf, startAngle - 270 * percent, -60 - (300 * percent), false, circlePaint);
 false參數表示是否繪製半徑     若爲true  會從橢圓重心開始繪製兩條半徑  和起點終點圍成一個封閉圖形  ,  percent 表示進度值  

大家都應該注意到了這個顏色的變化   ,是自定義的Evaluator ,根據我傳入的進度值   返回相應的顏色 ,這個類不是我寫的,感興趣的看下:
public class ColorEvaluator {

    private static int mCurrentRed = -1;

    private static int mCurrentGreen = -1;

    private static int mCurrentBlue = -1;

    public static void resetColor() {
        mCurrentBlue = -1;
        mCurrentGreen = -1;
        mCurrentRed = -1;
    }

    public static String evaluate(float fraction, String startColor, String endColor) {
        int startRed = Integer.parseInt(startColor.substring(1, 3), 16);
        int startGreen = Integer.parseInt(startColor.substring(3, 5), 16);
        int startBlue = Integer.parseInt(startColor.substring(5, 7), 16);
        int endRed = Integer.parseInt(endColor.substring(1, 3), 16);
        int endGreen = Integer.parseInt(endColor.substring(3, 5), 16);
        int endBlue = Integer.parseInt(endColor.substring(5, 7), 16);
        // 初始化顏色的值
        if (mCurrentRed == -1) {
            mCurrentRed = startRed;
        }
        if (mCurrentGreen == -1) {
            mCurrentGreen = startGreen;
        }
        if (mCurrentBlue == -1) {
            mCurrentBlue = startBlue;
        }
        // 計算初始顏色和結束顏色之間的差值
        int redDiff = Math.abs(startRed - endRed);
        int greenDiff = Math.abs(startGreen - endGreen);
        int blueDiff = Math.abs(startBlue - endBlue);
        int colorDiff = redDiff + greenDiff + blueDiff;
        if (mCurrentRed != endRed) {
            mCurrentRed = getCurrentColor(startRed, endRed, colorDiff, 0,
                    fraction);
        } else if (mCurrentGreen != endGreen) {
            mCurrentGreen = getCurrentColor(startGreen, endGreen, colorDiff,
                    redDiff, fraction);
        } else if (mCurrentBlue != endBlue) {
            mCurrentBlue = getCurrentColor(startBlue, endBlue, colorDiff,
                    redDiff + greenDiff, fraction);
        }
        // 將計算出的當前顏色的值組裝返回
        String currentColor = "#" + getHexString(mCurrentBlue)
                + getHexString(mCurrentGreen) + getHexString(mCurrentRed);             //  這裏做了一些修改
        //    正確的顏色組裝是  red  green  blue
        return currentColor;
    }

    /**
     * 根據fraction值來計算當前的顏色。
     */
    private static int getCurrentColor(int startColor, int endColor, int colorDiff,
                                       int offset, float fraction) {
        int currentColor;
        if (startColor > endColor) {
            currentColor = (int) (startColor - (fraction * colorDiff - offset));
            if (currentColor < endColor) {
                currentColor = endColor;
            }
        } else {
            currentColor = (int) (startColor + (fraction * colorDiff - offset));
            if (currentColor > endColor) {
                currentColor = endColor;
            }
        }
        return currentColor;
    }

    /**
     * 將10進制顏色值轉換成16進制。
     */
    private static String getHexString(int value) {
        String hexString = Integer.toHexString(value);
        if (hexString.length() == 1) {
            hexString = "0" + hexString;
        }
        return hexString;
    }

}


看第二個流程:

需要注意的是 ,每次調用onDraw方法  之前在畫布上已經繪製的內容將全部清空   然後進行繪製

先看右側的方塊,因爲我們只需要知道方塊飛行時在圓弧上對應的扇形的角度  ,左側的方塊對應的角度是一樣的  。



drawLine方法:

    /* @param startX The x-coordinate of the start point of the line
     * @param startY The y-coordinate of the start point of the line
     * @param paint  The paint used to draw the line
     */
    public void drawLine(float startX, float startY, float stopX, float stopY,
            @NonNull Paint paint) {
        native_drawLine(mNativeCanvasWrapper, startX, startY, stopX, stopY, paint.mNativePaint);
    }

---------------------------------------------------------------------------------------------------------------------------------------------

當然先得算出大圓弧的半徑 :設置圓環半徑爲R

勾股定理:( R +  X) ^2    +  (2*R)^ 2 =  (2*R + X )^ 2  

得出 :X  =  R / 2   .



然後把座標系移到大圓弧圓心    以此爲座標原點   方便座標計算

canvas.save();
canvas.translate(radius / 2 + strokeWidth, 2 * radius + strokeWidth);

然後就需要用到ValueAnimator 屬性動畫,  去計算從0 度到  最高位置的角度endAngle

endAngle = (float) Math.atan(4f / 3);
然後用我定義一個方法去初始化這個 ValueAnimator

 

public void initAnimatorFlyRect() {
        animation_fly = ValueAnimator.ofFloat(0f, endAngle);
        animation_fly.setDuration(1000);
        animation_fly.setInterpolator(new DecelerateInterpolator());     //  定義了動畫變化的速率
        // AccelerateDecelerateInterpolator表示  在開始和結束的時候減速   中間加速
        // DecelerateInterpolator 表示逐漸減速
        // AccelerateInterpolator  表示一直加速
        // LinearInterpolator    表示勻速

        animation_fly.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  //  角度值斷改變的監聽
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentAngle = (float) animation.getAnimatedValue();
                postInvalidate();
            }
        });
        animation_fly.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {            //  動畫結束的監聽
                super.onAnimationEnd(animation);
                currentAngle = 0f;
                status = drawBall;
                circlePaint.setColor(Color.argb(255, 0, 150, 136));    //   把畫圓的Paint變成 綠色
                postInvalidate();
                post(new Runnable() {
                    @Override
                    public void run() {
                        initAnimationDown();
                        animation_down.start();
                    }
                });
            }
        });
    }

特別注意:  ValueAnimator 必須在UI線程中  即主線程中啓動 ,但是我們當前是開了個子線程去更新UI   所以不滿足在UI線程中 

那怎麼辦??

發現View類中 有個  post(Runnable  action)方法   它API的解釋是這樣的

/**
 * Causes the Runnable to be added to the message queue.
 * The runnable will be run on the user interface thread.
 *
用post方法開的線程會運行在UI線程中  問題解決      

post方法內部是 用mHandler.post 方法  ,具體實現沒向下深究。。



if (progress == 100) {   
    post(new Runnable() { 
        @Override
        public void run() {
            animation_fly.start();
        }
    });
}
在進度值100 的 時候啓動這個Animator   ---  animation_fly

具體的方塊繪製代碼:

public void drawFlyRect(Canvas canvas) {

        float bigX = getMeasuredWidth() / 2 - radius * 3 / 2 + strokeWidth;
        float bigY = getMeasuredHeight() / 2;
        canvas.save();
        canvas.translate(bigX, bigY);//將座標移動到大圓圓心(方塊軌跡所在的那個大圓)
        //  兩個參數分別是平移的 X 軸距離  和  Y  軸距離
        float bigRadius = 5 * radius / 2;//大圓半徑
        //方塊起始端座標     起始點座標就是currentAngle 對應在圓弧上的點   注意這裏我們規定緯度低的爲起始端
        float x1 = (float) (bigRadius * Math.cos(currentAngle));        //  圓弧上的點在X軸上的投影長度
        float x11 = (float) (3 * radius - strokeWidth * 2 - (bigRadius * Math.cos(currentAngle)));
        float y1 = -(float) (bigRadius * Math.sin(currentAngle));       //  圓弧上的點在Y軸上的投影長度
        //方塊末端座標    末端點最難定   它也是圓弧上的一個點    和起始點相連成的直線就是方快
        //  所以起始點 和 末端點 構成的圓弧角度  我們不能用固定的值  畢竟希望這個方塊上升時長度在減小
        float huAngle = (float) (0.15 * endAngle - 0.10 * endAngle * (currentAngle / endAngle));  // 確定方塊的那段弧);
        float x2 = (float) (bigRadius * Math.cos(currentAngle + huAngle));
        float x22 = (float) (3 * radius - strokeWidth * 2 - (bigRadius * Math.cos(currentAngle + huAngle)));
        float y2 = -(float) (bigRadius * Math.sin(currentAngle + huAngle));
        canvas.drawLine(x1, y1, x2, y2, rectPaint);//小方塊,其實是一條直線
        canvas.drawLine(x11, y1, x22, y2, rectPaint);
        canvas.restore();

    }


到頂點的時候,即Animator結束的時候 對應方法   onAnimationEnd() , 裏面改變畫筆的顏色爲綠色   ,第二個過程結束。


看第三個流程:


倆方塊消失,取而代之的是一個綠色圓球  接着開始下落動畫  用ValueAnimator控制下落的縱座標,即高度 ,相信大家應該很熟悉了 ,通過ValueAnimator的ofFloat() 方法計算高度值


Canvas 有繪製圓的方法:

/* @param cx     The x-coordinate of the center of the cirle to be drawn
 * @param  cy     The y-coordinate of the center of the cirle to be drawn
 * @param  radius The radius of the cirle to be drawn
 * @param  paint  The paint used to draw the circle
 */
public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint) {
    native_drawCircle(mNativeCanvasWrapper, cx, cy, radius, paint.mNativePaint);
}

注意這個傳入的畫筆:

Paint ballPaint = new Paint();
ballPaint.setStyle(Paint.Style.FILL);
ballPaint.setColor(Color.argb(255, 0, 150, 136));    //  綠色
ballPaint.setAntiAlias(true);
它的Style是FILL   即實心圓球,如果是STROKE   就成了空心球 


下落的球對應的Animator :

public void initAnimationDown() {
        animation_down = ValueAnimator.ofFloat(ballRadius, radius * 2 + strokeWidth);
        animation_down.setDuration(600);
        animation_down.setInterpolator(new AccelerateInterpolator());
        animation_down.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentPosition = (float) animation.getAnimatedValue();
                postInvalidate();
            }
        });

        animation_down.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);

                status = drawMouthAndEyes;

                currentPosition = radius * 2 + strokeWidth;
                initAnimatorTraslateBall();
                initAnimatorMouth();
                animation_traslate.start();
                animation_mouth.start();
            }
        });
    }
繪製球的代碼   , currentPosition 由估值器提供

canvas.drawCircle(radius * 2 + strokeWidth, currentPosition, ballRadius, ballPaint); 


看第四個流程:

嘴巴和眼睛同時繪製 




上一個動畫留在了圓心處   所以這個動畫我的設計是 之前的墜落到圓心的小球變爲兩個同時向最終位置平移    並且畫筆寬度逐漸變大   

這需要一個ValueAnimator  計算其中一個點(另一個點的值都可以對稱算出)X、Y軸座標 和當前畫筆寬度   三個值總不能用三次ofFloat方法吧

所以想到了ofObject 方法     把這些值存到一個對象中   就乾脆定義一個類

public class PointAndSizeOfEyes {

    private float X;
    private float Y;
    private float eyeRadius;

    public PointAndSizeOfEyes(){
    }

    public PointAndSizeOfEyes(float x, float y, float eyeRadius) {
        X = x;
        Y = y;
        this.eyeRadius = eyeRadius;
    }

    ...........// get  set 方法省略
}


自定義一個  TypeEvaluator  去計算動畫過程的值:   就是重寫evaluate 方法就行  根據fraction進度值  , 去算出當前需要的值

public class PointAndSizeEvaluator implements TypeEvaluator {
    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {
        PointAndSizeOfEyes point_start = (PointAndSizeOfEyes) startValue;
        PointAndSizeOfEyes point_end = (PointAndSizeOfEyes) endValue;

        //  需要注意的是    平移後的座標X 還是 Y 都是變小的   所以  endValue < startValue
        return new PointAndSizeOfEyes()
                .setX(point_start.getX() - (point_start.getX() - point_end.getX()) * fraction)
                .setY(point_start.getY() - (point_start.getY() - point_end.getY()) * fraction)
                .setEyeRadius(point_start.getEyeRadius() + fraction * (point_end.getEyeRadius() - point_start.getEyeRadius()));
    }

}

繪製眼睛的屬性動畫 :

 public void initAnimatorTraslateBall() {
        //     Y軸眼睛的偏移量只有  1/4 radius ,   X軸  1/3 radius
        animation_traslate = ValueAnimator.ofObject(new PointAndSizeEvaluator(), new PointAndSizeOfEyes(currentPosition, currentPosition, ballRadius), new PointAndSizeOfEyes(currentPosition - radius / 3, currentPosition - radius / 4, bigBallRadius));
        animation_traslate.setDuration(1200);
        animation_traslate.setInterpolator(new AccelerateDecelerateInterpolator());
        animation_traslate.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentPoint = (PointAndSizeOfEyes) animation.getAnimatedValue();
                postInvalidate();
            }
        });

        animation_traslate.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
            }
        });
    }


繪製嘴巴的屬性動畫:

public void initAnimatorMouth() {


        //  笑臉所在外切矩形的設定
        //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        //
        //   8/3 = 2    整型相除等於2     必須用   8f/3f = 2.667
        //
        //  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        mRectf_mouth.set(new RectF((4f / 3f) * radius + strokeWidth * 2, (4f / 3f) * radius + strokeWidth, (8f / 3f) * radius + strokeWidth * 2, (8f / 3f) * radius + strokeWidth));

        animation_mouth = ValueAnimator.ofFloat(30 * 1.0f, 110 * 1.0f);    //   這裏計算的是 嘴巴逆時針增長的角度   從小到大
        animation_mouth.setDuration(1200);
        animation_mouth.setInterpolator(new DecelerateInterpolator());
        animation_mouth.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentMouthAngle = (float) animation.getAnimatedValue();
                postInvalidate();
            }
        });
        animation_mouth.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
            }
        });
    }


這兩個動畫需要同時啓動 

然後 onDraw 方法中 繪製:

canvas.drawCircle(currentPoint.getX() + strokeWidth, currentPoint.getY(), currentPoint.getEyeRadius(), ballPaint);  //   繪製左邊的眼睛
canvas.drawCircle(2 * currentPosition - currentPoint.getX() + strokeWidth, currentPoint.getY(), currentPoint.getEyeRadius(), ballPaint);  //  繪製右邊的眼睛
canvas.drawArc(mRectf_mouth, 145, -currentMouthAngle, false, mouthPaint);  // 繪製嘴巴  


到此整個動畫完成 , 有疑問的可以留言,撤。。。。。
















發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章