2011-03-09 18 views
13

Estoy tratando de implementar un carrusel 3D en el iPad, que consiste en UIViews, un efecto como el que se muestra en here.Efecto 3D Carrusel en el iPad

He tenido muchas preguntas similares en SO, pero no he encontrado ninguna respuesta staisfactory o ninguna respuesta en absoluto.

Estoy tratando de lograr el efecto mediante la modificación de la animación de flujo de cobertura, pero simplemente no da ese efecto resbaladizo.

alguna de las personas a cabo esto? (Abierto a sugerencias a través de cuarzo y OpenGL ambos)

Respuesta

65

No hay necesidad de sumergirse en cuarzo u OpenGL, suponiendo que no le importe lo anterior, la falta de definición. La página a la que se vincula obtiene la perspectiva equivocada (es por eso que las imágenes en el fondo parecen moverse más rápido que las que están en primer plano), por lo que las matemáticas pueden ser un poco de humo y espejos.

Hay un código de ejemplo completo en la parte inferior. Lo que hice fue usar seno y coseno para mover algunos puntos de vista. La teoría básica detrás de esto es que el punto en el ángulo a en el exterior de un círculo de radio r posicionado en el origen está en (a * sin (r), a * cos (r)). Esa es una conversión simple de polar a cartesiana, y debe quedar clara desde la trigonometría que la mayoría de los países enseñan a sus adolescentes; considere un triángulo rectángulo con una hipotenusa de longitud a - ¿qué longitud tienen los otros dos lados?

Lo que puede hacer entonces es reducir el radio de la parte y para convertir el círculo en una elipse. Y una elipse se parece un poco a un círculo que estás mirando desde un ángulo. Eso ignora la posibilidad de perspectiva, pero vaya con eso.

Luego falsifico la perspectiva haciendo un tamaño proporcional a la coordenada y. Y estoy ajustando el alfa de una manera como el sitio al que se vincula difumina, con la esperanza de que sea lo suficientemente bueno para su aplicación.

Efectuo la posición y la escala ajustando la transformación afín de las UIViews que quiero manipular. Configuré el alfa directamente en UIView. También ajusto zPosition en las capas de la vista (razón por la cual se importa QuartzCore). ZPosition es como la posición CSS z; no afecta la escala, solo el orden de dibujo. Entonces al establecerlo igual a la escala que he calculado, solo dice "dibujar cosas más grandes encima de cosas más pequeñas", dándonos el orden de dibujo correcto.

El seguimiento de los dedos se realiza siguiendo una UITouch a la vez mediante el ciclo touchesBegan/touchesMoved/touchesEnded. Si no se hace un seguimiento de ningún dedo y comienzan algunos toques, uno de ellos se convierte en el dedo que se rastrea. Si se mueve, entonces el carrusel se gira.

Para crear la inercia, tengo un pequeño método que se conecta a un temporizador que hace un seguimiento del ángulo actual versus el ángulo de una marca anterior. Esa diferencia se usa como una velocidad y al mismo tiempo se escala hacia abajo para producir inercia.

El temporizador se inicia con el dedo hacia arriba, ya que es cuando el carrusel debe comenzar a girar por su propia voluntad. Se detiene si el carrusel se detiene o si se coloca un nuevo dedo.

Dejando para llenar los espacios en blanco, mi código es:

#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 

variables miembro Así relevantes son una matriz mutable, 'carouselViews', un temporizador, 'animationTimer', dos flotadores, 'currentAngle' y 'lastAngle', y una UITouch, 'trackingTouch'. Obviamente, es probable que desee utilizar vistas que no sean solo cuadrados de colores y es posible que desee ajustar los números que he sacado de la nada para posicionarlos.De lo contrario, debería funcionar.

EDITAR: Debería decir que escribí y probé este código usando la plantilla de "aplicación basada en la vista" de iPhone en Xcode. Cree esa plantilla, descargue mis cosas en el controlador de vista creado y agregue las variables miembro necesarias para probar. Sin embargo, me he dado cuenta de que el seguimiento táctil supone 180 grados el ancho total de su vista, pero el método setCarouselAngle: obliga al carrusel a tener siempre 280 puntos (ese es el multiplicador 100 en xPosition multiplicado por dos, más el ancho de un ver). Entonces, el seguimiento de los dedos parecerá demasiado lento si lo ejecuta en un iPad. La solución obviamente no es suponer que el ancho de la vista es de 180 grados, ¡pero eso es solo un ejercicio!

+0

Gracias Tommy! Voy a intentar esto. Mientras tanto, hay un +1 para ti: D – Vin

+1

Eso fue realmente maravilloso. ¡Ojalá pudiera votarte más! – Vin

+0

Buen ejemplo. Gracias por compartir. –