我正在尝试在iPad上实现3D旋转木马,包括UIViews,效果类似here上显示的效果。
我在SO上经历了许多类似的问题,但没有找到任何令人满意的答案或根本没有答案。
我试图通过修改coverflow动画来达到效果,但它不会产生光滑的效果。
有没有人实现过这个?(通过quartz和openGL打开建议)
答案 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
修改强>