自定義屬性動畫框架

通過本篇文章秽之,你將會了解

  • 安卓屬性動畫的基本架構(gòu)
  • 插值器和估值器在動畫中的作用
  • 手擼屬性動畫

設(shè)想一下,如果你是google的工程師,讓你去設(shè)計一個屬性動畫玄货,你該如何設(shè)計?在設(shè)計屬性動畫時我們應(yīng)該要考慮哪些問題悼泌?

  • 生成動畫的api調(diào)用約簡單越好
  • 一個View可以有多個動畫松捉,但同時只能有一個在運行
  • 動畫的執(zhí)行不能依賴自身的for循環(huán)
  • 如何讓動畫動起來

我們先來看下屬性動畫的種類

  • 平移動畫
  • 透明度動畫
  • 縮放動畫
  • 旋轉(zhuǎn)動畫
  • 幀動畫

屬性動畫的使用

ObjectAnimator animator = ObjectAnimator.ofFloat(view,"scale",1f,2f,3f);
animator.setInterpolator(new LinearInterpolator());
animator.setDuration(500);
animator.start() 

動畫的本質(zhì)

???? 動畫實際上是改變View在某一時間點上的樣式屬性,比如在0.1s的時候View的x坐標為50px馆里,在0.2s的時候View的x坐標變?yōu)?50px隘世,在0.3s的時候View的x坐標變?yōu)?50px,肉眼看就會感覺View在向右移動鸠踪。

????實際上是通過一個線程每隔一段時間通過調(diào)用view.setX(index++)來改變屬性值產(chǎn)生動畫效果丙者。

????動畫實際上是一個復(fù)雜的流程,需要考慮的因素比較多营密,在開發(fā)者層面不建議直接調(diào)用view.setX()來實現(xiàn)動畫械媒。

動畫架構(gòu)分析

image.png

??????根據(jù)上面的架構(gòu)圖,我們將動畫任務(wù)拆成若干個關(guān)鍵幀,每個關(guān)鍵幀在不同的時間點執(zhí)行自己的動畫纷捞,最終將整個動畫完成痢虹,但每兩個關(guān)鍵幀之間是有時間間隔的,我們要實現(xiàn)一個補幀的操作來過渡兩個關(guān)鍵幀動畫主儡,使動畫看起來銜接平滑自然奖唯。
??????這里可能大家會有一個疑問:為什么要將動畫分解成不同的關(guān)鍵幀?原因是動畫完成是需要時間開銷的缀辩。如果不給出關(guān)鍵幀動畫臭埋,動畫的過程將無法控制,而且在不同的時間點臀玄,控件的狀態(tài)也不一樣瓢阴。

代碼設(shè)計架構(gòu)圖

image.png

擼代碼

1、首先我們來模擬VSync信號健无,每隔16ms發(fā)送一個信號去遍歷animationFrameCallbackList執(zhí)行動畫Callback荣恐,定義一個VSyncManager類來模擬

public class VSyncManager {
    private List<AnimationFrameCallback> list = new ArrayList<>();

    public static VSyncManager getInstance() {
        return Holder.instance;
    }

    private VSyncManager() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(16);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    for (AnimationFrameCallback animationFrameCallback : list) {
                        animationFrameCallback.doAnimationFrame(System.currentTimeMillis());
                    }
                }
            }
        }).start();
    }

    interface AnimationFrameCallback {
        boolean doAnimationFrame(long currentTime);
    }

    public void add(AnimationFrameCallback animationFrameCallback) {
        list.add(animationFrameCallback);
    }

    static class Holder {
        static final VSyncManager instance = new VSyncManager();
    }
}

定義一個時間插值器TimeInterpolator

public interface TimeInterpolator {
    float getInterpolator(float input);
}

創(chuàng)建一個線性插值器LinearInterpolator實現(xiàn)TimeInterpolator,插值器的輸出我們定義為輸入的一般累贤,你可以設(shè)置你想要的任何值

public class LinearInterpolator implements TimeInterpolator {
    @Override
    public float getInterpolator(float input) {
        return 0.5f*input;
    }
}

接著定義我們的關(guān)鍵幀實體類MyFloatKeyFrame叠穆,主要用來存儲三個屬性:當前動畫執(zhí)行的進度百分比,當前幀對應(yīng)的View的屬性值臼膏,當前幀對應(yīng)的屬性值的類型

public class MyFloatKeyFrame {
    //當前的百分比
    float fraction;
    //當前幀對應(yīng)的屬性值
    float mValue;
    //當前幀對應(yīng)得值得類型
    Class mValueType;

    public MyFloatKeyFrame(float fraction, float mValue) {
        this.fraction = fraction;
        this.mValue = mValue;
        mValueType = float.class;
    }

    public float getValue() {
        return mValue;
    }

    public void setValue(float mValue) {
        this.mValue = mValue;
    }

    public float getFraction() {
        return fraction;
    }

    public void setFraction(float fraction) {
        this.fraction = fraction;
    }
}

再接著定義關(guān)鍵幀集合硼被,用來初始化關(guān)鍵幀信息并且返回對應(yīng)的View的屬性值

public class MyKeyframeSet {
    //類型估值器
    TypeEvaluator mEvaluator;
    List<MyFloatKeyFrame> mKeyFrames;

    public MyKeyframeSet(MyFloatKeyFrame... keyFrame) {
        this.mEvaluator = new FloatEvaluator();
        mKeyFrames = Arrays.asList(keyFrame);
    }

    //關(guān)鍵幀初始化
    public static MyKeyframeSet ofFloat(float[] values) {
        if (values.length <= 0) {
            return null;
        }
        int numKeyframes = values.length;
        //循環(huán)賦值
        MyFloatKeyFrame keyFrame[] = new MyFloatKeyFrame[numKeyframes];
        keyFrame[0] = new MyFloatKeyFrame(0, values[0]);
        for (int i = 1; i < numKeyframes; i++) {
            keyFrame[i] = new MyFloatKeyFrame((float) i / (numKeyframes - 1), values[i]);
        }
        return new MyKeyframeSet(keyFrame);
    }

    //獲取當前百分比對應(yīng)得具體屬性值
    public Object getValue(float fraction) {
        MyFloatKeyFrame prevKeyFrame = mKeyFrames.get(0);
        for (int i = 0; i < mKeyFrames.size(); i++) {
            MyFloatKeyFrame nextKeyFrame = mKeyFrames.get(i);
            if (fraction < nextKeyFrame.getFraction()) {
                //當前百分比在此之間
                //計算間隔百分比
                float intervalFraction = (fraction - prevKeyFrame.getFraction())
                        / (nextKeyFrame.getFraction() - prevKeyFrame.getFraction());
                //通過估值器返回對應(yīng)得值
                return mEvaluator == null ?
                        prevKeyFrame.getValue() + intervalFraction * (nextKeyFrame.getValue() - prevKeyFrame.getValue()) :
                        ((Number) mEvaluator.evaluate(intervalFraction, prevKeyFrame.getValue(), nextKeyFrame.getValue())).floatValue();
            }
            prevKeyFrame = nextKeyFrame;
        }
        //對應(yīng)得幀不夠
        return mKeyFrames.get(mKeyFrames.size() - 1).getValue();
    }
}

根據(jù)當前動畫執(zhí)行進度百分比fraction獲取對應(yīng)得具體屬性值的相關(guān)計算邏輯可以參考下圖


image.png

接下來我們來定義動畫任務(wù)屬性值管理類MyFloatPropertyValuesHolder,主要作用是通過反射獲取控件對應(yīng)的方法渗磅,然后通過調(diào)用該方法(如setScale)給控件設(shè)置相應(yīng)的屬性值

public class MyFloatPropertyValuesHolder {
    //屬性名
    String mPropertyName;
    //屬性類型 float
    Class mValueType;
    //反射
    Method mSetter = null;
    //關(guān)鍵幀管理類
    MyKeyframeSet mKeyframeSet;

    public MyFloatPropertyValuesHolder(String propertyName, float... values) {
        this.mPropertyName = propertyName;
        mValueType = float.class;
        //交給關(guān)鍵幀管理初始化
        mKeyframeSet = MyKeyframeSet.ofFloat(values);
    }

    //通過反射獲取控件對應(yīng)的方法
    public void setupSetter() {
        char firstLetter = Character.toUpperCase(mPropertyName.charAt(0));
        String theRest = mPropertyName.substring(1);
        //setScaleX
        String methodName = "set" + firstLetter + theRest;
        try {
            mSetter = View.class.getMethod(methodName, float.class);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    //給控件設(shè)置相應(yīng)的屬性值
    public void setAnimatedValue(View view, float fraction) {
        Object value = mKeyframeSet.getValue(fraction);
        try {
            mSetter.invoke(view, value);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

最后定義我們對開發(fā)者暴露的MyObjectAnimator類嚷硫,功能類似Android源碼的的ObjectAnimator類,給開發(fā)人員調(diào)用設(shè)置屬性動畫的api

public class MyObjectAnimator implements VSYNCManager.AnimationFrameCallback {
    //動畫時長
    private long mDuration = 0;
    //需要執(zhí)行動畫的對象
    private WeakReference<View> mTarget;
    //屬性值管理類
    private MyFloatPropertyValuesHolder mFloatPropertyValuesHolder;
    private int index = 0;
    private TimeInterpolator interpolator;


    public long getDuration() {
        return mDuration;
    }

    public void setDuration(long mDuration) {
        this.mDuration = mDuration;
    }

    public int getIndex() {
        return index;
    }

    public void setIndex(int index) {
        this.index = index;
    }

    public TimeInterpolator getInterpolator() {
        return interpolator;
    }

    public void setInterpolator(TimeInterpolator interpolator) {
        this.interpolator = interpolator;
    }


    public MyObjectAnimator(View target, String propertyName, float... values) {
        mTarget = new WeakReference<>(target);
        mFloatPropertyValuesHolder = new MyFloatPropertyValuesHolder(propertyName, values);
    }

    public static MyObjectAnimator ofFloat(View target, String propertyName, float... values) {
        MyObjectAnimator anim = new MyObjectAnimator(target, propertyName, values);
        return anim;
    }

    //每隔16ms執(zhí)行一次
    @Override
    public boolean doAnimationFrame(long currentTime) {
        //后續(xù)的效果渲染
        //動畫的總幀數(shù)
        float total = mDuration / 16;
        //拿到執(zhí)行百分比 (index)/total
        float fraction = (index++) / total;
        //通過插值器去改變對應(yīng)的執(zhí)行百分比
        if (interpolator != null) {
            fraction = interpolator.getInterpolator(fraction);
        }
        //循環(huán) repeat
        if (index >= total) {
            index = 0;
        }
        //交給mFloatPropertyValuesHolder始鱼,改變對應(yīng)的屬性值
        mFloatPropertyValuesHolder.setAnimatedValue(mTarget.get(), fraction);
        return false;
    }

    //開啟動畫
    public void start() {
        //交給mFloatPropertyValuesHolder改變對應(yīng)的屬性值
        mFloatPropertyValuesHolder.setupSetter();
        VSYNCManager.getInstance().add(this);
    }
}

最后我們來使用下MyObjectAnimator來看看動畫效果

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = findViewById(R.id.bottom);
        MyObjectAnimator animator = MyObjectAnimator.ofFloat(button, "ScaleX", 1f, 2f, 3f, 1f);
        animator.setInterpolator(new LineInterpolator());
        animator.setDuration(3000);
        animator.start();
    }

布局文件如下

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/bottom"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="#008500"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

效果如下圖仔掸,對button進行橫向縮放,和使用原生的ObjectAnimator實現(xiàn)的效果基本一致

ObjectAnimator.gif
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末医清,一起剝皮案震驚了整個濱河市起暮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌会烙,老刑警劉巖负懦,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異柏腻,居然都是意外死亡密似,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門葫盼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來残腌,“玉大人,你說我怎么就攤上這事∨酌ǎ” “怎么了蟆盹?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長闺金。 經(jīng)常有香客問我逾滥,道長,這世上最難降的妖魔是什么败匹? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任寨昙,我火速辦了婚禮,結(jié)果婚禮上掀亩,老公的妹妹穿的比我還像新娘舔哪。我一直安慰自己,他們只是感情好槽棍,可當我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布捉蚤。 她就那樣靜靜地躺著,像睡著了一般炼七。 火紅的嫁衣襯著肌膚如雪缆巧。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天豌拙,我揣著相機與錄音陕悬,去河邊找鬼。 笑死按傅,一個胖子當著我的面吹牛捉超,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播逞敷,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼灌侣!你這毒婦竟也來了推捐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤侧啼,失蹤者是張志新(化名)和其女友劉穎牛柒,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體痊乾,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡皮壁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了哪审。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蛾魄。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出滴须,到底是詐尸還是另有隱情舌狗,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布扔水,位于F島的核電站痛侍,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏魔市。R本人自食惡果不足惜主届,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望待德。 院中可真熱鬧君丁,春花似錦、人聲如沸磅网。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽涧偷。三九已至簸喂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間燎潮,已是汗流浹背喻鳄。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留确封,地道東北人除呵。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像爪喘,于是被迫代替她去往敵國和親颜曾。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內(nèi)容