2009-05-03 15 views
6

Mam niestandardowy UIView, aby pokazać kafelkowy obraz.UIView skalowanie podczas animacji

- (void)drawRect:(CGRect)rect 
    { 
     ... 
     CGContextRef context = UIGraphicsGetCurrentContext();  
     CGContextClipToRect(context, 
      CGRectMake(0.0, 0.0, rect.size.width, rect.size.height));  
     CGContextDrawTiledImage(context, imageRect, imageRef); 
     ... 
    } 

Teraz próbuję animować rozmiar tego widoku.

To, czego się spodziewałem, to fakt, że powierzchnia kafelków rośnie, pozostawiając stałe rozmiary płytek. Zamiast tego podczas powiększania obszaru oryginalna zawartość widoku jest skalowana do nowego rozmiaru. Przynajmniej podczas animacji. Tak więc na początku i na końcu animacji wszystko jest w porządku. Podczas animacji płytki są zniekształcone.

Dlaczego CA próbuje skalować? Jak mogę temu zapobiec? Co mnie ominęło?

+0

Wygląda na to, że jest związany z trybem widoku. Ale ustawienie go na UIViewContentModeLeft też tak naprawdę nie rozwiązuje. Nie jest to już skalowanie, ale natychmiast pojawia się w nowym rozmiarze ramki. Ustawienie go na UIViewContentModeRedraw nie wydaje się w ogóle wywoływać drawRect podczas animacji. – tcurdt

Odpowiedz

15

Jeśli Core Animation musiała oddzwonić do twojego kodu dla każdej klatki animacji, nigdy nie byłaby tak szybka, jak to jest, a animacja niestandardowych właściwości była długo oczekiwaną funkcją i często zadawanymi pytaniami dla CA na Macu.

Korzystanie z UIViewContentModeRedraw jest na dobrej drodze i jest również najlepsze, co dostaniesz z CA. Problem wynika z punktu widzenia UIKit, że ramka ma tylko dwie wartości: wartość na początku przejścia i wartość na końcu przejścia, i to właśnie widzisz. Jeśli spojrzysz na dokumenty architektury Core Animation, zobaczysz, jak CA ma prywatną reprezentację wszystkich właściwości warstw i ich wartości zmieniających się w czasie. Właśnie tam odbywa się interpolacja klatek i nie można powiadomić o zmianach w tym momencie.

Jedynym sposobem jest użycie NSTimer (lub performSelector:withObject:afterDelay:), aby zmienić ramkę widoku w czasie, w staroświecki sposób.

+0

Dzięki, użyłem NSTimer, aby to zrobić (Call drawRect wraz z animacją). To działa dla mnie. – Strong

+0

@ jazou2012 Nie powinieneś wywoływać 'drawRect', powinieneś wywołać' setNeedsDisplay'. –

+0

@ Jean-PhilippePellet Tak, masz rację. 'setNeedsDisplay' wywoła' drawRect'! I to też miałem na myśli. – Strong

2

Jak wskazuje Duncan, animacja rdzenia nie będzie odnawiać zawartości warstwy UIView każdej klatki podczas zmiany jej rozmiaru. Musisz to zrobić sam, używając timera.

Faceci w Omni zamieścili ładny przykład, jak tworzyć animacje na podstawie własnych niestandardowych właściwości, które mogą mieć zastosowanie w Twojej sprawie. Ten przykład wraz z wyjaśnieniem, jak to działa, można znaleźć pod adresem here.

4

Miałem podobny problem w innym kontekście, natknąłem się na ten wątek i znalazłem propozycję Duncana ustawienia ramki w NSTimer. I wdrożone ten kod, aby osiągnąć ten sam:

CGFloat kDurationForFullScreenAnimation = 1.0; 
CGFloat kNumberOfSteps = 100.0; // (kDurationForFullScreenAnimation/kNumberOfSteps) will be the interval with which NSTimer will be triggered. 
int gCurrentStep = 0; 
-(void)animateFrameOfView:(UIView*)inView toNewFrame:(CGRect)inNewFrameRect withAnimationDuration:(NSTimeInterval)inAnimationDuration numberOfSteps:(int)inSteps 
{ 
    CGRect originalFrame = [inView frame]; 

    CGFloat differenceInXOrigin = originalFrame.origin.x - inNewFrameRect.origin.x; 
    CGFloat differenceInYOrigin = originalFrame.origin.y - inNewFrameRect.origin.y; 
    CGFloat stepValueForXAxis = differenceInXOrigin/inSteps; 
    CGFloat stepValueForYAxis = differenceInYOrigin/inSteps; 

    CGFloat differenceInWidth = originalFrame.size.width - inNewFrameRect.size.width; 
    CGFloat differenceInHeight = originalFrame.size.height - inNewFrameRect.size.height; 
    CGFloat stepValueForWidth = differenceInWidth/inSteps; 
    CGFloat stepValueForHeight = differenceInHeight/inSteps; 

    gCurrentStep = 0; 
    NSArray *info = [NSArray arrayWithObjects: inView, [NSNumber numberWithInt:inSteps], [NSNumber numberWithFloat:stepValueForXAxis], [NSNumber numberWithFloat:stepValueForYAxis], [NSNumber numberWithFloat:stepValueForWidth], [NSNumber numberWithFloat:stepValueForHeight], nil]; 
    NSTimer *aniTimer = [NSTimer timerWithTimeInterval:(kDurationForFullScreenAnimation/kNumberOfSteps) 
               target:self 
               selector:@selector(changeFrameWithAnimation:) 
               userInfo:info 
               repeats:YES]; 
    [self setAnimationTimer:aniTimer]; 
    [[NSRunLoop currentRunLoop] addTimer:aniTimer 
           forMode:NSDefaultRunLoopMode]; 
} 

-(void)changeFrameWithAnimation:(NSTimer*)inTimer 
{ 
    NSArray *userInfo = (NSArray*)[inTimer userInfo]; 
    UIView *inView = [userInfo objectAtIndex:0]; 
    int totalNumberOfSteps = [(NSNumber*)[userInfo objectAtIndex:1] intValue]; 
    if (gCurrentStep<totalNumberOfSteps) 
    { 
     CGFloat stepValueOfXAxis = [(NSNumber*)[userInfo objectAtIndex:2] floatValue]; 
     CGFloat stepValueOfYAxis = [(NSNumber*)[userInfo objectAtIndex:3] floatValue]; 
     CGFloat stepValueForWidth = [(NSNumber*)[userInfo objectAtIndex:4] floatValue]; 
     CGFloat stepValueForHeight = [(NSNumber*)[userInfo objectAtIndex:5] floatValue]; 

     CGRect currentFrame = [inView frame]; 
     CGRect newFrame; 
     newFrame.origin.x = currentFrame.origin.x - stepValueOfXAxis; 
     newFrame.origin.y = currentFrame.origin.y - stepValueOfYAxis; 
     newFrame.size.width = currentFrame.size.width - stepValueForWidth; 
     newFrame.size.height = currentFrame.size.height - stepValueForHeight; 

     [inView setFrame:newFrame]; 

     gCurrentStep++; 
    } 
    else 
    { 
     [[self animationTimer] invalidate]; 
    } 
} 

I okazało się, że to zajmuje dużo czasu, aby ustawić ramkę i timer, aby zakończyć jego działanie. Prawdopodobnie zależy to od tego, jak głęboka jest hierarchia widoków, do której wywołuje się parametr -setFrame za pomocą tego podejścia. I prawdopodobnie jest to powód, dla którego widok jest najpierw ponownie zmieniany, a następnie animowany do źródła w sdk. A może jest jakiś problem w mechanizmie timera w moim kodzie, z powodu którego wydajność jest utrudniona?

Działa, ale działa bardzo wolno, może dlatego, że moja hierarchia widoków jest zbyt głęboka.

+2

Twój kod działa doskonale dla mojego widoku (widok tła i nakładka tekstowa). Konieczne były 2 zmiany: 1. [unieważnienie self.animationTimer] u góry changeFrameWithAnimation, 2. zmiana czasu trwania animacji na 0.3, a także liczba kroków. Tak jak teraz, wywołujesz timer w czasie 10 ms, ale musisz robić to co 16ms przy częstotliwości odświeżania 60 Hz. – Jon

+0

Dzięki @ Jon, havent myślał o częstotliwości odświeżania 60Hz, ale w moim przypadku cały proces spowolniłby mój widok hierarchii był ciężki, a więc to było więcej czasu niż oczekiwano. Musiał więc zmienić to wymaganie! –

1

Jeśli to nie jest wielka animacja lub wydajność nie jest problem w czasie trwania animacji, można użyć CADisplayLink. Wygładza animację skalowania niestandardowych UibezierPaths UIView na przykład. Poniżej znajduje się przykładowy kod, który można dostosować do obrazu.

Podpisy iv mojego widoku niestandardowego zawierają displayLink jako CADisplayLink i dwa CGRekty: toFrame i fromFrame, jak również czas trwania i startTime.

- (void)yourAnimationMethodCall 
{ 
    toFrame = <get the destination frame> 
    fromFrame = self.frame; // current frame. 

    [displayLink removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; // just in case  
    displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(animateFrame:)]; 
    startTime = CACurrentMediaTime(); 
    duration = 0.25; 
    [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; 
} 

- (void)animateFrame:(CADisplayLink *)link 
{ 
    CGFloat dt = ([link timestamp] - startTime)/duration; 

    if (dt >= 1.0) { 
     self.frame = toFrame; 
     [displayLink removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; 
     displayLink = nil; 
     return; 
    } 

    CGRect f = toFrame; 
    f.size.height = (toFrame.size.height - fromFrame.size.height) * dt + fromFrame.size.height; 
    self.frame = f; 
} 

Należy zauważyć, że nie przetestowałem jego działania, ale działa ono sprawnie.

1

Natknąłem się na to pytanie, próbując animować zmianę rozmiaru na UIView z granicą. Mam nadzieję, że ci, którzy podobnie tutaj przybędą, mogą skorzystać z tej odpowiedzi. Początkowo byłem tworząc podklasę niestandardowy UIView i przesłanianie metody drawRect następująco:

- (void)drawRect:(CGRect)rect 
{ 
    CGContextRef ctx = UIGraphicsGetCurrentContext(); 

    CGContextSetLineWidth(ctx, 2.0f); 
    CGContextSetStrokeColorWithColor(ctx, [UIColor orangeColor].CGColor); 
    CGContextStrokeRect(ctx, rect); 

    [super drawRect:rect]; 
} 

Doprowadziło to do skalowania problemy podczas łączenia z animacji jak wspominają inni. Górna i dolna krawędź byłyby zbyt grube lub dwa cienkie i wyglądałyby na przeskalowane. Ten efekt jest łatwo zauważalny podczas przełączania się na wolne animacje.

Rozwiązaniem było zrezygnować z niestandardową podklasę i zamiast korzystać z tego podejścia:

[_borderView.layer setBorderWidth:2.0f]; 
[_borderView.layer setBorderColor:[UIColor orangeColor].CGColor]; 

to naprawić problem ze skalowaniem obramowanie na UIView podczas animacji.

Powiązane problemy