在Cocos2d中實現能夠慣性拖動的選擇界面


蘋果的應用講究用戶體驗

有的時候仔細想想

的確,很多細節決定了用戶體驗

比如說慣性拖動

可以說之前沒有任何一家廠商能把觸摸慣性拖動做的像蘋果的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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM