Overview of Content
如有引用參考請詳註出處,感謝
在這篇文章中,我們將深入探討 策略模式(Strategy Pattern
)在 Android 框架動畫中的實際應用。首先,我們會總覽整體內容,接著介紹策略模式的使用場景。然後,我們將深入了解策略模式的定義、UML結構以及在設計中的優缺點。隨後,我們會探討策略模式在實作中的細節,包括標準的實現方式以及如何有效管理策略模式的實踐
最後,我們將轉向 Android 源碼中的動畫範疇,包括動畫中的 TimeInterpolator 插值器、TypeEvaluator 類型估值器、Animation 的基本使用方法,以及在 View 中啟動動畫的技巧。同時,我們還將討論如何運用里式替換原則,以最佳化動畫程式碼。
Stragety 使用場景
定義一系列的演算法,並將每個一個演算法封裝,並 可互相取代
A. 對同一類型的問題有多種處理方式,多個 if/else
,switch/case
判斷時
B. 需要安全封裝多種同一類型
的操作
Stragety 定義 & UML
● Stragety 定義:
定義一組算法,並將其封裝起來,之後可以快速互換
● Stragety UML:
透過將共同方法定義在抽象,透過注入細節,實現相同方法 but 不同結果
角色 | 功能 |
---|---|
Context | 上下文角色,封裝 IStragety ,屏蔽高層模塊對策略的直接訪問 |
IStragety | 共同方法的抽象接口 |
ConstractStragetyA、B | 實作類演算法類(細節實現) |
Stragety 設計:優缺點
● Stragety 設計優點
● 結構清晰,簡單直觀,擴充方便
● 透過注入相對而言耦合度較低
● 不同演算法出現問題時,可以分開維護,也符合開閉原則
● Stragety 設計缺點
● 隨著演算法的增加,實現子類會變多
● 另一個代價是,外部需要知道使用的策略!不符合迪米特原則(最少知識原則)
Stragety 實現
假設去韓國旅行有三種方式,自助旅行、半自助旅行、旅行社,每種選擇方式所花費的金額也不一樣
最簡單直覺的寫法是使用 if / else
、switch / 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);
}
}
}
更多的物件導向設計
物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)!
創建模式 Creation Patterns
● 創建模式 PK
● 創建模式 - Creation Patterns
:
創建模式用於「物件的創建」,它關注於如何更靈活、更有效地創建物件。這些模式可以隱藏創建物件的細節,並提供創建物件的機制,例如單例模式、工廠模式… 等等,詳細解說請點擊以下連結
● Singleton 單例模式 | 解說實現 | Android Framework Context Service
● Abstract Factory 設計模式 | 實現解說 | Android MediaPlayer
● Factory 工廠方法模式 | 解說實現 | Java 集合設計
● Builder 建構者模式 | 實現與解說 | Android Framwrok Dialog 視窗
● Clone 原型模式 | 解說實現 | Android Framework Intent
行為模式 Behavioral Patterns
● 行為模式 PK
● 行為模式 - Behavioral Patterns
:
行為模式關注物件之間的「通信」和「職責分配」。它們描述了一系列物件如何協作,以完成特定任務。這些模式專注於改進物件之間的通信,從而提高系統的靈活性。例如,策略模式、觀察者模式… 等等,詳細解說請點擊以下連結
● Stragety 策略模式 | 解說實現 | Android Framework 動畫
● Interpreter 解譯器模式 | 解說實現 | Android Framework PackageManagerService
● Chain 責任鏈模式 | 解說實現 | Android Framework View 事件傳遞
● Specification 規格模式 | 解說實現 | Query 語句實做
● Command 命令、Servant 雇工模式 | 實現與解說 | 物件導向設計
● Memo 備忘錄模式 | 實現與解說 | Android Framwrok Activity 保存
● Visitor 設計模式 | 實現與解說 | 物件導向設計
● Template 設計模式 | 實現與解說 | 物件導向設計
● Mediator 模式設計 | 實現與解說 | 物件導向設計
● Composite 組合模式 | 實現與解說 | 物件導向設計
● Observer 觀察者模式 | JDK Observer | Android Framework Listview
結構模式 Structural Patterns
● 結構模式 PK
● 結構模式 - Structural Patterns
:
結構模式專注於「物件之間的組成」,以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時,不會破壞其整體結構。例如,外觀模式、代理模式… 等等,詳細解說請點擊以下連結
● Decorate 裝飾模式 | 解說實現 | 物件導向設計
● Iterator 迭代設計 | 解說實現 | 物件導向設計