Building the Sectors(构建扇区)
要让轮子停在当前扇区的中间点,你首先需要把轮子分为几个扇区。当用户的手指离开屏幕时我们要做一下内容:
1. 计算弧度值
2. 基于上一步的弧度值找出扇区
3. 旋转一个弧度到扇区的中间点
举个例子,入股被选中的扇区是zero并且用户只是轻微往上或往下拖拽了轮子,你想让轮子转会到zero扇区的中间点。尽管这有一点棘手,让我们一步一步的来做。
首先,让我们对容器container的世界有一个更好的理解。
在SMRotaryWheel.m中continueTrackingWithTouch方法的顶部下列代码:
CGFloat radians = atan2f(container.transform.b, container.transform.a); NSLog(@"rad is %f", radians); |
这记录了用户的手指拖拽的每个时刻容器container的旋转弧度。你会注意掉如果轮子被顺时针拖拽,弧度会是正值直到弧度值大于PI弧度(180度),或者如果你愿意,当标号为“0”的扇区处在圆中心点水平线以下的象限时,当你超过180度,你会看到负值,就像下面屏幕输出的。
这是你在计算扇区边界时必须考虑的:它们的最大值、中间值和最小值。被选中的扇区必会在最左边的位置,0扇区被初始化的位置。你要找到这个问题的答案:当弧度值是x的时候,哪个扇区是被定位器标识的?
要回答这个问题,你需要逆向思考。下面的图片显示了一个有八个扇区的轮子。
圆圈周围的数值表示每隔扇区的最大和最小弧度值。例如,不论何时只要容器container的弧度值在-0.39和0.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弧度,那它就是我们第一个中间点。
然后我们重复设置每个扇区的最大、中间和最小弧度值。
当计算最小和最大弧度值时,你要加上或减去扇区宽度的一半来得到正确的结果。记得角度变化范围是从-PI到PI,这样才正常,如果一个值超出了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.m中synthesize这个新属性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.m中iewDidLoad的section#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; } |
编译、运行,并体验通过改变扇区数来免费进行的实验吧!
转载于:https://blog.51cto.com/wuhao999/799356