Android 屬性動畫:實現小球墜落

一、要做什麼

項目需要實現的效果:小球墜落


 1. 首先繪製小球--自定義View 繪製圓;
 2. 模擬小球墜落--屬性動畫,重繪小球軌跡;
 3. 修改小球顏色--實現自定義TypeEvaluator;

實現的簡單效果如下:

這裏寫圖片描述

二、思考怎麼做

實現步驟如下:

1、自定義 AnimPointView:

/**
 * Created by Troy on 2017/3/20.
 *
 * 通過對對象進行值操作來實現動畫效果的功能,這就是ValueAnimator的高級用法
 */
public class AnimPointView extends View {

    public static final float sRADIUS = 20F;
    private Point mCurrentPoint;
    private Paint mPaint;
    private Paint mTextPaint;

    //動畫持續時間 默認5S
    private int mAnimDuration;
    private int mDefaultAnimDuration = 5;

    //小球序號
    private String mBallText;
    private String mDefaultBallText = "1";

    //初始顏色
    private String mBallStartColor;
    private String mDefaultBallStartColor = "#0000FF";

    //結束顏色
    private String mBallEndColor;
    private String mDefaultBallEndColor = "#FF0000";

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

    public AnimPointView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //自定義屬性
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Ball);
        mAnimDuration = typedArray.getInt(R.styleable.Ball_anim_duration, mDefaultAnimDuration);
        mBallText = typedArray.getString(R.styleable.Ball_ball_text);
        mBallStartColor = typedArray.getString(R.styleable.Ball_start_color);
        mBallEndColor = typedArray.getString(R.styleable.Ball_end_color);
        if(TextUtils.isEmpty(mBallText)){
            mBallText = mDefaultBallText;
        }
        if(TextUtils.isEmpty(mBallStartColor)){
            mBallStartColor = mDefaultBallStartColor;
        }
        if(TextUtils.isEmpty(mBallEndColor)){
            mBallEndColor = mDefaultBallEndColor;
        }
        //回收typedArray
        typedArray.recycle();
        init();
    }

    public AnimPointView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(){
        //畫圓的畫筆
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);

        //畫文字的畫筆
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setColor(Color.WHITE);
        mTextPaint.setTextSize(sRADIUS);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(mCurrentPoint == null){
            mCurrentPoint = new Point(sRADIUS, sRADIUS);
            drawCircle(canvas);
            startAnimation();
        }else {
            drawCircle(canvas);
        }
    }

    //繪製圓球
    private void drawCircle(Canvas canvas){
        float x = mCurrentPoint.getX();
        float y = mCurrentPoint.getY();
        canvas.drawCircle(x, y, sRADIUS, mPaint);
        canvas.drawText(mBallText, x, y + 5, mTextPaint);
    }

    // 調用了invalidate()方法,這樣的話 onDraw()方法就會重新調用,並且由於currentPoint 對象的座標已經改變了,
    // 那麼繪製的位置也會改變,於是一個平移的動畫效果也就實現了;
    private void startAnimation(){
        //改變小球的位置 ValueAnimator
        Point startPoint = new Point(getWidth() / 2, sRADIUS);
        Point endPoint = new Point(getWidth() / 2, getHeight() - sRADIUS);
        Log.i("TEST", "startPoint:" + startPoint.getX() + "-" + startPoint.getY());
        Log.i("TEST", "endPoint:" + endPoint.getX() + "-" + endPoint.getY());
        ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
        //動畫監聽事件,不斷重繪view

        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurrentPoint = (Point) animation.getAnimatedValue();
                //invalidate() 與 requestLayout()的區別,這個地方也可以用requestLayout();
                invalidate();
            }
        });
        //設置動畫的彈跳差值器
        anim.setInterpolator(new BounceInterpolator());

        //改變小球的顏色 ObjectAnimator
        ObjectAnimator anim2 = ObjectAnimator.ofObject(this, "color", new ColorEvaluator(),
                mBallStartColor, mBallEndColor);

        //組合動畫
        AnimatorSet animSet = new AnimatorSet();
        animSet.play(anim).with(anim2);
        animSet.setDuration(mAnimDuration*1000);
        animSet.start();
    }

    private String color;

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
        mPaint.setColor(Color.parseColor(color));
        invalidate();
    }
}

2、自定義屬性及佈局使用

在attrs.xml 文件中定義屬性:

<declare-styleable name="Ball">
        <attr name="ball_text" format="string"/>
        <attr name="start_color" format="string"/>
        <attr name="end_color" format="string"/>
        <attr name="anim_duration" format="integer"/>
    </declare-styleable>

在activity 佈局中使用:

<com.troy.bargraph.view.AnimPointView
        android:id="@+id/anim_point_view1"
        android:layout_width="0dp"
        android:layout_weight="1.2"
        android:layout_height="match_parent"
        app:ball_text="1"        //小球序號
        app:end_color="#66CDAA"  //結束顏色
        app:anim_duration="6"/>  //開始顏色

3、小球位置估值器

public class PointEvaluator implements TypeEvaluator {
    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {
    //fraction 與時間有關的係數,該值由差值器計算得出,由ValueAnimator調用 animateValue 
        Point startPoint = (Point)startValue;
        Point endPoint = (Point)endValue;
        float x = startPoint.getX() + fraction*(endPoint.getX() - startPoint.getX());
        float y = startPoint.getY() + fraction*(endPoint.getY() - startPoint.getY());
        return new Point(x, y);
    }
}

4、關於 evaluate 方法中fraction 因子的值來源

首先應該明白差值器的概念和基本使用,我們一般在代碼裏給動畫設置一個差值器:

anim.setInterpolator(new BounceInterpolator());

如果沒有設置差值器,系統默認使用加速減速差值器:

// The time interpolator to be used if none is set on the animation
    private static final TimeInterpolator sDefaultInterpolator =
            new AccelerateDecelerateInterpolator();

如果設置 null ,系統默認使用線性差值器:

/**
     * 1、interpolator 的作用:The time interpolator used in calculating the elapsed fraction of this animation. The
     * 2、差值器的賦值:interpolator determines whether the animation runs with linear or non-linear motion,
     * such as acceleration and deceleration. The default value is
     * {@link android.view.animation.AccelerateDecelerateInterpolator}
     *
     * @param value the interpolator to be used by this animation. A value of <code>null</code>
     * will result in linear interpolation.
     */
    @Override
    public void setInterpolator(TimeInterpolator value) {
        if (value != null) {
            mInterpolator = value;
        } else {
            // 當設置 null 時,使用線性差值器
            mInterpolator = new LinearInterpolator();
        }
    }

看ValueAnimator 的源碼可知 fraction 是由差值器計算出來的:

float fraction = mInterpolator.getInterpolation(fraction);
//getInterpolation 是父接口的方法,具體實現在子類中;

Interpolator 的直接子類如下:

這裏寫圖片描述

我們看最簡單的線性差值器的實現:

/**
 * An interpolator where the rate of change is constant
 */
@HasNativeInterpolator
public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {

    public LinearInterpolator() {
    }

    public LinearInterpolator(Context context, AttributeSet attrs) {
    }

    public float getInterpolation(float input) {
        return input; //輸入什麼返回什麼;
    }

    /** @hide */
    @Override
    public long createNativeInterpolator() {
        return NativeInterpolatorFactoryHelper.createLinearInterpolator();
    }
}

4、顏色改變估值器

public class ColorEvaluator implements TypeEvaluator {

    //將十六進制的顏色表示切割成三段,分別爲紅色段、綠色段、藍色段,分別計算其隨時間改變而對應的值;
    private int mCurrentRed = -1;

    private int mCurrentGreen = -1;

    private int mCurrentBlue = -1;

    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {

        String startColor = (String) startValue;
        String endColor = (String) endValue;

        // Integer.parseInt(String s ,int radix)方法: 輸出一個十進制數; radix 表示原來的進制;
        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(mCurrentRed)
                + getHexString(mCurrentGreen) + getHexString(mCurrentBlue);
        return currentColor;
    }

    /**
     * 根據fraction 值來計算當前的顏色。
     */
    private 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 String getHexString(int value) {
        String hexString = Integer.toHexString(value);
        if (hexString.length() == 1) {
            hexString = "0" + hexString;
        }
        return hexString;
    }

}

基本就是上面一些內容。

三、總結:

做完這個DEMO,應該掌握的知識點如下:

  1. View的知識點;重繪View 有 invalidate() 與 requestLayout();二者的區別。
  2. 常見的幾種估值器 TypeEvaluator ,及如果根據需求自定義 TypeEvaluator ;
  3. 常見的差值器 Interpolator;
  4. fraction 因子值的計算規則;

參考致謝:
(1)郭霖 http://blog.csdn.net/guolin_blog/article/details/44171115

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