我們時常需要這麼些功能,彈出一個層,給與用戶一些提示,這也是一種模態窗口,在沒有對當前對話框進行確認的時候,不能繼續往下操作。在設計如此功能之時,怎麼設計比較合理 ~ 是這篇文章要討論的問題。一葉 不傾向於提供給一個完整的解決方案,給一堆源碼。而會靠訴你如何根據你自己的需要去完善它,授人以魚不如授人以漁 ~
功能分析
我們設計一個對話框,對話框上有幾個按鈕(個數可定製),當然有個標題,會讓別人一眼看出它之功用,裏面可以有些詳細的提示文字,需要是模態窗口,而且窗口的大小可變,這樣能夠更好的適應不同的屏幕的大小。當然還有一個重要的功能,彈出效果 ~ 雖然從技術角度來說,實現起來並不難,或者說非常簡單,但這會以一個很好的用戶體驗展示給用戶。
爲了使用方面,我將接口設計的儘量簡潔,便於使用,如下所示,至於內部的實現,那就隨意了,接口函數是暴露在外面的,給別人使用,所以根據需要首先將它定義好,會讓你的實現步驟思路清晰 (本文所用到的源代碼可以從 這裏 獲取):
classPopupLayer:publicCCLayer{
public:
PopupLayer();
~PopupLayer();
virtualbool init();
CREATE_FUNC(PopupLayer);
// 需要重寫觸摸註冊函數,重新給定觸摸級別
virtualvoid registerWithTouchDispatcher(void);
// 重寫觸摸函數,永遠返回 true ,屏蔽其它層,達到 “模態” 效果
bool ccTouchBegan(cocos2d::CCTouch*pTouch, cocos2d::CCEvent*pEvent);
// 構架,並設置對話框背景圖片
staticPopupLayer* create(constchar* backgroundImage);
// 它可以顯示標題,並且設定顯示文字大小
void setTitle(constchar* title,int fontsize =20);
// 文本內容,padding 爲文字到對話框兩邊預留的距離,這是可控的,距上方的距離亦是如此
void setContentText(constchar* text,int fontsize =20,int padding =50,int paddintTop =100);
// 回調函數,當點擊按鈕後,我們關閉彈出層的同事,需要一個回調函數,以通知我們點擊了哪個按鈕(如果有多個)
void setCallbackFunc(CCObject* target, SEL_CallFuncN callfun);
// 爲了添加按鈕方面,封裝了一個函數,傳入些必要的參數
bool addButton(constchar* normalImage,constchar* selectedImage,constchar* title,int tag =0);
// 爲了在顯示層時之前的屬性生效,選擇在 onEnter 裏動態展示
virtualvoid onEnter();
virtualvoid onExit();
};
從使用方面的角度來說,定義了以上函數,以供外部調用,完成基本的功能需求。當然還有些隱藏的函數,如 setContentSize。由此開始我們的下一步設計 ~
onEnter 動態組建彈出層
前文提到過,需要的模態窗口大小是可變的,通過 ContentSize 來獲取,如果沒有設置 ContentSize ,那麼採取的方案是,窗口大小與傳入圖片一樣大,反之,將窗口設定爲指定大小。我們知道有很多類似於這樣的屬性設置修改等,對於完成這樣一個功能來說,有兩種方式。
其一,實時設置,實時刷新,比如在 static PopupLayer* create(const char* gackgroundImage)
的實現裏面,創建一個精靈,並設置好圖片,添加到當前層,如果調用了 setContentSize
我們再在此方法獲取精靈後去修改這個精靈的大小(很顯然在本文,並沒有實現 setContentSize 方法,但並不影響解說)。
其二,保留屬性,動態組建。這樣一種實現是在 static PopupLayer* create(const char* gackgroundImage)
方法內部,只保存圖片的名稱,而 setContentSize
也可以只專注於自己的工作即可。最後在一個適當的執行時期,根據以上兩個參數,動態創建符合需求的精靈,而這個操作在 onEnter 裏尤爲合適。
再以一個當前用到的例子,來說明 實時刷新 與 動態組建 的區別。看到定義中有一個 addButton 的方法,它是爲了可以在當前對話框中添加一個或者幾個按鈕,而添加幾個?當然並不確定,但確定的是,它們的位置會隨着按鈕個數的不同而不同,如果一個按鈕,將居中顯示(一分爲二),如果兩個(三等份距離),如果是實時刷新,我們在每一次 addButton 方法裏面創建一個新的按鈕,還需要設置正確的位置,當然在設置新位置的時候,你需要去判斷是否已經存在按鈕,還需要修改q前面按鈕的位置,可以預見,每添加一次,都需要去實時刷新之前設置過的值,如果代碼邏輯簡單還好,如果邏輯非常複雜,依賴屬性之多,那麼那將不好控制。如果使用 動態組建,在 addButton 方面裏面我們只需要用一個集合(類似機制)來保存所有的按鈕信息,僅此而已,然後在 onEnter 中,讀取這些信息,以實時添加按鈕至界面之中,而在運行到這裏之時,屬性基本已經確定了(這裏是按鈕個數)。只需計算一次各按鈕的位置,這樣的邏輯處理就相當清晰了 ~
可以說動態組建就一定比實時刷新要好麼?當然不是。一個採用實時刷新的例子: 在 Cocos2d-x 的 CCControl 的框架中,使用了實時刷新的解決方案,可以在很多地方看到類似 needsLayout()
這樣的操作,在設定一個控件的屬性之後,需要根據屬性更新控件的佈局等。
對於 實時刷新 與 動態組建 這兩種方式,需要根據實際情況來選擇,以簡化我們的開發,而在文本中,基本是以動態組建的方式設計,因爲在彈出層運行之時,所有的屬性就已經確定,它沒有必要再去根據屬性實時刷新。
彈出層的觸摸優先級,操作與顯示相一致
-128 是一個有意義的數字,它是 CCMenu 的默認觸摸級別,如果我們不使用自帶的菜單項,那麼對於觸摸級別的問題就很好處理了,隨便一個稍微大一點的值,即可。但是你需要自己寫按鈕處理的實現。當然使用 CCMenu 也是有好處的,它封裝了非常好用的點擊操作。爲了簡化開發,這裏就基於以 CCMenu 作爲對話框按鈕的操作實現。那麼我們的 彈出層 觸摸級別改設置爲多少?
在我們設定觸摸級別時,請記住一句話,操作與顯示相一致。此話何意?現在設想這樣一種情況,我們爲了屏蔽彈出層以外所有的層的操作,而將彈出層級別置爲 -129 ,這樣便保證了它是模態的了(一般而言,除了 CCMenu 的級別,用戶自定義也無需這麼大),但是如果當前彈出層上方還有其它的層(也可以是彈出層的父節點上方有其它層,如多層的 UI 設計),並且其上也有 CCMenu 按鈕,那麼就出現了顯示在 彈出層上放層中的 CCMenu 按鈕不能點擊,這不科學!實際,在彈出層的 CCMenu 也不能點擊,只是我們可以通過將彈出層的觸摸消息傳遞給層中的 CCMenu 解決。所以設置爲一味的追求最大的觸摸優先級,並不可取,它不能很好的做到 操作與顯示相一致,而優先級小於 CCMenu 並不能屏蔽下方的其它 CCMenu 操作,所以對於彈出層本身,它設置爲 -128 是合理的,對於同樣級別的這些元素來說(彈出層和CCMenu),能夠做到,顯示在最上方的優先處理。如果在彈出層上方還有層,是可以點擊的,爲什麼不呢!而我們要做的是,通過邏輯控制,讓彈出層節點,最後添加到場景的最上方。
對於 操作與顯示相一致 的一個解決方案,可以參考一葉的另兩篇文章,多層 UI 觸摸事件的輕量級設計,和 CCScrollView 實現幫助界面、關卡選擇。當然那裏並沒有用到 CCMenu,而是獨立創建了一套規則。
回調函數的實現方案
由於我們使用了 CCMenu 作爲按鈕,所以完全可以將 CCMenu 的回調函數,當作參數傳入進來,這樣處理確實很簡單,但是另一個問題,我門需要在事件處理完之後,關閉當前層,那這就不能僅僅如此了,需要進一步封裝,將彈出層的按鈕回調函數設置爲內部實現,然後在 回調給它的上層,之後關閉對話框(關閉的操作由對話框層來完成),對於回調一般有兩種方式,一種是 delegate
回調,這需要定義接口,由上層,繼承實現接口,並把自己當作參數,傳入彈出層,由彈出層調用 delegate 的接口方法實現,在 Cocos2d-x 裏面很多地方用到此方式。而另一種則是 函數綁定,就像 CCMenu 那樣的綁定函數。
在這裏,一葉選擇了後者,如果當前彈出層固定一個或者兩個按鈕,那麼我將使用 delegate 來實現函數回調,它會使回調步驟更爲清晰,但是,這裏設計的是按鈕個數可變,功能也不盡相同,使用回調函數,綁定 CCNode 參數,以其 tag 標示,點擊的是哪個按鈕,當然這裏也並不是說使用那種方式是絕對的好壞。delegate 對參數傳遞來說,更爲嚴謹。 void setCallbackFunc(CCObject* target, SEL_CallFuncN callfun);
的定義是類似 CCMenu 的回調機制,一個 CCNode 作爲參數,其 tag 用以標示當前點擊的是哪個按鈕。
彈出層的調用
voidPopup::popupLayer(){
// 定義一個彈出層,傳入一張背景圖
PopupLayer* pl =PopupLayer::create("popuplayer/BackGround.png");
// ContentSize 是可選的設置,可以不設置,如果設置把它當作 9 圖縮放
pl->setContentSize(CCSizeMake(400,350));
pl->setTitle("吾名一葉");
pl->setContentText("嬌蘭傲梅世人賞,卻少幽芬暗裏藏。不看百花共爭豔,獨愛疏櫻一枝香。",20,60,250);
// 設置回調函數,回調傳回一個 CCNode 以獲取 tag 判斷點擊的按鈕
// 這只是作爲一種封裝實現,如果使用 delegate 那就能夠更靈活的控制參數了
pl->setCallbackFunc(this, callfuncN_selector(Popup::buttonCallback));
// 添加按鈕,設置圖片,文字,tag 信息
pl->addButton("popuplayer/pop_button.png","popuplayer/pop_button.png","確定",0);
pl->addButton("popuplayer/pop_button.png","popuplayer/pop_button.png","取消",1);
// 添加到當前層
this->addChild(pl);
}
voidPopup::buttonCallback(cocos2d::CCNode*pNode){
// 打印 tag 0, 確定,1 ,取消
CCLog("button call back. tag: %d", pNode->getTag());
}
// 彈出效果 關鍵代碼,當前層直執行這個動作
CCAction* popupLayer =CCSequence::create(CCScaleTo::create(0.0,0.0),
CCScaleTo::create(0.06,1.05),
CCScaleTo::create(0.08,0.95),
CCScaleTo::create(0.08,1.0), NULL);
這裏一張截圖,其中 popup 按鈕是個 CCMenu,彈出層中兩個按鈕也是 CCMenu,而層本身的觸摸優先級別也是 -128 ,都是同級,那就依我所言,操作與顯示相一致,唯一要注意的是邏輯的控制,什麼時候彈出層,由哪個節點彈出層(一般由場景基層來負責),以保證它顯示在最上方。
通過以上的討論實踐,完成了對話框的基本模型,它實現了以下功能:
一個可以彈出的對話框實現
模態窗口的實現(需要邏輯的控制)
多按鈕的支持,位置自適應,提供回調函數
提供標題和內容設置
支持 九圖 ,控制適應彈出框大小
當然還有許多其它並沒有照顧到的功能,或者不完善的地方,這就需要用戶自己擴展,定製了,如,這樣一個層,至少應該是單例的,任何時候只應該存在一個,可以用單例模式實現,對於彈出層的內容方面,這裏只有標題和內容,並且標題位置固定,按鈕的位置還可以更靈活的設置等。