检测 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 笔画中的触摸的主要内容,如果未能解决你的问题,请参考以下文章

检测对子层元素的触摸

检测在 CAShapeLayer 中点击的位置

检测Android中的触摸位置[重复]

在带有触摸屏的 Windows 8 上检测 Chrome 中的触摸事件

是否在所有设备中都通过点击事件检测到触摸设备中的触摸?

子视图中的触摸检测