我正在尝试在 iPad 上实现一个 3D 旋转木马,由 UIView 组成,效果类似于 here 中显示的效果.
我在 SO 上经历过许多类似的问题,但没有找到任何令人满意的答案或根本没有答案。
我试图通过修改 coverflow 动画来实现这种效果,但它并没有提供那种流畅的效果。
有没有人实现过这个?(通过 quartz 和 openGL 开放征求建议)
最佳答案
无需深入研究 Quartz 或 OpenGL,假设您不介意前面的模糊。您链接到的页面透视错误(这就是背景中的图像看起来比前景中的图像移动得更快的原因),因此数学可能有点虚幻。
底部有完整的示例代码。我所做的是使用正弦和余弦来移动一些 View 。其背后的基本理论是位于原点上的半径为 r 的圆的外侧角度为 a 的点位于 (a*sin(r), a*cos(r))。这是一个简单的极坐标到笛卡尔的转换,从大多数国家教给青少年的三角学中应该很清楚;考虑一个斜边长度为 a 的直角三角形 — 其他两条边的长度是多少?
然后您可以做的是减小 y 部分的半径,将圆转换为椭圆。椭圆看起来有点像你从一个角度看的圆。这忽略了透视的可能性,但顺其自然。
然后我通过使大小与 y 坐标成比例来伪造透视图。我正在调整 alpha,就像您链接到的网站一样模糊,希望这对您的应用程序来说足够好。
我通过调整要操作的 UIView 的仿射变换来影响位置和比例。我直接在 UIView 上设置 alpha。我还调整了 View 层上的 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”、两个 float “currentAngle”和“lastAngle”,以及一个 UITouch“trackingTouch”。显然,您可能想要使用不仅仅是彩色方 block 的 View ,并且您可能想要调整我凭空提取的用于定位的数字。否则,它应该可以正常工作。
编辑:我应该说,我在 Xcode 中使用 iPhone“基于 View 的应用程序”模板编写并测试了这段代码。创建该模板,将我的东西转储到创建的 View Controller 中,并添加必要的成员变量进行测试。但是,我已经意识到触摸跟踪假定 180 度是您 View 的整个宽度,但是 setCarouselAngle: 方法强制旋转木马始终跨 280 个点(这是 xPosition 上的 100 乘数乘以二,加上看法)。因此,如果您在 iPad 上运行手指跟踪,它会显得太慢。解决方案显然不是假设 View 宽度为 180 度,但这留作练习!
关于ipad - iPad 上的 3D 轮播效果,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/5243614/