我正在开发一个使用 UIScrollView 在屏幕上滚动乐谱的应用程序。由于我需要以固定的时间间隔发生这种情况,并且还要以均匀的间隔播放短声音,因此我的代码基于 Apple 提供的现已弃用的“节拍器”示例。
问题是滚动并不顺利 - 它非常生涩。我的运行日志表明我正在使用的 NSTimer 并没有真正以准确的间隔触发(或者可能部分代码执行时间过长)。
录取:我是音乐家,不是专业程序员。我阅读了有关 GCD 的 Apple 文档(这似乎是比节拍器示例中的线程执行同时事件更好的方法),但我真的不知道如何将它应用到我的项目中。
我没有使用分页。内容大小比屏幕大小大得多:ScrollView 滚动,只是锯齿状。
我的代码执行良好,但滚动非常生涩。任何帮助将不胜感激,特别是如果它符合 KSS 原则!
// PlayView.m
#import "PlayView.h"
#include <stdlib.h>
NSInteger xWidth;
int xChange = 0;
float timeInterval;
AVAudioPlayer *audioPlayer1;
AVAudioPlayer *audioPlayer2;
float tempo;
int subdivisions;
int timesPlayed = 1;
int actualTimesPlayed = 0;
// ...
//RUN WHEN THE USER PRESSES THE PLAY BUTTON
-(void)start {
// Used in calculating the speed of timer firing.
// xWidth is the spacing between images (pixels)
subdivisions = (int)(xWidth);
// Keeps track of where we are in the measure
beatNumber = 0;
// Keeps track of how many measures we already played
timesPlayed = 1;
actualTimesPlayed = 0;
// Let the device idle without dimming the screen
UIApplication *myApp=[UIApplication sharedApplication];
myApp.idleTimerDisabled=YES;
[self startDriverThread];
}
// Taken straight from 'Metronome'
- (void)startDriverThread {
if (soundPlayerThread != nil) {
[soundPlayerThread cancel];
[self waitForSoundDriverThreadToFinish];
}
NSThread *driverThread = [[NSThread alloc] initWithTarget:self selector:@selector(startDriverTimer:) object:nil];
self.soundPlayerThread = driverThread;
[driverThread release];
[self.soundPlayerThread start];
}
// Taken straight from 'Metronome'
- (void)waitForSoundDriverThreadToFinish {
while (soundPlayerThread && ![soundPlayerThread isFinished]) {
// Wait for the thread to finish. I've experimented with different values
[NSThread sleepForTimeInterval:timeInterval];
}
}
// Taken straight from 'Metronome'
- (void)stopDriverThread {
[self.soundPlayerThread cancel];
[self waitForSoundDriverThreadToFinish];
self.soundPlayerThread = nil;
}
// Modification of 'Metronome'
- (void)startDriverTimer:(id)info {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Give the sound thread high priority to keep the timing steady.
[NSThread setThreadPriority:1.0];
BOOL continuePlaying = YES;
while (continuePlaying) { // Loop until cancelled.
// An autorelease pool to prevent the build-up of temporary objects.
NSAutoreleasePool *loopPool = [[NSAutoreleasePool alloc] init];
// Reset the beat number to 0 at the end of each musical measure
if (beatNumber == (subdivisions*4)) {
beatNumber = 0; }
// Incrementation of where we are in the bar on each firing of the timer
beatNumber++;
// On each beat, play a sound
if(beatNumber % subdivisions == 0) {
[self playSound]; }
// On each firing of the timer, run the 'animateScreen' function, which scrolls the UIScrollView and performs some other simple tasks
[self performSelectorOnMainThread:@selector(animateScreen) withObject:nil waitUntilDone:NO];
// xChange is the number of pixels scrolled, but only start scrolling two and a half beats into the first bar (in order to keep the main image event at the center of the screen in each measure
if ((actualTimesPlayed == 0 && beatNumber >= 2.5*subdivisions) || xChange > 0) {
xChange += 1; }
// The time interval at which the timer fires is calculated by dividing the tempo (beats per minute, entered by the user; between 60-94) and 60 (seconds) This alone would result in one firing of the timer per beat, but we need at double this speed for some of the calculations 'animateScreen' does in between the beats, and really many more so the scrolling is smooth.
//
// EXAMPLE: (60s/92bpm)/17% (image spacing) of 320pixels (screen width) = 0.012077
timeInterval = (60/tempo)/subdivisions;
NSDate *curtainTime = [[NSDate alloc] initWithTimeIntervalSinceNow:(timeInterval)];
NSDate *currentTime = [[NSDate alloc] init];
// Wake up periodically to see if we've been cancelled.
while (continuePlaying && ([currentTime compare:curtainTime] != NSOrderedDescending)) {
if ([soundPlayerThread isCancelled] == YES) {
continuePlaying = NO;
}
// Don't fully understand this; I've tried changing it to various values with no luck
[NSThread sleepForTimeInterval:timeInterval];
[currentTime release];
currentTime = [[NSDate alloc] init];
}
[curtainTime release];
[currentTime release];
[loopPool drain];
}
[pool drain];
}
- (void)playSound {
if(beatNumber % subdivisions == 0){
if (beatNumber == subdivisions) {
[audioPlayer1 play];
}
else {
[audioPlayer2 play];
}
}
}
- (void)animateScreen {
// BEAT 1
if (beatNumber == (subdivisions)) {
// do some stuff
// ...
}
// THE SECOND EIGTH OF 1
if (beatNumber == (int)(subdivisions*1.25) && actualTimesPlayed >0) {
// do some more stuff
// ..
}
// BEAT 2
if (beatNumber == (2*subdivisions)) {
// even more stuff
}
// BEAT 3
if (beatNumber == (3*subdivisions)) {
// ... more
}
// BEAT 4
if (beatNumber == (4*subdivisions)) {
// yet more stuff
// ...
actualTimesPlayed++;
timesPlayed++;
if (timesPlayed == 3) {
timesPlayed = 1; }
}
// On the "And of 4"
if (beatNumber == subdivisions/2 && actualTimesPlayed > 0) {
// STUFF
}
//Scroll over
[theScroller setContentOffset:CGPointMake(xChange, 0) animated:NO];
}
// ...
也许这不是最优雅的代码,但除了滚动之外,一切都正常。在我省略代码的地方发生了很多事情,但我知道事情并没有妨碍(当我将其注释掉并让程序完全裸露时 - 只是计时器和滚动,没有声音 - 它仍然是不顺利。)我相信计时器是问题所在。
非常感谢任何帮助/指导。