zoukankan      html  css  js  c++  java
  • 在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

  • 相关阅读:
    安装 Android 运行环境
    Sea.js
    css hack 兼容性
    solr全文检索基本原理
    Solr初步学习
    jquery中ajax的用法
    Javascript的模块化编程
    html 标签
    CSS盒子模型
    python 初学03 Eric+PyQt+python IDE与界面程序
  • 原文地址:https://www.cnblogs.com/sawyerzhu/p/2636275.html
Copyright © 2011-2022 走看看