【问题标题】:3D Carousel effect on the iPadiPad 上的 3D 轮播效果
【发布时间】:2011-07-11 17:53:59
【问题描述】:

我正在尝试在 iPad 上实现一个 3D Carousel,由 UIViews 组成,效果类似于here 上显示的效果。

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

我正在尝试通过修改封面流动画来实现效果,但它并没有给出那种光滑的效果。

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

【问题讨论】:

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


    【解决方案1】:

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

    底部有完整的示例代码。我所做的是使用正弦和余弦来移动一些视图。其背后的基本理论是,位于原点的半径为 r 的圆外侧的角度为 a 的点在 (a*sin(r), a*cos(r)) 处。这是一个简单的极坐标到笛卡尔转换,从大多数国家教给青少年的三角学中应该很清楚;考虑一个斜边长度为 a 的直角三角形——另外两条边的长度是多少?

    然后您可以做的是减小 y 部分的半径以将圆转换为椭圆。椭圆看起来有点像你从一个角度看的圆。这忽略了透视的可能性,但要顺其自然。

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

    我通过调整要操作的 UIView 的仿射变换来影响位置和缩放。我直接在 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 上的 100 乘数乘以 2,加上宽度看法)。因此,如果您在 iPad 上运行手指跟踪,它会显得太慢。解决方案显然不是假设视图宽度为 180 度,而是将其留作练习!

    【讨论】:

    • 谢谢汤米!我要试试这个。同时有一个 +1 给你 :D
    • 非常好的例子。感谢分享。
    • 哇,效果太棒了!非常感谢!只需 3 分钟即可完成。
    • 我在 touchesMoved[touches containsObject:trackingTouch] 中遇到了崩溃。当发生多次触摸时似乎会发生。我还没弄明白。
    • @roocell:猜测是我没有捕捉到 touchesCancelled 的错误,这可能会使trackingTouch 成为悬空指针。我已经修改了代码。
    【解决方案2】:

    一个伟大的不同类型的coverflow的开源代码,包括循环的-https://github.com/demosthenese/iCarousel

    编辑:

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

    【讨论】:

    • 感谢分享这个 Sagar。为此+1 :)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-01-19
    • 2015-03-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-09
    相关资源
    最近更新 更多