Подтвердить что ты не робот

Эффект 3D-карусели на iPad

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

Отслеживание пальцев выполняется с помощью следующего UITouch за один раз через цикл touchsBegan/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", таймер, "анимационный тимер", два поплавка, "currentAngle" и "lastAngle" и UITouch, "trackingTouch". Очевидно, вы, вероятно, захотите использовать представления, которые не просто цветные квадраты, и вы можете настроить числа, которые я вытащил из воздуха для позиционирования. В противном случае он должен работать.

EDIT: я должен сказать, что я написал и протестировал этот код, используя шаблон приложения "Просмотр на основе iPhone" в Xcode. Создайте этот шаблон, выгрузите мои вещи в созданный контроллер представления и добавьте необходимые переменные-члены для тестирования. Тем не менее, я понял, что отслеживание касания предполагает 180 градусов - это полная ширина вашего вида, но метод setCarouselAngle: заставляет карусель всегда составлять 280 пунктов (что умножитель 100 на xPosition раз два, плюс ширина Посмотреть). Таким образом, отслеживание пальцев окажется слишком медленным, если вы запустите его на iPad. Решение, очевидно, не предполагает, что ширина представления составляет 180 градусов, но это остается в виде упражнения!