碩一Java/Android練習-8、監聽者模式, 工廠模式, 裝甲步兵, 血條

前言

我們上次做了一個父類別Member,並讓Villager和Militia成為Member的子類別(extends Member),這次我們要繼續使用Member,輕鬆的寫出新的兵種ManAtArms(裝甲步兵)。

這次我們會初次接觸

  1. 監聽者模式
  2. 工廠模式


簡介

在這次的練習中,我把一些程式碼簡化了,原本的程式流程是:

當要新增村民時

  1. MainActivity呼叫newVillager()
  2. newVillager()中會
    1. 創建出一個可以移動的圖片
      1. 圖片移動時呼叫MainActivity的onVillagerMoved(…)
    2. 把圖片放入Layout中
    3. 呼叫mPlayer.newVillager(圖片)
  3. Player的newVillager(圖片)會做的事情是
    1. 創建一個村民,並把自己和圖片交給村民
      1. 村民會處理週期性的事情,像是採集
    2. 把村民放入List<Member>中

當要新增民兵時,也是這樣,但是因為我們不需要處理採集的問題,所以村民的2.1.1就不用了

  1. MainActivity呼叫newMilitia()
  2. newMilitia()中會
    1. 創建出一個可以移動的圖片
    2. 把圖片放入Layout中
    3. 呼叫mPlayer.newMilitia(圖片)
  3. Player的newVillager(圖片)會做的事情是
    1. 創建一個民兵,並把自己和圖片交給村民
      1. 民兵會處理週期性的事情,像是攻擊
    2. 把民兵放入List<Member>中

如果依照以上的模式繼續寫,那以後越來越多兵種,就會很麻煩:

  1. MainActivity要有很多個new成員()的方法;
  2. Player也要有很多new成員()的方法;
  3. 在MainActivity中每個new…()的方法中,都要決定要不要處理移動事件,像是村民在圖片移動時,就要呼叫onVillagerMoved(…);

當然每次都重複一樣的事情程式也可以完成,可是程式就會變得不好維護、可讀性低、撰寫時期出錯率高。

於是在物件導向的撰寫中,就會出現很多種設計模式,我們這次要講的監聽者模式與工廠模式就是其中的兩種。

我們這次要在圖片中加入監聽者模式,而創建新兵種則加入工廠模式。


監聽者模式

村民和民兵的所使用的ImageView,都分別覆寫了onTouch(…),讓圖片可以被拉著移動;而村民還多執行了MainActivity::onVillagerMoved(…)。 ps. MainActivity::onVillagerMoved(…)的意思是「MainActivity中的onVillagerMoved(…)方法」

也就是MainActivity只在乎Villager的圖片有沒有移動,因為Villager的圖片移動了之後要判斷村民是否在採集區上面;而其他的圖片MainActivity則不在意。

那其實我們可以寫一個新的class叫做MemberView繼承ImageView(class MemberView extends ImageView),

在MemberView裡面,我們一樣會覆寫ImageView的onTouch(…),實作「讓圖片可以被拉著移動」的程式碼,

另外我們還會在MemberView放置一個interface叫做OnMovedListener,並且寫一個method叫做setOnMoveListener(OnMoveListener),

當MainActivity創建這個MemberView並將它放入Layout之後,它就是個可以移動的圖片了;而若是需要監聽它的移動事件(像是Villager移動時要判斷是否在採集區上),就使用MemberView::setOnMoveListener(OnMoveListener)設置監聽器,當圖片移動時,就會藉由這個監聽器通知MainActivity。

這種設計模式就叫做監聽者模式,在Android或是Java的UI設計上被大量使用,在Android中大家最熟悉的就是Button::setOnClickListener(OnClickListener)。

當你下次與其他人討論程式時,不用說「我在某class裡面放了一個interface,然後當事件發生時,某class會通知對它有興趣的另一個class」這麼一長串;你只要說「我在這個class裡面使用監聽者模式。」,對方就懂你在說什麼囉!

以下為MemberView的部分程式碼,除了大家已經看過的「讓圖片可以被拉著移動」的程式碼之外,還有interface在程式中的宣告與使用。

public class MemberView extends android.support.v7.widget.AppCompatImageView {
    private OnMoveListener mOnMoveListener;

    ...

    private float thumbX0;
    private float thumbY0;
    private float viewX0;
    private float viewY0;
    private boolean hasMoved;

    ...

    public MemberView(Context context) {
        super(context);
    }

    public void setOnMoveListener(OnMoveListener onMoveListener) {
        mOnMoveListener = onMoveListener;
    }

    //覆寫onTouchEvent,處理觸碰事件,我們要拖著照片移動
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN: //手指碰到螢幕
                //紀錄手指碰到螢幕的位置
                thumbX0 = event.getRawX();
                thumbY0 = event.getRawY();
                //紀錄元件的初始位置
                viewX0 = getX();
                viewY0 = getY();
                //紀錄在這次的觸碰事件中,從開始到結束,是否有移動過,如果沒有,會在手指離開螢幕的時候觸發點擊事件
                hasMoved = false;
                break;

            case MotionEvent.ACTION_MOVE: //手指在螢幕上移動
                //紀錄在這次的觸碰事件中,從開始到結束,是否有移動過,如果沒有,會在手指離開螢幕的時候觸發點擊事件
                hasMoved = true;
                //取得移動後的手指位置
                float x = event.getRawX();
                float y = event.getRawY();
                //更新元件的位置
                setX(viewX0 + (x - thumbX0));
                setY(viewY0 + (y - thumbY0));
                //寫在MainActivity的方法,處理當村民移動時要做的事情
                onViewMoved(this);
                break;
            case MotionEvent.ACTION_UP: //手指離開螢幕
                //如果手指不曾移動,觸發點擊事件
                if (!hasMoved) performClick();
                return false;
        }
        return true;
    }

    //Android要求,只要有覆寫onTouchEvent(),就必須覆寫performClick()
    @Override
    public boolean performClick() {
        return super.performClick();
    }

    //當圖片被移動時,通知有註冊監聽的地方
    private void onViewMoved(MemberView memberView) {
        //若mOnMoveListener有參考到物件,則透過這個物件(OnMoveListener)呼叫"onMove(MemberImageView)"來通知「移動事件」的發生
        if (mOnMoveListener != null) {
            mOnMoveListener.onMove(this);
        }
    }

    ...

    public interface OnMoveListener {
        void onMove(MemberView memberView);
    }
}

以下是MainActivity的部分程式碼,當「新增村民」、「新增民兵」、「新增裝甲步兵」被點時,做的事情都是

  1. 創建MemberView,然後把它放進Layout中
  2. 創建Member,並將MemberView交給Member
  3. 將Member交給Player

其中,因為村民需要監聽移動事件,所以還呼叫了setOnMoveListener(…)設置監聽器;民兵和裝甲步兵則不需要設置。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    ...
    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.newVillagerButton:
                //新增一個村民
                MemberView villagerView = newMemberView(R.drawable.villager);
                villagerView.setOnMoveListener(new MemberView.OnMoveListener() {
                    @Override
                    public void onMove(MemberView memberView) {
                        onVillageMoved(memberView);
                    }
                });

                Member villager = MemberFactory.newInstance(MemberFactory.Type.VILLAGER, mPlayer, villagerView);
                mPlayer.addMember(villager);
                break;

            case R.id.newMilitiaButton:
                //新增一個民兵
                MemberView militiaView = newMemberView(R.drawable.militia);
                Member militia = MemberFactory.newInstance(MemberFactory.Type.MILITIA, mPlayer, militiaView);
                mPlayer.addMember(militia);
                break;

            case R.id.newManAtArmsButton:
                MemberView manAtArmsView = newMemberView(R.drawable.man_at_arms);
                Member manAtArms = MemberFactory.newInstance(MemberFactory.Type.MAN_AT_ARMS, mPlayer, manAtArmsView);
                mPlayer.addMember(manAtArms);
                break;
        }
    }
    /**
    * 創建MemberView,並將MemberView放入Layout中
    * @param drawable 圖片資源
    * @return MemberView
    */
    private MemberView newMemberView(int drawable) {
        //創建ImageView,並覆寫onTouchEvent(),讓我們可以拖著照片移動
        MemberView memberView = new MemberView(this);

        //設定元件圖片
        memberView.setImageDrawable(getResources().getDrawable(drawable));

        //取得Layout
        ConstraintLayout constraintLayout = findViewById(R.id.constraintLayout);

        //把ImageView放入Layout,長寬各為200pixels
        constraintLayout.addView(memberView, 200, 200);

        return memberView;
    };

    private void onVillageMoved(ImageView view) {
        //取得擁有此ImageView的村民
        Villager villager = (Villager) mPlayer.findMemberByView(view);

        //判斷村民位置,設定工作
        if (isOverlapping(view, mForestImageView)) {
            villager.setJob(Villager.JOB_LUMBERJACK);

        } else if (isOverlapping(view, mFarmlandImageView)) {
            villager.setJob(Villager.JOB_FARMER);

        } else if (isOverlapping(view, mGoldImageView)) {
            villager.setJob(Villager.JOB_GOLD_MINER);

        } else if (isOverlapping(view, mStoneImageView)) {
            villager.setJob(Villager.JOB_STONE_MINER);

        } else {
            villager.setJob(Villager.JOB_NONE);

        }
    }
    ...
}

 


工廠模式

再來,我們還將產生兵種的程式碼交由「工廠Factory」處理,這樣的好處是(MainActivity)使用簡單,因為程式碼都寫在MemberFactory裡面了;而因為產生Member的程式碼都寫在MemberFactory裡面,將來要更改或是擴充時,使用它的地方(MainActivtiy)不需要做更改,只要改MmeberFactory裡面的程式就行了。

以下就是MemberFactory的程式碼

可以看到有一個enum Type紀錄著可以使用的類別,而newInstance(…)則會將Member實例化之後,將參數設置進去,並回傳。

這是個最簡單的工廠例子,當程式變的很複雜的時候,工廠的好處就會更明顯,像是Android在建立AlertDialog的時候,我們使用的是AlertFialog.Builder;使用Notification的時候,使用的是Notification.Builder。這些都是工廠模式的應用,我們不需要知道AlertDialog和Notifaction裡面在幹嘛,只要使用Builder提供的一些setter方法,最後使用build(),就可以獲取到一個AlertDialog或Notification的實例。

而我們這樣寫了之後,假如我們以後又新增了一個兵種叫做長劍兵,不用在MainActivity和Player又寫一個「創建長劍兵」的方法;不用分散的在程式中到處東改西改,要做的事情只有:

  1. 寫一個長劍兵類別
  2. 在MemberFactory的enum Type裡面加入長劍兵
  3. 在MemberFactory的newInstance(…)裡面加入創建長劍兵的程式碼
  4. 在MainActivity使用MemberFactory.newInstance(…)

一切事情變得簡單明瞭。

/**
 * Member的工廠類別
 */
public class MemberFactory {
    /**
     * 成員的類別(村民、民兵、裝甲步兵...)
     */
    public enum Type {
        VILLAGER,
        MILITIA,
        MAN_AT_ARMS
    }

    /**
     * 靜態方法,也就是說MemberFactory不需要實例化,就可以直接使用,產生並回傳一個Member
     *
     * @param type       成員的類別(Type.Villager, Type.MILITIA, ...)
     * @param player     擁有這個Member的Player
     * @param memberView 和這個Member對應的圖示
     * @return
     */
    public static Member newInstance(Type type, Player player, MemberView memberView) {
        //宣告Member
        Member member;

        //建構Member物件
        switch (type) {
            case VILLAGER:
                member = new Villager(player, memberView);
                break;
            case MILITIA:
                member = new Militia(player, memberView);
                break;
            case MAN_AT_ARMS:
                member = new ManAtArms(player, memberView);
                break;

            //防呆: 在enum裡面放了新類別,可是忘記在這裡實作
            default:
                throw new RuntimeException("輸入的Type尚未實作");
        }

        //回傳Member物件
        return member;
    }
}

 


裝甲步兵 ManAtArms

最後再來看看,當我們將程式做了以上簡化之後,要新增一個兵種時,會不會簡單許多?

1. 寫一個裝甲步兵的類別ManAtArms,這部份很簡單,直接繼承Member,然後把Militia已經寫好的攻擊程式碼複製過來

package com.nclab.cycu.lab453practice.members;


import com.nclab.cycu.lab453practice.MemberView;
import com.nclab.cycu.lab453practice.Player;

/**
 * 裝甲步兵
 */
public class ManAtArms extends Member {

    /**
     * @param player    擁有這個裝甲步兵的Player
     * @param imageView 這個裝甲步兵的視圖
     */
    public ManAtArms(Player player, MemberView imageView) {
        super(player, imageView, 45, 45, 6, 0, 0, 1);
    }

    /**
     * 攻擊Member
     *
     * @param targetMember 攻擊目標
     */
    private void attack(Member targetMember) {
        int damage = getAttack() - targetMember.getNearDefence();
        if (damage <= 0) {
            damage = 1;
        }
        targetMember.setHp(targetMember.getHp() - damage);
    }

    @Override
    protected void onLoop() {
        //尋找附近的單位
        Member member = getPlayer().findNearbyMember(this);
        //攻擊單位
        if (member != null) {
            attack(member);
        }
    }

}

2. 在MemberFactory裡面新增裝甲步兵

public class MemberFactory {
    /**
     * 成員的類別(村民、民兵、裝甲步兵...)
     */
    public enum Type {
        ...,
        MAN_AT_ARMS
    }

    /**
     * 靜態方法,也就是說MemberFactory不需要實例化,就可以直接使用,產生並回傳一個Member
     *
     * @param type       成員的類別(Type.Villager, Type.MILITIA, ...)
     * @param player     擁有這個Member的Player
     * @param memberView 和這個Member對應的圖示
     * @return
     */
    public static Member newInstance(Type type, Player player, MemberView memberView) {
        //宣告Member
        Member member;

        //建構Member物件
        switch (type) {
            ...
            case MAN_AT_ARMS:
                member = new ManAtArms(player, memberView);
                break;

            //防呆: 在enum裡面放了新類別,可是忘記在這裡實作
            ...
        }

        //回傳Member物件
        return member;
    }
}

3.在MainActivity裡面,建立MemberView,因為我們不需要設置監聽,所以就不呼叫MemberView::setOnMoveListener(…);然後使用工廠建立Member;最後把Member交給Player

@Override
    public void onClick(View view) {
        switch (view.getId()) {
            ...
            case R.id.newManAtArmsButton:
                MemberView manAtArmsView = newMemberView(R.drawable.man_at_arms);

                Member manAtArms = MemberFactory.newInstance(MemberFactory.Type.MAN_AT_ARMS, mPlayer, manAtArmsView);
                mPlayer.addMember(manAtArms);
                break;
        }
    }

結論-設計模式

使用設計模式來寫程式有好有壞,當然使用設計模式可以讓程式更有架構,但是有時候只是一個小程式,使用設計模式反而浪費時間與精力而且程式太小不足以顯現設計模式的好處。不過在我們應用中,以上兩種模式的確讓我們的程式開發起來更順暢,對吧?

物件導向還有很多種設計模式,這些設計模式都不是一開始就存在,而是前人的經驗累積與概念發想才出現的,也沒有說哪種設計模式比較好或比較壞,到底要不要使用設計模式就看個人囉!

我們在寫Android的時候,其實很容易就會學習到這些設計模式,像我們不知不覺就在使用監聽者模式和工廠模式,只是我們都不知道它其實是大家已經有共識的一種模式。


血條

講完設計模式之後,

最後來看看血條的程式碼吧!

MemberView

我們覆寫ImageView的onDraw(Canvas),所有的View顯示出來的圖都是從onDraw(Canvas)繪製的,所以我們要做的事情就是

  1. 讓Member依然使用ImageView::onDraw(Canvas)的程式碼,把該畫的畫出來
  2. 最後在圖片下方畫上血條

使用到的元件為

  1. 畫筆 Paint (自己宣告,要畫寫條的畫筆)
  2. 畫布 Canvas (從onDraw(Canvas)傳入)

最後寫一個方法叫做setHp(int hp, int maxHp),讓Member使用。

另外要注意的是ImageView::invalidate(),這個方法會讓畫面重新刷新一次,也就是說onDraw(…)會被重新呼叫。

public class MemberView extends android.support.v7.widget.AppCompatImageView {
    ...

    private Paint mPaint = new Paint();

    ...

    private int mHp, mMaxHp;

    ...

    public void setHp(int hp, int maxHp) {
        mHp = hp;
        mMaxHp = maxHp;
        //重畫一張圖
        getHandler().post(new Runnable() {
            @Override
            public void run() {
                invalidate();
            }
        });
    }

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

        //畫紅底
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawRect(getLeft()
                , (float) (getTop() + (getBottom() - getTop()) * 0.95)
                , getRight()
                , getBottom()
                , mPaint);
        //畫綠血
        mPaint.setColor(Color.GREEN);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawRect(getLeft()
                , (float) (getTop() + (getBottom() - getTop()) * 0.95)
                , getLeft() + (getRight() - getLeft()) * ((float) mHp / mMaxHp)
                , getBottom()
                , mPaint);
    }

    ...
}

Member

Thread不解釋囉,大家應該要會了,還不懂的同學回去看舊文。

public abstract class Member {
    ...

    private class MemberThread extends Thread {
        @Override
        public void run() {
            while (Member.this.isAlive()) {
                //更新圖示上的血條
                mMemberView.setHp(mHp, mMaxHp);

                //呼叫onLoop(),繼承Member的類別可以覆寫onLoop(),處理週期性的事情
                onLoop();

                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

GitHub

https://github.com/jim83803/Lab453Practice/commit/77766caf895f1cef86e4897fc5b2702a28f8d384?diff=split

發佈留言