深入瞭解一些Android動畫

一、PropertyValuesHolder

閱讀本文需要上一文Android屬性動畫的基礎,這樣纔可以明白接下來要講什麼。

1.理解和使用

PropertyValuesHolder 是ObjectAnimation類似的一個方法,只是少了一個target,就是要執行的控件。看看正常的使用方法:會同時執行全部的Holder

public void  doPropertyValuesHolder(){
        //定義一個旋轉Holder
        PropertyValuesHolder rotationHolder=
                PropertyValuesHolder.ofFloat(
                        "rotation",
                        60f,40f,100f,-60f,40f,88f,77f);

        //定義一個透明Holder
        PropertyValuesHolder alphaHolder=
                PropertyValuesHolder.ofFloat(
                        "alpha",
                        0.01f,0.5f,1.0f,0.8f,0.2f,0.0f);
        
    	//加載進ObjectAnimator
        ObjectAnimator objectAnimator=ObjectAnimator.ofPropertyValuesHolder(ballImageView,rotationHolder,alphaHolder);
        objectAnimator.setDuration(3000);
        objectAnimator.start();
    }

2.方法和參數

可以看看這個方法的參數:

ObjectAnimator ofPropertyValuesHolder(Object target,PropertyValuesHolder... values)
PropertyValuesHolder ofFloat(String propertyName, float... values)

Object target 是要顯示動畫的控件

PropertyValuesHolder... values 裝載多個PropertyValuesHolder

String propertyName 代表要反射的參數,跟ObjectAnimation的參數是一樣的

float... values 代表是可變長參數
這樣的方法還有以下圖片這些:

圖片.png

其中ofObject()方法 ,也是跟ObjectAnimation的相似,也是要自定義TypeEvaluator。
圖片.png

二、Keyframe

1.理解和使用

看名字,就是理解爲關鍵幀的意思,在動畫中,在某幀做一些操作,從而實現對比效果比較明顯的效果。
關鍵幀表示是某個物體在哪個時間點應該在哪個位置上。
具體使用:

 public void  doPropertyValuesHolderKeyFrame(){

        //頭keyframe1,從進度0.6開始,在進度60%的時候,數值是0.1f
        Keyframe keyframe1=Keyframe.ofFloat(0.6f,0.1f);

        //中間keyframe2
        Keyframe keyframe2=Keyframe.ofFloat(0.1f,0.8f);

        //尾部keyframe3,以50%進度作爲結束,這時候的數值爲0.2f
        Keyframe keyframe3=Keyframe.ofFloat(0.5f,0.2f);

        //裝載到Holder中,並設置要反射的方法,這是反射的是setAlpha()方法,控制透明度
        PropertyValuesHolder alphaHolder=PropertyValuesHolder.ofKeyframe("alpha",keyframe1,keyframe2,keyframe3);

        //把裝載到Holder中裝載到ObjectAnimator或者ValueAnimation
        ObjectAnimator objectAnimator=ObjectAnimator.ofPropertyValuesHolder(ballImageView,alphaHolder);
        objectAnimator.setDuration(3000);
        objectAnimator.start();
    }

2.方法和參數

Keyframe ofFloat(float fraction, float value)

float fraction 表示進度

float value 表示在這個進度下的數值

 PropertyValuesHolder ofKeyframe(String propertyName, Keyframe... values)

String propertyName 要反射的set方法

Keyframe... values 傳入Keyframe

Keyframe的方法,也是和其他的類似的。
圖片.png

Keyframe的set方法,設置進度,插值器,數值。
沒有設置插值器的時候,默認是線性插值器

 keyframe1.setInterpolator(new LinearInterpolator()); //默認線性插值器

圖片.png

3.幀的操作

直接寫結論:

  • 如果去掉0幀,則以第一個關鍵幀爲起始位置
  • 如果去掉結束幀(進度爲1),則以最後一個關鍵幀爲結束位置
  • 使用keyframe來構建動畫,至少需要2幀

三、ViewPropertyAnimator

1.理解和使用

可以通過串行的形式,快速定義動畫,省去一些定義,在每次界面繪製的時候,啓動動畫,比其他的更節省消耗。
比如:

 ballImageView.animate().alpha(0.5f).rotation(360f).scaleX(1.5f).translationX(100f);

2.參數和方法

可以看到這些方法的返回值,基本都是ViewPropertyAnimator
圖片.png
圖片.png
再引用一張表格:

函數 含義
alpha(float value) 設置透明度
scaleY(float value) 設置 Y軸方向的縮放大小
scaleX(float value) 設置X軸方向的縮放大小
translationY(float value) 設置Y軸方向的移動值
translationX(float value) 設置X軸方向的移動值
rotation(float value) 設置繞Z軸旋轉度數
rotationX(float value) 設置繞x軸旋轉度數
rotationY(float value) 設置繞 Y 軸旋轉度數
x(float value) 相對於父容器的左上角座標在 X軸方向的最終位置
y(float value) 相對於父容器的左上角座標在Y軸方向的最終位置
alphaBy(float value) 設置透明度增量
rotationBy(float value) 設置繞Z軸旋轉增量
rotationXBy(float value) 設置繞 X 油旋轉增量
rotationYBy(float value) 設置統Y軸旋轉增量
translationXBy(float value) 設置X軸方向的移動值增量
translationYBy(float value) 設置Y軸方向的移動值增量
scaleXBy(float value) 設置X軸方向的縮放大小增量
scaleYBy(float value) 設置 Y軸方向的縮放大小增量
xBy(float value) 相對於父容器的左上角座標在 X軸方向的位置增量
yBy(float value) 相對於父容器的左上角座標在 Y軸方向的位置增量
setlnterpolator(Timelnterpolator interpolator) 設置插值器
setStartDelay(long startDelay) 設置開始延時
setDuration(long duration) 設置動畫時長

四、animateLayoutChanges

android:animateLayoutChanges="true"
在Layout加入控件,或者移除控件的時候,添加動畫,但是隻能使用默認動畫。

 <LinearLayout
            android:animateLayoutChanges="true"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"/>

五、LayoutTransition

LayoutTransition可以控制ViewGroup的動畫,可以使用自定義的動畫。
具體使用:

 public void doLayoutTransition(){

        LinearLayout linearLayout=new LinearLayout(this);

        //1.創建實例
        LayoutTransition transition=new LayoutTransition();

        //2.創建動畫
        ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(null,"rotation",0f,90f,0f);

        //3.動畫出現形式進行設置
        transition.setAnimator(LayoutTransition.DISAPPEARING,objectAnimator);

        //4.將LayoutTransition設置到ViewGroup中
        linearLayout.setLayoutTransition(transition);
     
      	//5.開源動畫庫 NineOldAndroids
     
    }

setAnimator(int transitionType, Animator animator)

這個方法中,transitionType有五個選項

image.png

CHANGE_APPEARING 由於容器中要顯示一個新的元素,其他需要變化的元素所應用的動畫(問題多,不常用)

_CHANGE_DISAPPEARING_ 當個容器中某個元素要消失時,其他需要變化的元素所應用的動畫(問題多,不常用)

_CHANGING_ 容器中正在更改的元素的動畫變化

_APPEARING_ 元素在容器中出現時所定義的動畫

_DISAPPEARING_ 元素在容器中消失時所定義的動畫

六、PathMeasure

PathMeasure類似一個計算器,可以計算出目標path的座標,長度等

1.初始化

  public void doPathMeasure(){
        Path path=new Path();

        //初始化方法1
        PathMeasure pathMeasure1=new PathMeasure();
        pathMeasure1.setPath(path,true);

        //初始化方法2
        PathMeasure pathMeasure2=new PathMeasure(path,false);
    }

setPath(Path path, boolean forceClosed)
path 就是代表要計算的目標Path。
forceClosed 是否閉合,true會計算閉合狀態下的Path,false會按照Path原來情況來計算。

2.函數調用

自定義一個view

public class PathView extends View {
    Path mPath;
    Paint mPaint;
    PathMeasure mPathMeasure;

    public PathView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mPath=new Path();
        mPaint=new Paint();
        mPathMeasure=new PathMeasure();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.translate(250,250); //畫布移動
        mPaint.setColor(Color.BLUE); //畫筆顏色
        mPaint.setStrokeWidth(5); //畫筆粗細
        mPaint.setStyle(Paint.Style.STROKE); //畫筆風格

        mPath.moveTo(0,0);
        mPath.lineTo(0,100);
        mPath.lineTo(100,100);
        mPath.lineTo(100,0);

        mPathMeasure.setPath(mPath,true);
        Log.v("showLog",
                "getLength()=="+mPathMeasure.getLength()
                        +"  isClosed()=="+ mPathMeasure.isClosed()); //結果400.0  true

        mPathMeasure.setPath(mPath,false);
        Log.v("showLog",
                "getLength()=="+mPathMeasure.getLength()
                        +"  isClosed()=="+ mPathMeasure.isClosed()); //結果300.0  false

        canvas.drawPath(mPath,mPaint); //繪製路徑
    }
}

繪製效果:

image.png

2.1 PathMeasure.getLength()

PathMeasure.getLength() 函數用於測量路徑的長度

2.2 PathMeasure.isClosed()

PathMeasure.isClosed() 函數用於返回是否測量閉合狀態

2.3 PathMeasure.nextContour()

   		mPath.addRect(-50, -50, 50, 50, Path.Direction.CW);
        canvas.drawPath(mPath, mPaint);

        mPath.addRect(-100, -100, 100, 100, Path.Direction.CW);
        canvas.drawPath(mPath, mPaint);

        mPath.addRect(-120, -120, 120, 120, Path.Direction.CW);
        canvas.drawPath(mPath, mPaint);

        mPathMeasure.setPath(mPath, false);

        do {
            float len = mPathMeasure.getLength();
            Log.v("showLog", "len=" + len);
        } while (mPathMeasure.nextContour());

效果:

image.png

打印結果:

len=400.0
len=800.0
len=960.0

PathMeasure.nextContour()得到的順序與添加的Path的順序相同

PathMeasure.getLength()只是得到當前path的長度,不是全部的長度

2.3 getSegment()

使用getSegment函數需要禁用硬件加速 在構造方法中加入
setLayerType(LAYER_TYPE_SOFTWARE,null);

 		mPath.addRect(-50, -50, 50, 50, Path.Direction.CW);
        mPathMeasure.setPath(mPath,false); //計算的path
        mPathMeasure.getSegment(0,150,mDstPath,true); //截取並添加到mDstPath,是添加,不是其他
        canvas.drawPath(mPath, mPaint); //繪製原來的path

        canvas.translate(200,0); //畫布移動
        mPaint.setColor(Color.RED);
        canvas.drawPath(mDstPath, mPaint); //繪製添加後的mDstPath
 boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)

startDpath開始截取的點,截取的起始點,是以左上角的點開始的

stopD截取停止的點

dst截取後添加到的path

startWithMoveTo是否保存原狀,true保存原樣,false則會連接初始點和終點,和原來的不一定相同形狀
以上代碼的效果: 截圖的方向,與原來的path的生成方向有關

image.png

2.4 動態畫圓的例子

代碼:


public class PathView extends View {
    Path mPath, mDstPath;
    Paint mPaint;
    PathMeasure mPathMeasure;
    float mCurAnimValue;

    public PathView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mPath = new Path();
        mDstPath = new Path();
        mPaint = new Paint();
        mPathMeasure = new PathMeasure();

        mPaint.setColor(Color.BLUE); //畫筆顏色
        mPaint.setStrokeWidth(5); //畫筆粗細
        mPaint.setStyle(Paint.Style.STROKE); //畫筆風格

        mPath.addCircle(100, 100, 50, Path.Direction.CW); //一個完整的圓
        mPathMeasure.setPath(mPath, true); //要計算的path

        ValueAnimator animator = ValueAnimator.ofFloat(0, 1); //進度 0~1
        animator.setRepeatCount(ValueAnimator.INFINITE); //無限循環
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurAnimValue = (Float) animation.getAnimatedValue(); //得到當前的進度
                invalidate();//重繪,重新執行onDraw()方法
            }
        });
        animator.setDuration(5000);
        animator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(100, 100); //畫布移動

        float stop=mPathMeasure.getLength()*mCurAnimValue; //一個進度確定一個截取點
        mDstPath.reset();
        mPathMeasure.getSegment(0,stop,mDstPath,true); //一點點添加

        canvas.drawPath(mDstPath,mPaint); //每次有進度更新,就繪製一小段截取
    }
}

效果:
動態畫圓.gif

2.5 getPosTan()

先看看函數的定義:

boolean getPosTan(float distance, float pos[], float tan[]) 

float distance 距離path的其實長度

float pos[] 該點的座標值。x和y pos[0]=x,pos[1]=y

float tan[] 該點的正切值。x和y pos[0]=x,pos[1]=y tan<a=y/x

2.6 箭頭畫圓的例子

代碼:


public class PathView extends View {
    Path mPath, mDstPath;
    Paint mPaint;
    PathMeasure mPathMeasure;
    float mCurAnimValue;
    Bitmap mArrowBmp;
    float[] mPos;
    float[] mTan;
    int mCenterX,mCenterY;
    float mRadius;

    public PathView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mPath = new Path();
        mDstPath = new Path();
        mPaint = new Paint();
        mPathMeasure = new PathMeasure();
        mPos=new float[2];
        mTan=new float[2];

        //加載箭頭圖片
        mArrowBmp= BitmapFactory.decodeResource(getResources(), R.drawable.arrow);

        mPaint.setColor(Color.BLUE); //畫筆顏色
        mPaint.setStrokeWidth(5); //畫筆粗細
        mPaint.setStyle(Paint.Style.STROKE); //畫筆風格

        mPath.addCircle(540, 972, 486, Path.Direction.CW); //一個完整的圓
        mPathMeasure.setPath(mPath, true); //要計算的path

        ValueAnimator animator = ValueAnimator.ofFloat(0, 1); //進度 0~1
        animator.setRepeatCount(ValueAnimator.INFINITE); //無限循環
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurAnimValue = (Float) animation.getAnimatedValue(); //得到當前的進度
                invalidate();//重繪,重新執行onDraw()方法
            }
        });
        animator.setDuration(5000);
        animator.start();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {

        /*
         * 得到h,w的最小的那個值;
         * >> 1 移位 跟 /2 相同;
         * 乘以0.9f,表示佔佈局的90%
         * */
        mRadius = (Math.min(h, w) >> 1) * 0.9f;

        // 中心座標
        mCenterX = w / 2;
        mCenterY = h / 2;
        Log.v("showLog",mCenterX+"  "+mCenterY+"  "+mRadius);
        postInvalidate();
        super.onSizeChanged(w, h, oldw, oldh);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        float stop=mPathMeasure.getLength()*mCurAnimValue; //一個進度確定一個截取點

        mDstPath.reset();
        mPathMeasure.getSegment(0,stop,mDstPath,true); //一點點添加

        canvas.drawPath(mDstPath,mPaint); //每次有進度更新,就繪製一小段截取

        mPathMeasure.getPosTan(stop,mPos,mTan); //獲得每點的正切值和座標

        /**
         *    Math.atan2(mTan[1],mTan[0])獲得tan的弧度值
         *    *180.0/Math.PI將轉化爲角度值
         * */
        float degrees=(float)(Math.atan2(mTan[1],mTan[0])*180.0/Math.PI);

        Matrix matrix=new Matrix();

        /**
         * 將圖片圍繞中心點旋轉指定角度
         * postRotate(float degrees, float px, float py)
         * degrees是角度  (px,py)是圖片中心點
         * */
        matrix.postRotate(degrees,mArrowBmp.getWidth()/2,mArrowBmp.getHeight()/2);

        /**
         * 將圖片從默認的(0,0)點移動到路徑的最前端
         * */
        matrix.postTranslate(mPos[0]-mArrowBmp.getWidth()/2,mPos[1]-mArrowBmp.getHeight()/2);

        //繪製圖片
        canvas.drawBitmap(mArrowBmp,matrix,mPaint);

    }
}

效果:

箭頭動態畫圓.gif

2.7 getMatrix()

參數類型:

boolean getMatrix(float distance, Matrix matrix, int flags)

使用方法:

  		//計算方位角
        Matrix matrix = new Matrix();

		//獲取位置信息
        mPathMeasure.getMatrix(stop,matrix,PathMeasure.POSITION_MATRIX_FLAG);

		//獲取切邊信息
        mPathMeasure.getMatrix(stop,matrix,PathMeasure.TANGENT_MATRIX_FLAG); 

2.8 支付成功例子

public class TickView extends View {
    Path mPath, mDstPath;
    Paint mPaint;
    PathMeasure mPathMeasure;
    float mCurAnimValue;
    int mCenterX, mCenterY;
    float mRadius;

    public TickView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mPath = new Path();
        mDstPath = new Path();
        mPaint = new Paint();
        mPathMeasure = new PathMeasure();

        mPaint.setColor(Color.BLUE); //畫筆顏色
        mPaint.setStrokeWidth(5); //畫筆粗細
        mPaint.setStyle(Paint.Style.STROKE); //畫筆風格

        mCenterX = 540;
        mCenterY = 972;
        mRadius = 486 / 2;

        /**
         * 圓
         * */
        mPath.addCircle(mCenterX, mCenterY, mRadius, Path.Direction.CW);

        /**
         * 對勾
         * */
        mPath.moveTo(mCenterX - mRadius / 2, mCenterY);
        mPath.lineTo(mCenterX, mCenterY + mRadius / 2);
        mPath.lineTo(mCenterX + mRadius / 2, mCenterY - mRadius / 3);

        mPathMeasure.setPath(mPath, false); //要計算的path

        ValueAnimator animator = ValueAnimator.ofFloat(0, 2); //進度 0~1 是圓,1~2是對勾
        animator.setRepeatCount(ValueAnimator.RESTART);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurAnimValue = (Float) animation.getAnimatedValue(); //得到當前的進度
                invalidate();//重繪,重新執行onDraw()方法
            }
        });
        animator.setDuration(5000);
        animator.start();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {

        /*
         * 得到h,w的最小的那個值;
         * >> 1 移位 跟 /2 相同;
         * 乘以0.9f,表示佔佈局的90%
         * */
        mRadius = (Math.min(h, w) >> 1) * 0.9f;

        // 中心座標
        mCenterX = w / 2;
        mCenterY = h / 2;
        Log.v("showLog", mCenterX + "  " + mCenterY + "  " + mRadius);
        postInvalidate();
        super.onSizeChanged(w, h, oldw, oldh);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (mCurAnimValue < 1) {
            float stop = mPathMeasure.getLength() * mCurAnimValue;
            mPathMeasure.getSegment(0, stop, mDstPath, true);
        } else if (mCurAnimValue == 1) {
            mPathMeasure.getSegment(0, mPathMeasure.getLength(), mDstPath, true);
            mPathMeasure.nextContour();
        } else {
            float stop = mPathMeasure.getLength() * (mCurAnimValue - 1);
            mPathMeasure.getSegment(0, stop, mDstPath, true);
        }

        canvas.drawPath(mDstPath, mPaint);
    }
}

效果:
對勾動畫.gif

編程中我們會遇到多少挫折?表放棄,沙漠盡頭必是綠洲。

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