iPad上的3D旋转木马效果

时间:2011-03-09 09:21:03

标签: ipad ios opengl-es quartz-graphics ios-4.2

我正在尝试在iPad上实现3D旋转木马,包括UIViews,效果类似here上显示的效果。

我在SO上经历了许多类似的问题,但没有找到任何令人满意的答案或根本没有答案。

我试图通过修改coverflow动画来达到效果,但它不会产生光滑的效果。

有没有人实现过这个?(通过quartz和openGL打开建议)

2 个答案:

答案 0 :(得分:66)

无需深入研究Quartz或OpenGL,假设您不介意模糊。您链接到的页面会导致视角错误(这就是为什么背景中的图像看起来比前景中的图像移动得更快),所以数学可能会有点烟雾和镜子。

底部有完整的示例代码。我所做的是使用正弦和余弦来移动一些观点。其背后的基本理论是位于原点上的半径r的圆外侧的角度α的点为(a * sin(r),a * cos(r))。这是笛卡尔转换的简单极性,应该从大多数国家教给他们十几岁的三角学中清楚地看出来;考虑一个直角三角形,斜边长度为a - 另外两边的长度是多少?

您可以做的是减小y部分的半径以将圆转换为椭圆。椭圆看起来有点像圆形,从一个角度看。这忽视了透视的可能性,但是随之而去。

然后通过使大小与y坐标成比例来伪造透视。我正在调整alpha,就像你链接到的网站一样模糊,希望对你的应用程序来说足够好。

通过调整我想要操作的UIViews的仿射变换来影响位置和比例。我直接在UIView上设置了alpha。我还在视图的图层上调整zPosition(这就是导入QuartzCore的原因)。 zPosition就像CSS z位置;它不影响比例,只影响绘图顺序。因此,通过设置它等于我计算的比例,它只是说“在较小的东西上绘制更大的东西”,给我们正确的绘制顺序。

通过touchesBegan / touchesMoved / touchesEnded循环一次跟踪一个UITouch来完成手指跟踪。如果没有跟踪手指并且开始一些触摸,则其中一个触摸成为被跟踪的手指。如果它移动则旋转木马。

为了创造惯性,我有一个附加到计时器的方法跟踪当前角度与前一个刻度的角度。这种差异用作速度,同时按比例缩小以产生惯性。

计时器在手指上启动,因为旋转木马应该自动开始旋转。如果旋转木马停止或新手指放下,它就会停止。

请您填写空白,我的代码是:

#import <QuartzCore/QuartzCore.h>

@implementation testCarouselViewController

- (void)setCarouselAngle:(float)angle
{
    // we want to step around the outside of a circle in
    // linear steps; work out the distance from one step
    // to the next
    float angleToAdd = 360.0f / [carouselViews count];

    // apply positions to all carousel views
    for(UIView *view in carouselViews)
    {
        float angleInRadians = angle * M_PI / 180.0f;

        // get a location based on the angle
        float xPosition = (self.view.bounds.size.width * 0.5f) + 100.0f * sinf(angleInRadians);
        float yPosition = (self.view.bounds.size.height * 0.5f) + 30.0f * cosf(angleInRadians);

        // get a scale too; effectively we have:
        //
        //  0.75f   the minimum scale
        //  0.25f   the amount by which the scale varies over half a circle
        //
        // so this will give scales between 0.75 and 1.25. Adjust to suit!
        float scale = 0.75f + 0.25f * (cosf(angleInRadians) + 1.0);

        // apply location and scale
        view.transform = CGAffineTransformScale(CGAffineTransformMakeTranslation(xPosition, yPosition), scale, scale);

        // tweak alpha using the same system as applied for scale, this time
        // with 0.3 the minimum and a semicircle range of 0.5
        view.alpha = 0.3f + 0.5f * (cosf(angleInRadians) + 1.0);

        // setting the z position on the layer has the effect of setting the
        // draw order, without having to reorder our list of subviews
        view.layer.zPosition = scale;

        // work out what the next angle is going to be
        angle += angleToAdd;
    }
}

- (void)animateAngle
{
    // work out the difference between the current angle and
    // the last one, and add that again but made a bit smaller.
    // This gives us inertial scrolling.
    float angleNow = currentAngle;
    currentAngle += (currentAngle - lastAngle) * 0.97f;
    lastAngle = angleNow;

    // push the new angle into the carousel
    [self setCarouselAngle:currentAngle];

    // if the last angle and the current one are now
    // really similar then cancel the animation timer
    if(fabsf(lastAngle - currentAngle) < 0.001)
    {
        [animationTimer invalidate];
        animationTimer = nil;
    }
}

// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad 
{
    [super viewDidLoad];

    // create views that are an 80x80 rect, centred on (0, 0)
    CGRect frameForViews = CGRectMake(-40, -40, 80, 80);

    // create six views, each with a different colour. 
    carouselViews = [[NSMutableArray alloc] initWithCapacity:6];
    int c = 6;
    while(c--)
    {
        UIView *view = [[UIView alloc] initWithFrame:frameForViews];

        // We don't really care what the colours are as long as they're different,
        // so just do anything
        view.backgroundColor = [UIColor colorWithRed:(c&4) ? 1.0 : 0.0 green:(c&2) ? 1.0 : 0.0 blue:(c&1) ? 1.0 : 0.0 alpha:1.0];

        // make the view visible, also add it to our array of carousel views
        [carouselViews addObject:view];
        [self.view addSubview:view];
    }

    currentAngle = lastAngle = 0.0f;
    [self setCarouselAngle:currentAngle];

    /*
        Note: I've omitted viewDidUnload for brevity; remember to implement one and
        clean up after all the objects created here
    */
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // if we're not already tracking a touch then...
    if(!trackingTouch)
    {
        // ... track any of the new touches, we don't care which ...
        trackingTouch = [touches anyObject];

        // ... and cancel any animation that may be ongoing
        [animationTimer invalidate];
        animationTimer = nil;
        lastAngle = currentAngle;
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    // if our touch moved then...
    if([touches containsObject:trackingTouch])
    {
        // use the movement of the touch to decide
        // how much to rotate the carousel
        CGPoint locationNow = [trackingTouch locationInView:self.view];
        CGPoint locationThen = [trackingTouch previousLocationInView:self.view];

        lastAngle = currentAngle;
        currentAngle += (locationNow.x - locationThen.x) * 180.0f / self.view.bounds.size.width;
        // the 180.0f / self.view.bounds.size.width just says "let a full width of my view
        // be a 180 degree rotation"

        // and update the view positions
        [self setCarouselAngle:currentAngle];
    }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    // if our touch ended then...
    if([touches containsObject:trackingTouch])
    {
        // make sure we're no longer tracking it
        trackingTouch = nil;

        // and kick off the inertial animation
        animationTimer = [NSTimer scheduledTimerWithTimeInterval:0.02 target:self selector:@selector(animateAngle) userInfo:nil repeats:YES];
    }
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    // treat cancelled touches exactly like ones that end naturally
    [self touchesEnded:touches withEvent:event];
}

@end

所以相关的成员变量是一个可变数组,'carouselViews',一个计时器,'animationTimer',两个浮点数,'currentAngle'和'lastAngle',以及一个UITouch,'trackingTouch'。显然你可能想要使用不仅仅是彩色方块的视图,你可能想要调整我用来定位的数字。否则,它应该工作。

编辑:我应该说,我使用Xcode中的iPhone“基于视图的应用程序”模板编写并测试了此代码。创建该模板,将我的东西转储到创建的视图控制器中,并添加必要的成员变量进行测试。但是,我已经意识到触摸跟踪假定180度是视图的整个宽度,但是setCarouselAngle:方法强制旋转木马始终为280点(这是xPosition乘以2的100乘数加上a的宽度视图)。因此,如果您在iPad上运行,手指跟踪似乎会太慢。解决方案显然不是假设视图宽度是180度,而是留作练习!

答案 1 :(得分:5)

一个很棒的开源代码,包含圆形的 - https://github.com/demosthenese/iCarousel

修改

存储库的新路径 - https://github.com/nicklockwood/iCarousel