蘋果的應用講究用戶體驗
有的時候仔細想想
的確,很多細節決定了用戶體驗
比如說慣性拖動
可以說之前沒有任何一家廠商能把觸摸慣性拖動做的像蘋果的UI那么流暢
Cocos2D中實現能夠慣性拖動的選擇界面
完成的效果:
制作一個簡單的圖層,通過傳入許多的節點,圖層自動將節點排版,並能夠通過物理拖拽來選擇其中的某一個節點,並通知節點的代理來處理
首先新建一個cocos2d項目,我用的版本是2.0,命名為SimplePhysicsDragSelectorTest
新建一個objective-c class,我這里命名為SZSimplePhysicsDragSelector
在SimplePhysicsDragSelector.h文件里添加以下代碼:
#import "cocos2d.h" @class SZSimplePhysicsDragSelector; @protocol SZSimplePhysicsDragSelectorDelegate <NSObject> @optional // call when the selected icon changes -(void)onSelectedIconChanged:(SZSimplePhysicsDragSelector*)selector; @end @interface SZSimplePhysicsDragSelector : CCLayer { CCNode *s_content;//所有節點圖標的父節點 NSMutableArray *s_icons;//節點圖標清單 CCNode *s_selectedIcon;//選定的節點 BOOL isDragging;//是否在拖拽狀態 CGPoint lastTouchPoint;//上一個觸摸點 float lastx;//上一個圖層內容x坐標 float xvel;//內容在x軸上的速度 int maxX;//內容可以自然移動到的最大極限x坐標 int minX;//內容可以自然移動到的最小極限x坐標 float acceleration;//加速度 float f;//合外力 id<SZSimplePhysicsDragSelectorDelegate> s_delegate;//代理 } @property (nonatomic, readonly) NSMutableArray *Icons; @property (nonatomic, readonly) CCNode *SelectedIcon; @property (nonatomic, assign) id<SZSimplePhysicsDragSelectorDelegate> Delegate; - (id)initWithIcons:(NSArray*)icons; @end
這里聲明了SZSimplePhysicsDragSelector需要使用到的變量和方法,同時聲明了SZSimplePhysicsDragSelector代理的方法
變量的作用如注釋里描述的,后面將會詳細說到
解釋下代理方法:
-(void)onSelectedIconChanged:(SZSimplePhysicsDragSelector*)selector;
在圖層選擇的節點發生改變時將會發送此消息給代理,如果改變為沒有選擇節點也會發送此消息
初始化
在SZSimplePhysicsDragSelector.m文件中添加以下代碼:
@implementation SZSimplePhysicsDragSelector @synthesize Delegate = s_delegate; @synthesize Icons = s_icons; @synthesize SelectedIcon = s_selectedIcon; - (id)initWithIcons:(NSArray *)icons { self = [super init]; if (self) { s_icons = [[NSMutableArray alloc] initWithArray:icons]; s_content = nil; s_selectedIcon = nil; isDragging = NO; lastTouchPoint = CGPointZero; lastx = 0.0f; xvel = 0.0f; minX = 0; maxX = 0; acceleration = 0.0f; f = 0.0f; self.isTouchEnabled = true;// 啟用接收觸摸事件 s_delegate = nil; } return self; } - (void)dealloc { [s_icons release]; [super dealloc]; } #pragma mark Override methods -(void) onEnter { [super onEnter]; s_content = [[CCSprite alloc]init]; [self addChild:s_content]; [self scheduleUpdate];//開啟計時器 } -(void) onExit { [self unscheduleUpdate];//關閉計時器 [self removeChild:s_content cleanup:YES]; [s_content release]; s_content = nil; s_selectedIcon = nil; [super onExit]; } @end
以上代碼實現了初始化&內存釋放以及onEnter和onExist方法
在選擇器被添加到某一個節點中時,將會自動創建一個內容節點s_content,用來存放所有的節點,並一起移動
布局節點
在onEnter方法中布局視圖,並實現layout方法-(void) onEnter
-(void) onEnter { [super onEnter]; s_content = [[CCSprite alloc]init]; [self addChild:s_content]; [self layout]; [self scheduleUpdate]; } -(void) layout { int i = 1; for (CCNode *icon in s_icons) { CGPoint position = ccp((i-1) * 180, 0); float distance = fabsf(icon.position.x)/100; icon.position = position; if (![s_content.children containsObject:icon]) { [s_content addChild:icon]; } i++; } s_selectedIcon = [s_icons lastObject]; if ([s_delegate respondsToSelector:@selector(onSelectedIconChanged:)]) { [s_delegate onSelectedIconChanged:self]; } minX = - (i-1) * 180 - 100; maxX = 100; }
解釋下layout方法
將180pt作為每兩個節點之間的間距,同時第一個節點在s_content中的位置應該是(0,0)所以計算得出位置的公式(間距和初始位置可以根據需要更改)
position = ccp((i-1) * 180, 0)
之后添加節點到s_content,並且設置最后一個為初始選定的節點,最后通知代理選定節點發生更改
關於極限位置(minX,maxX)是這樣設定的,前面說到180作為間距,(0,0)為初始節點位置,所以最后一個節點的x坐標為(i-1) * 180(i為節點個數),當需要選擇右邊的節點時實際上是將s_content的位置向左移動,所以選擇到最后一個節點時s_content的位置應該是-(i-1) * 180,同理第一個選擇到第一個節點時s_content的位置應該是(0,0),此外我希望極限位置能夠比頭尾節點位置的范圍稍大,所以最終我設定
minX = - (i-1) * 180 - 100;
maxX = 100;
觸摸記錄
布局完成,接下來我們需要實現觸摸事件消息來記錄數據供模擬物理使用
在SZSimplePhysicsDragSelector.m文件中添加以下代碼:
- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; { s_selectedIcon = nil; if ([s_delegate respondsToSelector:@selector(onSelectedIconChanged:)]) { [s_delegate onSelectedIconChanged:self]; } UITouch *touch = [touches anyObject]; CGPoint position = [self convertTouchToNodeSpace:touch]; lastTouchPoint = position; isDragging = true; } - (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; { UITouch *touch = [touches anyObject]; CGPoint position = [self convertTouchToNodeSpace:touch]; CGPoint translate = ccpSub(position, lastTouchPoint); translate.y = 0; s_content.position = ccpAdd(s_content.position, translate); lastTouchPoint = position; } - (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; { isDragging = false; } - (void)ccTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; { isDragging = false; }
這里分開說下4個觸摸事件
- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
在方法中我們清空了選擇的節點並通知代理選擇的節點改變,標記自身狀態為拖拽中
- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
在方法中根據此刻觸摸點與上一次觸摸點的位置差,來移動s_content的位置,從而使內容跟隨觸摸移動,最后在記錄下此刻的位置為上一次觸摸位置,供下一次計算使用
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
標記自身狀態為未拖拽
- (void)ccTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
標記自身狀態為未拖拽
這樣我們已經能夠辨別自身是否在拖拽狀態以及正確拖拽內容
模擬物理計算
首先說明一下思路:
我們在udpate方法中我們需要檢測圖層的狀態:
若圖層在被拖拽狀態,則不需要模擬物理,只需要計算出用戶觸摸拖拽內容在x軸上的速度
若圖層在未拖拽狀態,則根據已經記錄下的x軸移動速度,和通過受力計算出的加速度,改變x軸移動速度,最后在根據計算出的移動速度來計算實際位移
在SZSimplePhysicsDragSelector.m文件中添加以下代碼:
-(void) update:(ccTime)dt; { [self updateMove:dt]; } - (void) updateMove:(ccTime)dt { if ( !isDragging ) { // *** CHANGE BEHAVIOR HERE *** // float F1 = 0.0f; float F2 = 0.0f; float F3 = 0.0f; CGPoint pos = s_content.position; //F1 // friction F1 = - xvel * 0.1; //F2 // prevent icons out of range if ( pos.x < minX ) { F2 = (minX - pos.x); } else if ( pos.x > maxX ) { F2 = (maxX - pos.x); } //F3 // suck planet if (fabsf(xvel) < 100 && !s_selectedIcon) { CCNode *nearestIcon = nil; for (CCNode *icon in s_icons) { if (nearestIcon) { CGPoint pt1 = [icon.parent convertToWorldSpace:icon.position]; float distance1 = fabsf(pt1.x - [CCDirector sharedDirector].winSize.width/2); CGPoint pt2 = [nearestIcon.parent convertToWorldSpace:nearestIcon.position]; float distance2 = fabsf(pt2.x - [CCDirector sharedDirector].winSize.width/2); if (distance1 < distance2) { nearestIcon = icon; } } else { nearestIcon = icon; } } if (nearestIcon) { s_selectedIcon = nearestIcon; if ([s_delegate respondsToSelector:@selector(onSelectedIconChanged:)]) { [s_delegate onSelectedIconChanged:self]; } } } if (s_selectedIcon) { CGPoint pt = [s_selectedIcon.parent convertToWorldSpace:s_selectedIcon.position];; float distance = pt.x - [CCDirector sharedDirector].winSize.width/2; F3 = - distance; } //CALCULATE f = F1 + F2 + F3; acceleration = f/1; xvel += acceleration; pos.x += xvel*dt; s_content.position = pos; } else { xvel = ( s_content.position.x - lastx ) / dt; lastx = s_content.position.x; } }
在onEnter方法中,我們已經啟用了計時器,所以udpate方法將會在每個最小時間間隔被調用
其他就如同剛才整理的那樣,沒什么問題,主要使這個受力問題,這個受力是我經過了好多數值的嘗試后,得出的比較能符合要求的效果
內容受到的力分為
F1阻力:方向與內容移動速度方向相反,大小與移動速度快慢呈正比
F1 = - xvel * 0.1;
F2超出邊界的額外受力:方向與超出邊界的方向相反,大小與超出邊界的距離呈正比
F2 = (minX - pos.x);或者F2 = (maxX - pos.x);
F3將選定節點吸至屏幕中央的吸力:方向從選定節點指向屏幕中央,大小與選定節點到屏幕中央的距離呈正比:
F3 = - distance;
此外有個細節,如果我們不斷的施加吸力,會出現一種情況:很難將選定的節點拖拽出去,因為吸力太大了,所以在代碼中添加了一個條件
fabsf(xvel) < 100,當移動速度小於100時,才產生吸力,這樣你會發現拖拽順暢多了,並且也能夠在選定了節點后短時間內變為靜止
還有什么?
最后在添加一個隨着移動而變化節點大小的效果,讓拖拽看起來更加舒服
在原有代碼內添加以下內容:
-(void) layout { int i = 1; for (CCNode *icon in s_icons) { CGPoint position = ccp((i-1) * 180, 0); float distance = fabsf(icon.position.x)/100; float scale = 1/(1+distance); icon.position = position; icon.scale = scale;//初始化縮放比例 if (![s_content.children containsObject:icon]) { [s_content addChild:icon]; } i++; } s_selectedIcon = [s_icons lastObject]; if ([s_delegate respondsToSelector:@selector(onSelectedIconChanged:)]) { [s_delegate onSelectedIconChanged:self]; } minX = - (i-1) * 180 - 100; maxX = 100; } -(void) update:(ccTime)dt; { [self updateMove:dt]; [self updateScale:dt];//更新縮放比例
} -(void) updateScale:(ccTime)dt; { for (CCNode *icon in s_icons) { CGPoint pt = [self convertToNodeSpace:[icon.parent convertToWorldSpace:icon.position]]; float distance = fabsf(pt.x)/100; icon.scale = 1/(1+distance); } }
測試
好了,代碼完成了,接下來測試一下效果
把HelloWorldLayer的初始化方法替換為以下代碼:
// create and initialize a Label CCLabelTTF *label = [CCLabelTTF labelWithString:@"Sawyer's Test" fontName:@"Marker Felt" fontSize:64]; // ask director for the window size CGSize size = [[CCDirector sharedDirector] winSize]; // position the label on the center of the screen label.position = ccp( size.width /2 , size.height/2 ); // add the label as a child to this Layer [self addChild: label]; // add the test selector to the layer NSMutableArray *icons = [NSMutableArray array]; int i = 10; while (i) { [icons addObject:[CCSprite spriteWithFile:@"Icon@2x.png"]]; i--; } SZSimplePhysicsDragSelector *selector = [[[SZSimplePhysicsDragSelector alloc] initWithIcons:icons] autorelease]; selector.position = self.anchorPointInPoints;
selector.Delegate = self; [self addChild:selector];
運行ios模擬器,你應該看到以下效果:
還算滿意,希望大家能夠用到各位的游戲中
測試代碼
測試代碼可以在以下鏈接下載
SimplePhysicsDragSelectorTest.zip