Building the Sectors(构建扇区)

 

要让轮子停在当前扇区的中间点,你首先需要把轮子分为几个扇区。当用户的手指离开屏幕时我们要做一下内容:

1.         计算弧度值

2.         基于上一步的弧度值找出扇区

3.         旋转一个弧度到扇区的中间点

举个例子,入股被选中的扇区是zero并且用户只是轻微往上或往下拖拽了轮子,你想让轮子转会到zero扇区的中间点。尽管这有一点棘手,让我们一步一步的来做。

首先,让我们对容器container的世界有一个更好的理解。

SMRotaryWheel.mcontinueTrackingWithTouch方法的顶部下列代码:

CGFloat radians = atan2f(container.transform.b, container.transform.a);
NSLog(@"rad is %f", radians);

这记录了用户的手指拖拽的每个时刻容器container的旋转弧度。你会注意掉如果轮子被顺时针拖拽,弧度会是正值直到弧度值大于PI弧度(180度),或者如果你愿意,当标号为“0”的扇区处在圆中心点水平线以下的象限时,当你超过180度,你会看到负值,就像下面屏幕输出的。

这是你在计算扇区边界时必须考虑的:它们的最大值、中间值和最小值。被选中的扇区必会在最左边的位置,0扇区被初始化的位置。你要找到这个问题的答案:当弧度值是x的时候,哪个扇区是被定位器标识的?

要回答这个问题,你需要逆向思考。下面的图片显示了一个有八个扇区的轮子。

圆圈周围的数值表示每隔扇区的最大和最小弧度值。例如,不论何时只要容器container的弧度值在-0.390.39之间,轮子就应停在扇区0的中间点位置。

再者,你必须考虑象限(正或负)来正确的加减角度差。有一个特殊,你必须处理跨两个象限的扇区0和扇区4。对于扇区0,中间点是0弧度,它还算较为简单。然而对于扇区4,中间点是PI-PI,因为中间点跨越正负象限的分界线。所以,事情变得有点复杂。

你可以从下面这张图片看到,如果有奇数个扇区,那么扇区中间点的弧度值会稍简单点。

为了保证灵活和全面,这篇教程会考虑偶数和奇数个扇区的情况,并提供各自的程序代码。但是首先,我们要定义一个新类来表示扇区,并存储每个扇区弧度的最大值、中间值和最小值。

IOS\Cocoa Touch\Objective-C class模版创建一个新文件。起名类SMSector,并且继承自NSObject。现在来到SMSector.h文件并用下面的代码替换其中的内容:

@interface SMSector : NSObject
 
@property float minValue;
@property float maxValue;
@property float midValue;
@property int sector;
 
@end

转移到SMSector.m文件,并用下面的实现代码替换其中的内容:

#import "SMSector.h"
 
@implementation SMSector
 
@synthesize minValue, maxValue, midValue, sector;
 
- (NSString *) description {
    return [NSString stringWithFormat:@"%i | %f, %f, %f", self.sector, self.minValue, self.midValue, self.maxValue];
}
 
@end

SMRotaryWheel.h导入SMSector类:

#import "SMSector.h"

然后增加一个新属性property,名叫sectors

@property (nonatomic, strong) NSMutableArray *sectors;

来到SMRotaryWheel.m并添加两个新的帮助方法定义来创建扇区(在已经有的caculateDistanceFromCenter的下边):

@interface SMRotaryWheel()
    ...
    - (void) buildSectorsEven;
    - (void) buildSectorsOdd;
@end

然后,synthesize这个新的属性property

@synthesize sectors;

下一步,在drawWheel方法的最后,添加下面的代码这样当你创建转轮的时候扇区就会被初始化。

// 8 - Initialize sectors
    sectors = [NSMutableArray arrayWithCapacity:numberOfSections];
    if (numberOfSections % 2 == 0) {
        [self buildSectorsEven];
    } else {
        [self buildSectorsOdd];
    }

让我们开始一个比较简单的情况,当有奇数个扇区时。在SMRotaryWheel.m的底部(@end的上面)添加下面的方法实现代码:

- (void) buildSectorsOdd {
               // 1 - Define sector length
    CGFloat fanWidth = M_PI*2/numberOfSections;
               // 2 - Set initial midpoint
    CGFloat mid = 0;
               // 3 - Iterate through all sectors
    for (int i = 0; i < numberOfSections; i++) {
        SMSector *sector = [[SMSector alloc] init];
                               // 4 - Set sector values
        sector.midValue = mid;
        sector.minValue = mid - (fanWidth/2);
        sector.maxValue = mid + (fanWidth/2);
        sector.sector = i;
        mid -= fanWidth;
        if (sector.minValue < - M_PI) {
            mid = -mid;
            mid -= fanWidth; 
        }
                               // 5 - Add sector to array
        [sectors addObject:sector];
                               NSLog(@"cl is %@", sector);
    }
}

让我们一步步的分析上面的代码:

首先,我们定义了每隔扇区弧度值的长度(或者叫宽度如果你愿意)。

然后,我们用初始中间点声明了一个变量。既然我们的起始点是0弧度,那它就是我们第一个中间点。

然后我们重复设置每个扇区的最大、中间和最小弧度值。

当计算最小和最大弧度值时,你要加上或减去扇区宽度的一半来得到正确的结果。记得角度变化范围是从-PIPI,这样才正常,如果一个值超出了PI-PI,那意味着你改变了象限。你既然是顺时针定位轮子,你就得考虑弧度最小值小于PI的情况,并且改变中间点的标记。

最后,一旦创建一个扇区,我们把这个扇区添加到预先定义的扇区数组中。

现在在SMViewController.m中修改viewDidLoad方法的section#2处设置sections值为3,正如下面代码:

SMRotaryWheel *wheel = [[SMRotaryWheel alloc] initWithFrame:CGRectMake(0, 0, 200, 200)                                                      andDelegate:self 
withSections:3];

如果你现在编译并运行,控制台应该显示以下的结果:

这些数值跟上面被分为三部分的轮子的数值是相同的。所以你的计算工作非常精确!

 

Animating the Selection Centering(旋转到扇区中心)

 

最后一步是实现校准当前扇区的中心点,让我们温习下这是什么意思。

当用户的手指离开屏幕你必须计算x值,当前的弧度值,并且根据这个值确定选中的扇区。然后你得计算x和扇区中心点的差值,并用它来构建一个仿射变换。

首先在SMRotaryWheel.h中添加一个新属性property来记录当天扇区:

@property int currentSector;

然后,在SMRotaryWheel.msynthesize这个新属性property

@synthesize currentSector;

要处理手指离开屏幕的事件,你要重写endTrackingWithTouch:withEvent:方法(重写touchedEnded:withEvent:方法如果你扩展UIView)。

SMRotaryWheel.m,在continueTrackingWithTouch:withEvent:方法的下面添加下列代码来处理奇数个扇区:

- (void)endTrackingWithTouch:(UITouch*)touch withEvent:(UIEvent*)event
{
    // 1 - Get current container rotation in radians
    CGFloat radians = atan2f(container.transform.b, container.transform.a);
    // 2 - Initialize new value
    CGFloat newVal = 0.0;
    // 3 - Iterate through all the sectors
    for (SMSector *s in sectors) {
        // 4 - See if the current sector contains the radian value
        if (radians > s.minValue && radians < s.maxValue) {
            // 5 - Set new value 
            newVal = radians - s.midValue;
            // 6 - Get sector number
            currentSector = s.sector;
                                              break;
        }
    }
    // 7 - Set up animation for final rotation
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.2];
    CGAffineTransform t = CGAffineTransformRotate(container.transform, -newVal);
    container.transform = t;
    [UIView commitAnimations];
}

这个方法相当的简单。它计算当前的弧度冰雨最小和最大弧度进行比较来确定正确的扇区。然后计算出差值并创建一个新的仿射变换,为了让效果看起来很自然,设置这个旋转动画持续0.2秒。

通过修改SMViewController.miewDidLoadsection#2出的代码重新创建了三个扇区,编译、运行……哈哈!它工作了!抓住轮子并按你的意愿拖拽,你会看到当你停止拖拽并把手指抬起时,它选中了右边的扇区。

现在这些代码适合于所有带有奇数个扇区的轮子。要考虑偶数个扇区,你必须重做section#3出的循环类检测异常的情况,在这种情况下,弧度最小值是正的,最大值是负的。用下面的代码替换掉endTrackingWithTouch:withEvent:中的sections#4,#5#6部分:

// 4 - Check for anomaly (occurs with even number of sectors)
        if (s.minValue > 0 && s.maxValue < 0) {
            if (s.maxValue > radians || s.minValue < radians) {
                // 5 - Find the quadrant (positive or negative)
                if (radians > 0) {
                    newVal = radians - M_PI;
                } else {
                    newVal = M_PI + radians;                    
                }
                currentSector = s.sector;
            }
        }
        // 6 - All non-anomalous cases
        else if (radians > s.minValue && radians < s.maxValue) {
            newVal = radians - s.midValue;
            currentSector = s.sector;
        }

编译、运行,并体验通过改变扇区数来免费进行的实验吧!