Полагаю, вы хотите, чтобы при взятии верхней карты в стопку она была перетасована в нижнюю часть стопки, открывая вторую карту в стопке, например:
В реальной жизни анимация намного плавнее.Мне пришлось использовать низкую частоту кадров, чтобы сделать анимированный GIF маленьким.
Один из способов сделать это - создать подкласс UIView
, который управляет стеком карточных представлений.Давайте назовем менеджера просмотра BoardView
.Мы дадим ему два открытых метода: один для перетаскивания верхней карты внизу, а другой - для перетаскивания нижней карты вверх.
@interface BoardView : UIView
- (IBAction)goToNextCard;
- (IBAction)goToPriorCard;
@end
@implementation BoardView {
BOOL _isRestacking;
}
Мы будем использовать переменную _isRestacking
для отслеживанияявляется ли представление доски движением карты в сторону.
A BoardView
рассматривает все свои подпредставления как представления карты.Он заботится о том, чтобы сложить их, центрируя верхнюю карту.На снимке экрана вы смещаете нижние карты на слегка рандомизированные суммы.Мы можем сделать его немного сексуальнее, вращая нижние карты случайным образом.Этот метод применяет небольшое случайное вращение к представлению:
- (void)jostleSubview:(UIView *)subview {
subview.transform = CGAffineTransformMakeRotation((((double)arc4random() / UINT32_MAX) - .5) * .2);
}
Мы хотим применить это к каждому подпредставлению по мере его добавления:
- (void)didAddSubview:(UIView *)subview {
[self jostleSubview:subview];
}
Система отправляет layoutSubviews
всякий раз, когдаразмер представления изменяется, или когда представлению дали новые подпредставления.Мы воспользуемся этим, чтобы выложить все карты в стопку в середине границ поля.Но если представление в данный момент анимирует карту, мы не хотим делать макет, потому что это убило бы анимацию.
- (void)layoutSubviews {
if (_isRestacking)
return;
CGRect bounds = self.bounds;
CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
UIView *topView = self.subviews.lastObject;
CGFloat offset = 10.0f / self.subviews.count;
for (UIView *view in self.subviews.reverseObjectEnumerator) {
view.center = center;
if (view == topView) {
// Make the top card be square to the edges of the screen.
view.transform = CGAffineTransformIdentity;
}
center.x -= offset;
center.y -= offset;
}
}
Теперь мы готовы справиться с перетасовкой.Чтобы «перейти к следующей карточке», нам нужно анимировать, переместив верхнюю карточку в сторону, затем переместить ее в нижнюю часть порядка размещения подпредставлений и затем оживить обратно к середине.Нам также нужно подтолкнуть позиции всех других карт, потому что они все переместились ближе к вершине стека.
- (void)goToNextCard {
if (self.subviews.count < 2)
return;
Сначала мы анимируем перемещение верхней карты в сторону,Чтобы это выглядело сексуально, мы поворачиваем карту по мере ее перемещения.
UIView *movingView = self.subviews.lastObject;
[UIView animateWithDuration:1 delay:0 options:UIViewAnimationCurveEaseInOut animations:^{
_isRestacking = YES;
CGPoint center = movingView.center;
center.x = -hypotf(movingView.frame.size.width / 2, movingView.frame.size.height / 2);
movingView.center = center;
movingView.transform = CGAffineTransformMakeRotation(-M_PI_4);
}
В блоке завершения мы перемещаем карту в конец стопки.
completion:^(BOOL finished) {
_isRestacking = NO;
[self sendSubviewToBack:movingView];
Ичтобы переместить теперь нижнюю карту обратно в стек и подтолкнуть все остальные карты, мы просто назовем layoutSubviews
.Но мы не должны вызывать layoutSubviews
напрямую, поэтому вместо этого мы используем соответствующие API: setNeedsLayout
, за которыми следует layoutIfNeeded
.Мы называем layoutIfNeeded
внутри блока анимации, чтобы карты анимировались на свои новые позиции.
[self setNeedsLayout];
[UIView animateWithDuration:1 delay:0 options:UIViewAnimationCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction animations:^{
[self jostleSubview:movingView];
[self layoutIfNeeded];
} completion:nil];
}];
}
Это конец goToNextCard
.Мы можем сделать goToPriorCard
аналогично:
- (void)goToPriorCard {
if (self.subviews.count < 2)
return;
UIView *movingView = [self.subviews objectAtIndex:0];
[UIView animateWithDuration:1 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
_isRestacking = YES;
CGPoint center = movingView.center;
center.x = -movingView.frame.size.height / 2;
movingView.center = center;
movingView.transform = CGAffineTransformMakeRotation(-M_PI_4);
} completion:^(BOOL finished) {
_isRestacking = NO;
UIView *priorTopView = self.subviews.lastObject;
[self bringSubviewToFront:movingView];
[self setNeedsLayout];
[UIView animateWithDuration:1 delay:0 options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction animations:^{
[self jostleSubview:priorTopView];
[self layoutIfNeeded];
} completion:nil];
}];
}
Когда у вас есть BoardView
, вам просто нужно прикрепить распознаватель жестов смахивания, который отправляет ему сообщение goToNextCard
, и другой распознаватель жестов смахиванием, который отправляет егосообщение goToPriorCard
.И вам нужно добавить несколько подпредставлений, которые будут действовать как карты.
Еще одна деталь: чтобы края карт выглядели гладкими, когда они толкаются, вам нужно установить UIViewEdgeAntialiasing
в YES
в вашемInfo.plist
.
Вы можете найти мой тестовый проект здесь: http://dl.dropbox.com/u/26919672/cardstack.zip