检测 CAShapeLayer 笔画中的触摸
Posted
技术标签:
【中文标题】检测 CAShapeLayer 笔画中的触摸【英文标题】:Detecting the touch in CAShapeLayer stroke 【发布时间】:2015-01-22 19:12:29 【问题描述】:我创建了一个播放单个音频的简单音频播放器。视图显示 CAShapeLayer 循环进度,还显示使用 CATextLayer 的当前时间。下图显示,视图:
到目前为止一切正常,我可以播放、暂停并且 CAShapeLayer 显示进度。现在,我想做到这一点,当我触摸 CAShapeLayer 路径的笔触(轨道)部分时,我想寻找那个时间的玩家。我尝试了几种方法,但我无法检测到笔画所有部分的触摸。我所做的计算似乎不太合适。如果有人能帮我解决这个问题,我会非常高兴。
这是我的完整代码,
@interface ViewController ()
@property (nonatomic, weak) CAShapeLayer *progressLayer;
@property (nonatomic, weak) CAShapeLayer *trackLayer;
@property (nonatomic, weak) CATextLayer *textLayer;
@property (nonatomic, strong) AVAudioPlayer *audioPlayer;
@property (nonatomic, weak) NSTimer *audioPlayerTimer;
@end
@implementation ViewController
- (void)viewDidLoad
[super viewDidLoad];
[self prepareLayers];
[self prepareAudioPlayer];
[self prepareGestureRecognizers];
- (void)prepareLayers
CGFloat lineWidth = 40;
CGRect shapeRect = CGRectMake(0,
0,
CGRectGetWidth(self.view.bounds),
CGRectGetWidth(self.view.bounds));
CGRect actualRect = CGRectInset(shapeRect, lineWidth / 2.0, lineWidth / 2.0);
CGPoint center = CGPointMake(CGRectGetMidX(actualRect), CGRectGetMidY(actualRect));
CGFloat radius = CGRectGetWidth(actualRect) / 2.0;
UIBezierPath *track = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:0 endAngle:2 * M_PI
clockwise:true];
UIBezierPath *progressLayerPath = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:-M_PI_2
endAngle:2 * M_PI - M_PI_2
clockwise:true];
progressLayerPath.lineWidth = lineWidth;
CAShapeLayer *trackLayer = [CAShapeLayer layer];
trackLayer.contentsScale = [UIScreen mainScreen].scale;
trackLayer.shouldRasterize = YES;
trackLayer.rasterizationScale = [UIScreen mainScreen].scale;
trackLayer.bounds = actualRect;
trackLayer.strokeColor = [UIColor lightGrayColor].CGColor;
trackLayer.fillColor = [UIColor whiteColor].CGColor;
trackLayer.position = self.view.center;
trackLayer.lineWidth = lineWidth;
trackLayer.path = track.CGPath;
self.trackLayer = trackLayer;
CAShapeLayer *progressLayer = [CAShapeLayer layer];
progressLayer.contentsScale = [UIScreen mainScreen].scale;
progressLayer.shouldRasterize = YES;
progressLayer.rasterizationScale = [UIScreen mainScreen].scale;
progressLayer.masksToBounds = NO;
progressLayer.strokeEnd = 0;
progressLayer.bounds = actualRect;
progressLayer.fillColor = nil;
progressLayer.path = progressLayerPath.CGPath;
progressLayer.position = self.view.center;
progressLayer.lineWidth = lineWidth;
progressLayer.lineJoin = kCALineCapRound;
progressLayer.lineCap = kCALineCapRound;
progressLayer.strokeColor = [UIColor redColor].CGColor;
CATextLayer *textLayer = [CATextLayer layer];
textLayer.contentsScale = [UIScreen mainScreen].scale;
textLayer.shouldRasterize = YES;
textLayer.rasterizationScale = [UIScreen mainScreen].scale;
textLayer.font = (__bridge CTFontRef)[UIFont systemFontOfSize:30.0];
textLayer.position = self.view.center;
textLayer.bounds = CGRectMake(0, 0, 300, 100);
[self.view.layer addSublayer:trackLayer];
[self.view.layer addSublayer:progressLayer];
[self.view.layer addSublayer:textLayer];
self.trackLayer = trackLayer;
self.progressLayer = progressLayer;
self.textLayer = textLayer;
[self displayText:@"Play"];
- (void)prepareAudioPlayer
NSError *error;
self.audioPlayer = [[AVAudioPlayer alloc]
initWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"Song" withExtension:@"mp3"]
error:&error];
self.audioPlayer.volume = 0.2;
if (!self.audioPlayer)
NSLog(@"Error occurred, could not create audio player");
return;
[self.audioPlayer prepareToPlay];
- (void)prepareGestureRecognizers
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(playerViewTapped:)];
[self.view addGestureRecognizer:tap];
- (void)playerViewTapped:(UITapGestureRecognizer *)tap
CGPoint tappedPoint = [tap locationInView:self.view];
if ([self.view.layer hitTest:tappedPoint] == self.progressLayer)
CGPoint locationInProgressLayer = [self.view.layer convertPoint:tappedPoint toLayer:self.progressLayer];
NSLog(@"Progress view tapped %@", NSStringFromCGPoint(locationInProgressLayer));
// this is called sometimes but not always when I tap the stroke
else if ([self.view.layer hitTest:tappedPoint] == self.textLayer)
if ([self.audioPlayer isPlaying])
[self.audioPlayerTimer invalidate];
[self displayText:@"Play"];
[self.audioPlayer pause];
else
[self.audioPlayer play];
self.audioPlayerTimer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(increaseProgress:)
userInfo:nil
repeats:YES];
- (void)increaseProgress:(NSTimer *)timer
NSTimeInterval currentTime = self.audioPlayer.currentTime;
NSTimeInterval totalDuration = self.audioPlayer.duration;
float progress = currentTime / totalDuration;
self.progressLayer.strokeEnd = progress;
int minute = ((int)currentTime) / 60;
int second = (int)currentTime % 60;
NSString *progressString = [NSString stringWithFormat:@"%02d : %02d ", minute,second];
[self displayText:progressString];
- (void)displayText:(NSString *)text
UIColor *redColor = [UIColor redColor];
UIFont *font = [UIFont fontWithName:@"HelveticaNeue" size:70];
NSDictionary *attribtues = @
NSForegroundColorAttributeName: redColor,
NSFontAttributeName: font,
;
NSAttributedString *progressAttrString = [[NSAttributedString alloc] initWithString:text
attributes:attribtues];
self.textLayer.alignmentMode = kCAAlignmentCenter;
self.textLayer.string = progressAttrString;
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
void(^animationBlock)(id<UIViewControllerTransitionCoordinatorContext>) =
^(id<UIViewControllerTransitionCoordinatorContext> context)
CGRect rect = (CGRect).origin = CGPointZero, .size = size;
CGPoint center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
self.progressLayer.position = center;
self.textLayer.position = center;
self.trackLayer.position = center;
;
[coordinator animateAlongsideTransitionInView:self.view
animation:animationBlock completion:nil];
@end
【问题讨论】:
【参考方案1】:据我所知,CALAyer hitTest 方法会进行非常原始的边界检查。如果该点在图层边界内,则返回 YES,否则返回 NO。
CAShapeLayer 不会尝试判断该点是否与形状图层的路径相交。
UIBezierPath 确实有一个 hitTest 方法,但我很确定它会检测到封闭路径内部的触摸,并且不适用于您正在使用的粗线弧线。
如果您想使用路径进行测试,我认为您将不得不推出自己的 hitTest 逻辑,该逻辑构建一个 UIBezierPath ,它是定义您正在测试的形状(您的粗弧线)的闭合路径。为正在运行的动画执行此操作可能太慢了。
您将面临的另一个问题:您正在询问具有飞行动画的图层。即使您只是为不起作用的图层的中心属性设置动画。相反,您必须询问图层的presentationLayer,这是一个近似于飞行动画设置的图层。
我认为您最好的解决方案是获取弧的开始和结束位置,将开始到结束的值转换为开始和结束角度,使用 trig 将您的触摸位置转换为角度和半径,看看是否触摸位置在开始到结束触摸的范围内,并且在指示线的内外半径内。那只需要一些基本的触发。
编辑:
提示:用于将 x 和 y 位置转换为角度的三角函数是反正切。 arc tangent 接受一个 X 和一个 Y 值并返回一个角度。但是,要正确使用反正切,您需要实现一堆逻辑来确定您的点在圆的哪个象限中。在 C 中,您想要的数学库函数是 atan2()
。 atan2()
函数会为您处理一切。您将转换您的点,使 0,0 位于中心,atan2()
将为您提供沿圆的角度。要计算距圆心的距离,请使用距离公式a² + b² = c²
。
【讨论】:
以上是关于检测 CAShapeLayer 笔画中的触摸的主要内容,如果未能解决你的问题,请参考以下文章