3D карусель на iPad - PullRequest
       22

3D карусель на iPad

13 голосов
/ 09 марта 2011

Я пытаюсь реализовать 3D-карусель на iPad, состоящую из UIViews, эффект, подобный тому, что показан над здесь .

Я прошел через много похожих вопросов по SO, но не нашел ни удовлетворительных ответов, ни ответов вообще.

Я пытаюсь добиться эффекта за счет изменения анимации прикрытия, но это просто не дает этого скользкого эффекта.

Кто-нибудь реализовал это? (Открыт для предложений через кварц и openGL оба)

Ответы [ 2 ]

66 голосов
/ 10 марта 2011

Нет необходимости погружаться в Quartz или OpenGL, если вы не возражаете против размытия. Страница, на которую вы ссылаетесь, дает неправильную перспективу (поэтому изображения на заднем плане выглядят более быстрыми, чем на переднем плане), поэтому в математике может быть немного дыма и зеркал.

Ниже приведен полный пример кода. То, что я сделал, использовало синус и косинус, чтобы двигать некоторые взгляды. Основная теория позади этого состоит в том, что точка под углом a снаружи круга радиуса r, расположенного в начале координат, находится в (a * sin (r), a * cos (r)). Это простая полярно-декартова конверсия, и она должна быть понятна из тригонометрии, которую большинство стран преподают своим подросткам; рассмотрим прямоугольный треугольник с гипотенузой длины а - какой длины две другие стороны?

То, что вы можете затем сделать, это уменьшить радиус части y, чтобы преобразовать круг в эллипс. И эллипс выглядит как круг, на который вы смотрите под углом. Это игнорирует возможность перспективы, но идти с этим.

Затем я подделываю перспективу, делая размер, пропорциональный координате y. И я настраиваю альфа так, как сайт, на который вы ссылаетесь, размыт, в надежде, что этого достаточно для вашего приложения.

Я влияю на положение и масштаб, регулируя аффинное преобразование UIViews, которыми я хочу манипулировать. Я установил альфа прямо на UIView. Я также настраиваю zPosition на слоях вида (именно поэтому QuartzCore импортируется). ZPosition похож на CSS z-позицию; это не влияет на масштаб, только порядок рисования. Поэтому, установив его в соответствии с масштабом, который я вычислил, он просто говорит: «Нарисуйте большие вещи поверх меньших», что дает нам правильный порядок рисования.

Отслеживание пальцев выполняется по одному UITouch за раз в цикле touchesBegan / touchesMoved / touchesEnded. Если палец не отслеживается, и начинаются некоторые касания, одно из них становится отслеживаемым пальцем. Если он движется, то карусель вращается.

Чтобы создать инерцию, у меня есть небольшой метод, который привязывает таймер к отслеживанию текущего угла в сравнении с углом на один такт раньше. Эта разница используется как скорость и одновременно масштабируется вниз для создания инерции.

Таймер запускается пальцем вверх, так как именно тогда карусель должна начать вращаться по собственному желанию. Он останавливается, если карусель останавливается или новый палец опущен вниз.

Оставив вас заполнить пробелы, мой код:

#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'. Очевидно, что вы, вероятно, захотите использовать виды, которые не являются просто цветными квадратами, и вы можете настроить числа, которые я вытащил из воздуха для позиционирования. В противном случае, это должно просто работать.

РЕДАКТИРОВАТЬ: я должен сказать, что я написал и протестировал этот код с помощью шаблона iPhone 'View-based application' в Xcode. Создайте этот шаблон, поместите мои данные в созданный контроллер представления и добавьте необходимые переменные-члены для тестирования. Тем не менее, я понял, что сенсорное отслеживание предполагает, что 180 градусов - это полная ширина вашего вида, но setCarouselAngle: метод заставляет карусель всегда иметь 280 точек в поперечнике (это множитель 100 на xPosition, умноженный на два, плюс ширина Посмотреть). Таким образом, отслеживание пальцев будет слишком медленным, если вы запустите его на iPad. Очевидно, что решение состоит в том, чтобы не предполагать, что ширина обзора составляет 180 градусов, но это оставлено в качестве упражнения!

5 голосов
/ 23 мая 2011

Отличный код с открытым исходным кодом различного рода, включая круговой - https://github.com/demosthenese/iCarousel

Редактировать:

Новый путь к хранилищу - https://github.com/nicklockwood/iCarousel

...