Stragety 策略模式 | 解說實現 | Android Framework 動畫

Stragety 策略模式 | 解說實現 | Android Framework 動畫

Overview of Content

如有引用參考請詳註出處,感謝

在這篇文章中,我們將深入探討 策略模式(Strategy Pattern在 Android 框架動畫中的實際應用。首先,我們會總覽整體內容,接著介紹策略模式的使用場景。然後,我們將深入了解策略模式的定義、UML結構以及在設計中的優缺點。隨後,我們會探討策略模式在實作中的細節,包括標準的實現方式以及如何有效管理策略模式的實踐

最後,我們將轉向 Android 源碼中的動畫範疇,包括動畫中的 TimeInterpolator 插值器、TypeEvaluator 類型估值器、Animation 的基本使用方法,以及在 View 中啟動動畫的技巧。同時,我們還將討論如何運用里式替換原則,以最佳化動畫程式碼。


Stragety 使用場景

定義一系列的演算法,並將每個一個演算法封裝,並 互相取代

A. 對同一類型的問題有多種處理方式,多個 if/elseswitch/case 判斷時

B. 需要安全封裝多種同一類型的操作

Stragety 定義 & UML

Stragety 定義

定義一組算法,並將其封裝起來,之後可以快速互換

Stragety UML

透過將共同方法定義在抽象,透過注入細節,實現相同方法 but 不同結果

角色功能
Context上下文角色,封裝 IStragety,屏蔽高層模塊對策略的直接訪問
IStragety共同方法的抽象接口
ConstractStragetyA、B實作類演算法類(細節實現)

Stragety 設計:優缺點

Stragety 設計優點

● 結構清晰,簡單直觀,擴充方便

● 透過注入相對而言耦合度較低

● 不同演算法出現問題時,可以分開維護,也符合開閉原則

Stragety 設計缺點

● 隨著演算法的增加,實現子類會變多

● 另一個代價是,外部需要知道使用的策略!不符合迪米特原則(最少知識原則)


Stragety 實現

假設去韓國旅行有三種方式,自助旅行、半自助旅行、旅行社,每種選擇方式所花費的金額也不一樣

最簡單直覺的寫法是使用 if / elseswitch / case 去分類,但 這種寫法不符合開閉原則,每次新增一個旅遊金額算法,都需要修改 TravelCal 方法


public class TravelCal {
    
    public int getTravelCal(String type) {
        int result;
        
        if("Agency".equals(type)) {
            result = 300;
        } else if("SelfHelp".equals(type)) {
            result = 50;
        } else if("Backpacking".equals(type)) {
            result = 100;
        } else {
            result = -1;
        }
        
        return result;
    }
}

Stragety 實現標準

A. IStragety 介面:宣告共同的抽象方法,以目前案例就是定義每一種旅行方式所需的花費


// IStragety.java

public interface TravelCast {
    // 共同抽象
    int calCast();
}

B. ConstractStragety:定義細節,讓每一個方案去實作計算花費金額


// 旅行社
public class Agency implements TravelCast {
    @Override
    public int calCast() {
        return 300;
    }
}

// 自助
public class SelfHelp implements TravelCast {
    @Override
    public int calCast() {
        return 100;
    }
}

// 背包客
public class Backpacking implements TravelCast {
    @Override
    public int calCast() {
        return 50;
    }
}

● User 使用:透過替換實作細節,來達到不同的金額

● 開閉原則:

如果有新的旅行方案,同樣透過繼承 TravelCast 抽象來拓展,並不會觸碰到其他類


public class TestStrategy {

    public static void main(String[] args) {
        TravelCast travelCast;

        travelCast = new Agency();
        printCast(travelCast);

        travelCast = new SelfHelp();
        printCast(travelCast);

        travelCast = new Backpacking();
        printCast(travelCast);
    }

    public static void printCast(TravelCast travelCast) {
        System.out.println(travelCast.getClass().getSimpleName() + " Cast: " + travelCast.calCast());
    }

}

--實作--

管理 Stragety 實作:Context 管理

● 在最原始的 Stragety 實作,User 類會了解到細節 (TravelCast 的實作),這就不符合 最少知識依賴導致 設計原則

● 這邊我們透過一個 Manager 管理類,透過外部注入 Option 選項,來決定最終要產生的花費

上面重複,沒有修改的類 (IStragety、ConstractStragety),這邊不會列出

A. Manager:新增一個管理類,聚合管理所有的 IStragety,並給予預設值


// Manager 類

public class TravelManager {

    enum Option {
        BACK_PACKING,
        SELF_HELP,
        AGENCY
    }

    // 聚合所有 TravelCast 抽象
    private final Map<Option, TravelCast> map = new HashMap<>() {
        {
            put(Option.BACK_PACKING, new Backpacking());
            put(Option.SELF_HELP, new SelfHelp());
            put(Option.AGENCY, new Agency());
        }
    };

    private Option option;

    public TravelManager() {
        // 設定 default
        option = Option.AGENCY;
    }

    // 透過注入,來達到切換細節
    public TravelManager setOption(Option option) {
        this.option = option;
        return this;
    }

    public void printCast() {
        TravelCast travelCast = map.get(option);

        if(travelCast == null) {
            throw new IllegalAccessError("setTravelCast first");
        }

        System.out.println(travelCast.getClass().getSimpleName() + " Cast: " + travelCast.calCast());
    }
}

● 這裡有另外一種方式叫做 策略枚舉,透過枚舉的封裝,可以加強可讀性

B. User 使用透過 Manager 來 管理細節,而 User 只需要知道 Manager 即可


// User 使用

public class TestStrategy {

    public static void main(String[] args) {
        TravelManager travelManager = new TravelManager();

        travelManager.setOption(TravelManager.Option.AGENCY).printCast();

        travelManager.setOption(TravelManager.Option.SELF_HELP).printCast();

        travelManager.setOption(TravelManager.Option.BACK_PACKING).printCast();
    }

}

Manager(Context) 不符合開閉原則

雖然這裡透過 Manager 來管理 IStragety 所有細節,但如果有新增類,就需要手動修改 Manager 類

● 這裡還可以使用工廠、代理、享元... 等等模式來實做 Manager(依需求決定)


Android source 動畫

動畫透過人類的 視覺暫留 來達成,透過快速切換來達到動畫的效果

視覺暫留: 對於上個映像會暫時留在眼中

來源每秒楨數
標準電影24 楨/秒
Android 動畫60 楨/秒

Android 動畫:TimeInterpolator 插值器

● 透過 TimeInterpolator 插值器,可以讓我們接收動畫的運作時間,透過時間的百分比來調整動畫的速度

● 下圖代表給予相同的時間,但不同插值器會返回不同結果,從這裡其實就可以看出 Stragety 設計,不同的插值器代表了不同的算法

● 常見 Interpolator 插值器如下

插值器對動畫的功能
LinearInterpolator線性加速
AccelerateDecelerateInterpolator加速 & 減速
DecelerateInterpolator減速

Android 動畫:TypeEvaluator 類型估值器

● TypeEvaluator 的功能是,得到 TimeInterpolator 插值器的數值後,將得到的時間百分比、屬性起始值、目標值來運算最終結果,將最終運算結果交給 View


// TypeEvaluator.java

public interface TypeEvaluator<T> {    // 泛型

    public T evaluate(float fraction, T startValue, T endValue);

}

● 常見的 Evaluator 類型估值器如下

插值器對動畫的功能
FloatEvaluator浮點數運算
IntEvaluator整數運算
ArgbEvaluator顏色運算

// IntEvaluator.java

public class IntEvaluator implements TypeEvaluator<Integer> {

    /**
     * fraction 是時間插值器運算出的值 (0 ~ 1 之間)
     */ 
    public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
        int startInt = startValue;
        return (int)(startInt + fraction * (endValue - startInt));
    }
}

Animation 基礎使用

A. 設定動畫 xml 檔案


<!-- R.anim.click_btn -->

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300">

    <scale android:fromXScale="0.5" android:fromYScale="0.5"
        android:pivotX="50%" android:pivotY="50%"
        android:toXScale="1.0" android:toYScale="1.0"/>
</set>

B. 使用 AnimationUtils 來讀取 xml 檔案


// 讀取 xml 資源檔

Animation animation = AnimationUtils.loadAnimation(this, R.anim.click_btn);

C. 將 Animation 設定給 View


Button btn = findViewById(R.id.btn);

// Button 加載動畫
btn.startAnimation(animation);

● startAnimation 是 View 的方法

View startAnimation 啟動動畫


// View.java

public void startAnimation(Animation animation) {
    // 初始化動畫開始時間
    animation.setStartTime(Animation.START_ON_FIRST_FRAME);

    // 對該 View 設定 Animation
    setAnimation(animation);

    // 刷新 Parent 緩存
    invalidateParentCaches();
    
    // 刷新 View 自身
    invalidate(true);
}

// -------------------------------------------------------
// Animation.java

public static final int START_ON_FIRST_FRAME = -1;

● 刷新後 ViewGroup 會透過 dispatchDraw 方法,對 View 的部分區域重繪,ViewGroup 重繪時會呼叫 ViewGroup#drawChild 方法


// ViewGroup.java

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        // @ 追蹤 draw 方法
        return child.draw(canvas, this, drawingTime);
    }

// -------------------------------------------------------------
// View.java

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    
    ... 省略部分

    if (a != null) {
        // @ 追蹤 applyLegacyAnimation 方法
        more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
        
        ...
    } else {

        ... 省略部分
    }
}

A. applyLegacyAnimation 方法:初始化、標記 View 動畫啟動


// View.java

    @CallSuper
    protected void onAnimationStart() {
        // 標記該 View 已啟動 Animation
        mPrivateFlags |= PFLAG_ANIMATION_STARTED;
    }

    private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
            Animation a, boolean scalingRequired) {
        Transformation invalidationTransform;
        final int flags = parent.mGroupFlags;

        final boolean initialized = a.isInitialized();

        // 檢查是否初始化
        if (!initialized) {
            // 尚未初始化則初始化
            a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
            a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
            if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);

            // 標記 View 啟動動畫
            onAnimationStart();
        }

        // 儲存動畫訊息
        final Transformation t = parent.getChildTransformation();

        // @ 追蹤 getTransformation 方法
        // 獲取動畫相關值
        boolean more = a.getTransformation(drawingTime, t, 1f);

        ...

        if (more) {
            if (!a.willChangeBounds()) {

                ... 省略部分

            } else {
                if (parent.mInvalidateRegion == null) {
                    parent.mInvalidateRegion = new RectF();
                }

                final RectF region = parent.mInvalidateRegion;

                // 獲取重繪區域
                a.getInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop, region,
                        invalidationTransform);

                // child 繪製的動畫可能螢幕之外,需要確保該刷新不會被取消
                parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;

                // 計算有效區域
                final int left = mLeft + (int) region.left;
                final int top = mTop + (int) region.top;

                // ViewGroup 刷新指定區塊
                parent.invalidate(left, 
                                  top, 
                                  left + (int) (region.width() + .5f),
                                  top + (int) (region.height() + .5f));
            }
        }
        return more;

    }

B. Animation#getTransformation 方法:重點是計算動畫時間,決定該動畫是否仍需要繪製,返回 true 代表要繪製動畫,如果


// Animation.java

    Interpolator mInterpolator;

    public boolean getTransformation(long currentTime, 
                                     Transformation outTransformation,
                                     float scale) {
        mScaleFactor = scale;
        
        // @ 追蹤 getTransformation 方法
        return getTransformation(currentTime, outTransformation);
    }

    public boolean getTransformation(long currentTime, Transformation outTransformation) {
        if (mStartTime == -1) {
            // 設定動畫啟動時間
            mStartTime = currentTime;
        }

        // 該 Animation 總時間
        final long startOffset = getStartOffset();
        
        final long duration = mDuration;
        
        // 時間流逝的百分比
        float normalizedTime;

        if (duration != 0) {
            normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /
                    (float) duration;
        } else {
            // 動畫尚未啟動 (延遲)
            normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
        }

        // 動畫完成 or 取消,expired = true
        // 1.0 代表動畫時間已經過
        final boolean expired = normalizedTime >= 1.0f || isCanceled();
        mMore = !expired;

        ...

        // 動畫時間正在運作中
        if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
            
            ... 省略部分

            // 插值器
            final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
            
            // @ 分析 applyTransformation 方法
            applyTransformation(interpolatedTime, outTransformation);
        }
        
        ...

        if (!mMore && mOneMoreTime) {
            mOneMoreTime = false;
            return true;
        }

        return mMore;
    }

● 從這裡可以看到 Interpolator 的身影,同時也可以發現 使用者如果設定 不同 Interpolator 也會也不同的效果


// Animation.java

    public Animation() {
        // 設定預設插值器
        ensureInterpolator();
    }

    public void setInterpolator(Interpolator i) {
        mInterpolator = i;
    }

    protected void ensureInterpolator() {
        if (mInterpolator == null) {
            // 預設插值器為 AccelerateDecelerateInterpolator
            mInterpolator = new AccelerateDecelerateInterpolator();
        }
    }

● 預設插值器為 AccelerateDecelerateInterpolator:這是加減速插值器


// AccelerateDecelerateInterpolator.java

@HasNativeInterpolator
public class AccelerateDecelerateInterpolator extends BaseInterpolator
        implements NativeInterpolator {
        
    public AccelerateDecelerateInterpolator() { }

    @SuppressWarnings({"UnusedDeclaration"})
    public AccelerateDecelerateInterpolator(Context context, AttributeSet attrs) { }

    public float getInterpolation(float input) {
        // 加速插值器
        return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
    }

    /** @hide */
    @Override
    public long createNativeInterpolator() {
        // 減速插值器
        return NativeInterpolatorFactory.createAccelerateDecelerateInterpolator();
    }
}

里式原則應用:applyTransformation

Animation#applyTransformation 是一個空實現,給每一個繼承於它的子類實現細節,這就是理式原則的典型應用,父類可以任意地被子類替換


// Animation.java

    protected void applyTransformation(float interpolatedTime, Transformation t) {
    }

● 如過動畫 xml 有設定 alpha 值,則會對應到 AlphaAnimation 類、選轉則是對應到 RotateAnimation 類


// AlphaAnimation.java  

public class AlphaAnimation extends Animation {
    
    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        final float alpha = mFromAlpha;
        
        // 設定透明度
        t.setAlpha(alpha + ((mToAlpha - alpha) * interpolatedTime));
    }
}

// -----------------------------------------------------
// RotateAnimation.java

public class RotateAnimation extends Animation {

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        float degrees = mFromDegrees + ((mToDegrees - mFromDegrees) * interpolatedTime);
        float scale = getScaleFactor();

        if (mPivotX == 0.0f && mPivotY == 0.0f) {
            t.getMatrix().setRotate(degrees);
        } else {
            t.getMatrix().setRotate(degrees, mPivotX * scale, mPivotY * scale);
        }
    }
}

更多的物件導向設計

物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)!

設計建模 2 大概念- UML 分類、使用

物件導向設計原則 – 6 大原則(一)

物件導向設計原則 – 6 大原則(二)

創建、行為、結構型設計 8 個比較 | 包裝模式 | 最佳實踐

創建模式 Creation Patterns

創建模式 PK

創建模式 - Creation Patterns

結構模式 Structural Patterns

結構模式 PK

結構模式 - Structural Patterns

結構模式專注於「物件之間的組成」,以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時,不會破壞其整體結構。例如,外觀模式、代理模式… 等等,詳細解說請點擊以下連結

Bridge 橋接模式 | 解說實現 | 物件導向設計

Decorate 裝飾模式 | 解說實現 | 物件導向設計

Proxy 代理模式 | 解說實現 | 分析動態代理

Iterator 迭代設計 | 解說實現 | 物件導向設計

Facade 外觀、門面模式 | 解說實現 | 物件導向設計

Adapter 設計模式 | 解說實現 | 物件導向設計

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響