Очень нестандартная анимация UIButton в iOS

У меня есть пользовательская кнопка, которая является моим собственным подклассомUIButton, Это похоже на окруженную стрелку, начинающуюся под некоторым угломstartAngle закончить в некоторыхendAngle=startAngle+1.5*M_PI. startAngle является свойством кнопки, которое затем используется в ееdrawRect: метод. Я хочу, чтобы при нажатии этой кнопки эта стрелка непрерывно вращалась на 2Pi вокруг ее центра. Поэтому я подумал, что я могу легко использовать[UIView beginAnimations: context:] но, по-видимому, его нельзя использовать, поскольку он не позволяет анимировать пользовательские свойства. CoreAnimation также не подходит, поскольку он только оживляетCALayer свойства.

Так что это самый простой способ реализовать анимацию пользовательского свойстваUIView подкласс в iOS? Или, может быть, я что-то упустил и возможно с уже упомянутыми техниками?

Спасибо.

 k06a13 апр. 2012 г., 00:07
Просто добавьте вращение для просмотра. Вам не нужно перерисовывать ... Я имею в виду вид вашей кнопки. Ваша CustomButton является подклассом UIButton, а подкласс UIView.
 BartoNaz13 апр. 2012 г., 00:10
Благодарю. Это приемлемо в этом конкретном случае, но не универсально. У меня есть другая кнопка, где она не поможет, поскольку повернутая часть не зависит от других элементов кнопки, поэтому мне нужно точно анимировать свойство.
 BartoNaz13 апр. 2012 г., 00:37
Да, таймер выглядит подходящим. Спасибо.
 Christian Schnorr14 апр. 2012 г., 17:09
Нет, не используйте NSTtimer. Перейти на CADisplayLink.
 k06a13 апр. 2012 г., 00:23
Вы можете использовать таймер: [NSTimer executeSelector: withObject: afterDelay:]. Должно работать нормально на 24-30 fps ...

Ответы на вопрос(3)

- (void)onImageAction:(id)sende,r
{
    UIButton *iconButton = (UIButton *)sender;
    if(isExpand)
{
    isExpand = FALSE;

    // With Concurrent Block Programming:
    [UIView animateWithDuration:0.4 animations:^{
        [iconButton setFrame:[[btnFrameList objectAtIndex:iconButton.tag] CGRectValue]];
    } completion: ^(BOOL finished) {
        [self animationDidStop:@"Expand" finished:YES context:nil];
    }];
}
else
{
    isExpand = TRUE;

    [UIView animateWithDuration:0.4 animations:^{
        [iconButton setFrame:CGRectMake(30,02, 225, 205)];
    }];

    for(UIButton *button in [viewThumb subviews])
    {
        [button setUserInteractionEnabled:FALSE];
        //[button setHidden:TRUE];
    }
    [viewThumb bringSubviewToFront:iconButton];

    [iconButton setUserInteractionEnabled:TRUE];
   // [iconButton setHidden:FALSE];
    }
}

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

Так что в моем случае у меня был подкласс UIButton, который рисовал изогнутую стрелку, которая начиналась под некоторым угломFloat32 angle; который является основным свойством, с которого начинается рисование всей стрелки. Это означает, что просто изменение значения угла повернёт стрелку целиком. Поэтому для анимации этого вращения я поместил следующие строки в заголовочный файл моего класса:

NSTimer* timer;
Float32 animationDuration;  // Duration of one animation loop
Float32 animationFrameRate;  // Frames per second
Float32 initAngle;  // Represents the initial angle 
Float32 angle;  // Angle used for drawing
UInt8 nFrames;  // Number of played frames

-(void)startAnimation;  // Method to start the animation
-(void)animate:(NSTimer*) timer;  // Method for drawing one animation step and stopping the animation

Теперь в файле реализации я установил значения длительности и частоты кадров моей анимации и начальные углы рисования:

initAngle=0.75*M_PI;
angle=initAngle;
animationDuration=1.5f;  // Arrow makes full 360° in 1.5 seconds
animationFrameRate=15.f;  // Frame rate will be 15 frames per second
nFrames=0;  // The animation hasn't been played yet

Для запуска анимации нам нужно создать экземпляр NSTimer, который будет вызывать методanimate:(NSTimer*) timer каждые 1/15 секунды:

-(void)startAnimation
{
   timer = [NSTimer scheduledTimerWithTimeInterval:1.f/animationFrameRate target:self selector:@selector(animate:) userInfo:nil repeats:YES];
}

Этот таймер позвонитanimate: немедленно, а затем повторяйте его каждые 1/15 секунды, пока он не будет остановлен вручную.

Итак, теперь идет реализация нашего метода для анимации одного шага:

-(void)animate:(NSTimer *)timer
{
   nFrames++;  // Incrementing number of played frames
   Float32 animProgress = nFrames/(animationDuration*animationFrameRate); // The current progress of animation
   angle=initAngle+animProgress*2.f*M_PI;  // Setting angle to new value with added rotation corresponding to current animation progress
   if (animProgress>=1.f) 
   {  // Stopping animation when progress is >= 1
      angle=initAngle; // Setting angle to initial value to exclude rounding effects
      [timer invalidate];  // Stopping the timer
      nFrames=0;  // Resetting counter of played frames for being able to play animation again
   }
   [self setNeedsDisplay];  // Redrawing with updated angle value
}

Первое, что я хочу отметить, это то, что для меня просто линия сравненияangle==initAngle не работал из-за эффектов округления. Они не одинаковы после полного вращения. Вот почему я проверяю, достаточно ли они достаточно близки, и затем устанавливаю значение угла в исходное значение, чтобы заблокировать небольшой сдвиг значения угла после многих повторяющихся циклов анимации. И чтобы быть полностью правильным, этот код должен также управлять преобразованием углов, чтобы всегда быть между 0 и 2 * M_PI с чем-то вроде этого:

angle=normalizedAngle(initAngle+animProgress*2.f*M_PI);

где

Float32 normalizedAngle(Float32 angle)
{
   while(angle>2.f*M_PI) angle-=2.f*M_PI;
   while(angle<0.f) angle+=2.f*M_PI;
   return angle
}

И еще одна важная вещь заключается в том, что, к сожалению, я не знаю какого-либо простого способа применения по-прежнему анимаций по умолчанию, облегченных, облегченных или других, для этого вида ручной анимации. Я думаю, что этого не существует. Но, конечно, это можно сделать руками. Линия, стоящая за этой функцией синхронизации

Float32 animProgress = nFrames/(animationDuration*animationFrameRate);

Это можно рассматривать какFloat32 y = x;, это означает линейное поведение, постоянную скорость, которая равна скорости времени. Но вы можете изменить его, чтобы быть какy = cos(x) или жеy = sqrt(x) или жеy = pow(x,3.f) который даст некоторое нелинейное поведение. Вы можете думать сами, принимая во внимание, что x перейдет от 0 (начало анимации) к 1 (конец анимации).

Для лучшего вида кода лучше создать независимую функцию синхронизации:

Float32 animationCurve(Float32 x)
{
  return sin(x*0.5*M_PI);
}

Но теперь, поскольку зависимость между прогрессом анимации и временем не является линейной, безопаснее использовать время в качестве индикатора для остановки анимации. (Возможно, вы захотите, например, чтобы ваша стрелка совершила 1,5 полных оборота, а затем повернула назад к начальному углу, это означает, что ваш animProgress изменится с 0 до 1,5 и затем вернется к 1, а timeProgress изменится с 0 до 1.) Поэтому, чтобы быть в безопасности, мы теперь разделяем ход времени и анимацию:

Float32 timeProgress = nFrames/(animationDuration*animationFrameRate);
Float32 animProgress = animationCurve(timeProgress);

и затем проверьте ход времени, чтобы решить, должна ли анимация остановиться:

if(timeProgress>=1.f)
{
   // Stop the animation
}

Кстати, если кто-то знает некоторые источники со списком полезных функций синхронизации для анимации, я был бы признателен, если бы вы поделились ими.

Встроенная в MacOS X утилита Grapher очень помогает в визуализации функций, поэтому вы можете видеть, как прогресс вашей анимации будет зависеть от времени.

Надеюсь, это кому-нибудь поможет ...

 13 апр. 2012 г., 23:32
Спасибо за показ этого решения
 14 апр. 2012 г., 17:10
NSTimers не подходят для анимации пользовательского интерфейса. Вот для чего нужны CADisplayLinks.
 BartoNaz14 апр. 2012 г., 17:26
А что не так с NSTimer, который делает его непригодным для анимации пользовательского интерфейса? По крайней мере, в моем случае это работает очень хорошо. Это не эффективно или в чем проблема? Не могли бы вы объяснить?
 BartoNaz14 апр. 2012 г., 18:47
ХОРОШО. Я нашел довольно полезный учебник на -zearfoss.wordpress.com/2011/09/02/more-cadisplaylink , Похоже, CADisplayLink действительно очень похож на NSTimer. Единственное, что должно быть изменено в моем подходе, - это изменить способ, рассчитывается timeProgress. Я постараюсь реализовать это и выложу обновленную версию реализации. Но мой вопрос о том, почему CADisplayLink лучше, остается открытым.
Решение Вопроса

БлагодаряJenox Я обновил код анимации, используя CADisplayLink, который кажется более правильным решением, чем NSTimer. Поэтому я покажу правильную реализацию с CADisplayLink сейчас. Это очень близко к предыдущему, но даже немного проще.

Мы добавляем фреймворк QuartzCore в наш проект. Затем мы помещаем следующие строки в заголовочный файл нашего класса:

CADisplayLink* timer;
Float32 animationDuration;  // Duration of one animation loop
Float32 initAngle;  // Represents the initial angle 
Float32 angle;  // Angle used for drawing
CFTimeInterval startFrame;  // Timestamp of the animation start

-(void)startAnimation;  // Method to start the animation
-(void)animate;  // Method for updating the property or stopping the animation

Теперь в файле реализации мы устанавливаем значения продолжительности анимации и другие начальные значения:

initAngle=0.75*M_PI;
angle=initAngle;
animationDuration=1.5f;  // Arrow makes full 360° in 1.5 seconds
startFrame=0;  // The animation hasn't been played yet

Для запуска анимации нам нужно создать экземпляр CADisplayLink, который будет вызывать методanimate и добавьте его в основной RunLoop нашего приложения:

-(void)startAnimation
{
   timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(animate)];
   [timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}

Этот таймер позвонитanimate метод каждого runLoop приложения.

Итак, теперь пришла реализация нашего метода для обновления свойства после каждого цикла:

-(void)animate
{
   if(startFrame==0) {
      startFrame=timer.timestamp;  // Setting timestamp of start of animation to current moment
      return;  // Exiting till the next run loop
   } 
   CFTimeInterval elapsedTime = timer.timestamp-startFrame;  // Time that has elapsed from start of animation
   Float32 timeProgress = elapsedTime/animationDuration;  // Determine the fraction of full animation which should be shown
   Float32 animProgress = timingFunction(timeProgress); // The current progress of animation
   angle=initAngle+animProgress*2.f*M_PI;  // Setting angle to new value with added rotation corresponding to current animation progress
   if (timeProgress>=1.f) 
   {  // Stopping animation
      angle=initAngle; // Setting angle to initial value to exclude rounding effects
      [timer invalidate];  // Stopping the timer
      startFrame=0;  // Resetting time of start of animation
   }
   [self setNeedsDisplay];  // Redrawing with updated angle value
}

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

И я должен признать, что анимация работает немного более плавно, чем в случае с NSTimer. По умолчанию CADisplayLink вызываетanimate метод каждого цикла выполнения. Когда я рассчитывал частоту кадров, она составляла 120 кадров в секунду. Я думаю, что это не очень эффективно, поэтому я уменьшил частоту кадров до 22 кадров в секунду, изменив свойство frameInterval в CADisplayLink перед добавлением его в mainRunLoop:

timer.frameInterval=3;

Это означает, что он будет называтьanimate метод сначала запускает цикл, затем ничего не делает следующие 3 цикла, вызывает 4-й и так далее. Вот почему frameInterval может быть только целочисленным.

Ваш ответ на вопрос