需求:實現(xiàn)一個彈幕容器胳赌,里面同時會有多行互不重疊的、運動中的彈幕 蜈首。每一條彈幕均需要支持點擊事件岗憋。
用腳底板想的方法:在彈幕容器里面創(chuàng)建幾個 UIButton肃晚,并且 addTarget,增加點擊事件仔戈。最后利用 UIView 的 block API 實現(xiàn)動畫关串。
結(jié)果:嗯...可惜的是,代碼運行起來杂穷,你會發(fā)現(xiàn)在 UIButton 運動過程悍缠,點擊事件并沒有響應(yīng),而且非常奇怪的是:為什么在 UIButton 動畫過程耐量,去點擊 UIButton 動畫的終點,點擊事件竟然響應(yīng)了滤港?廊蜒?這是為什么呢?
Core Anmation 動畫過程原理的引用:
在iOS中溅漾,屏幕每秒鐘重繪60次山叮。如果動畫時長比60分之一秒要長,Core Animation就需要在設(shè)置一次新值和新值生效之間添履,對屏幕上的圖層進行重新組織屁倔。這意味著CALayer除了“真實”值(就是你設(shè)置的值)之外,必須要知道當(dāng)前顯示在屏幕上的屬性值的記錄暮胧。
每個圖層屬性的顯示值都被存儲在一個叫做呈現(xiàn)圖層的獨立圖層當(dāng)中锐借,他可以通過-presentationLayer方法來訪問问麸。這個呈現(xiàn)圖層實際上是模型圖層的復(fù)制,但是它的屬性值代表了在任何指定時刻當(dāng)前外觀效果钞翔。換句話說严卖,你可以通過呈現(xiàn)圖層的值來獲取當(dāng)前屏幕上真正顯示出來的值。
補充:模型圖層在動畫開始的那一刻就已經(jīng)達到終點位置布轿,響應(yīng)點擊事件的也是它哮笆。
解決辦法:重寫彈幕容器 view 的 touchesBegan 方法。代碼如下:
@interface ZYYBarrageView ()
@property (nonatomic, strong) UIView *redView; // 將要做平移的 subview
@end
@implementation ZYYBarrageView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
}
return self;
}
- (void)commonInit {
self.redView = [[UIView alloc] initWithFrame:CGRectMake(0.f, 0.f, 30.f, 30.f)];
self.redView.backgroundColor = [UIColor redColor];
[self addSubview:self.redView];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// 重點開始Lぁ稠肘!UITouch 獲取在 barrageView 坐標(biāo)系下的坐標(biāo)
CGPoint touchPoint = [[touches anyObject] locationInView:self];
// 判斷觸摸點是否在 redView 的呈現(xiàn)樹的框框之中
if ([self.redView.layer.presentationLayer hitTest:touchPoint]) {
// 響應(yīng)紅色塊點擊
return;
} else {
}
}
進一步的需求:在 ZYYBarrageView 的同一層級,但層次偏后會有 UIButton。正常情況下萝毛,因為 ZYYBarrageView 的存在项阴,UIButton 是無法響應(yīng)點擊事件的。代碼如下:
@property (nonatomic, strong) ZYYBarrageView *barrageView; // 彈幕 view 支持多行 view 在里面進行運動
@property (nonatomic, strong) UIButton *yellowBtn; // 靠后的 UIButton
- (void)viewDidLoad {
[super viewDidLoad];
// self.yellowBtn 位于 self.barrageView 之后
[self.view addSubview:self.yellowBtn];
[self.view addSubview:self.barrageView];
}
- (ZYYBarrageView *)barrageView {
if (!_barrageView) {
_barrageView = [[ZYYBarrageView alloc] initWithFrame:CGRectMake(0.f, 30.f, SCREEN_WIDTH, 30.f)];
_barrageView.backgroundColor = [UIColor clearColor];
}
return _barrageView;
}
- (UIButton *)yellowBtn {
if (!_yellowBtn) {
_yellowBtn = [UIButton buttonWithType:UIButtonTypeCustom];
_yellowBtn.frame = CGRectMake(90.f, 30.f, 80.f, 30.f);
_yellowBtn.backgroundColor = [UIColor yellowColor];
[_yellowBtn setTitle:@"黃色按鈕" forState:UIControlStateNormal];
[_yellowBtn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[_yellowBtn addTarget:self action:@selector(onYellowBtn:) forControlEvents:UIControlEventTouchUpInside];
}
return _yellowBtn;
}
- (void)onYellowBtn:(id)sender {
// 響應(yīng)黃色按鈕
}
怎么辦珊泳?
Responder Chain 原理講解:手指點擊屏幕鲁冯,經(jīng)過系統(tǒng)響應(yīng)(之前過程省略不說,文末有參考鏈接)色查,調(diào)用 UIApplication 的 sendEvent: 方法薯演,將 UIEvent 傳給 UIWindow, 通過遞歸調(diào)用 UIView 層級的 hitTest(_:with:)秧了,結(jié)合 point(inside:with:) 找到 UIEvent 中每一個UITouch 所屬的 UIView(其實是想找到離觸摸事件點最近的那個 UIView)跨扮。這個過程是從 UIView 層級的最頂層往最底層遞歸查詢。同一層級的 UIView验毡,會優(yōu)先深度遍歷界面靠前的 UIView衡创。找到最底層 UIView 后,沿著 Responder Chain 逐步向上傳遞(UIControl 子類默認會攔截傳遞)晶通。
解決思路:重寫 ZYYBarrageView 的 hitTest(_:with:) 方法璃氢。代碼如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
BOOL isPointInsideSubview = [self.redView.layer.presentationLayer hitTest:point];
if (isPointInsideSubview == NO) {
// 如果沒有點擊在移動的 redView 上,返回 nil
// 系統(tǒng)會去遍歷位于 ZYYBarrageView 后面的 UIButton狮辽,UIButton 能得到響應(yīng)
return nil;
} else {
return [super hitTest:point withEvent:event];
}
}
如此一也,可以完美解決啦~