由于最近公司需使用到跑马灯实现文字的循环播放,所以使用到了MarqueeLabel框架进行实现功能,传送门:https://github.com/cbpowell/MarqueeLabel。
MarqueeLabel框架涉及到了图层方面的知识,图层就是将视图显示出来,UIView的显示也是基于图层CALayer才能完美地呈现给用户。而且不能响应一切的事件,事件的处理交由UIView进行。
MarqueeLabel就是集成子UILabel进行控件的封装,我们通过代码进行分析:
1.获取文本内容的实际宽高:
- (CGSize)subLabelSize { //获取当前文本的宽高
// Calculate expected size 进行一些初始化设置
CGSize expectedLabelSize = CGSizeZero;
CGSize maximumLabelSize = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX);//设置最大显示长度
// Get size of subLabel 根据self.subLabel的文本获取当前最合适的尺寸
expectedLabelSize = [self.subLabel sizeThatFits:maximumLabelSize];
#ifdef TARGET_OS_IOS
// Sanitize width to 5461.0f (largest width a UILabel will draw on an iPhone 6S Plus)
expectedLabelSize.width = MIN(expectedLabelSize.width, 5461.0f); //比较最大显示长度
#elif TARGET_OS_TV
// Sanitize width to 16384.0 (largest width a UILabel will draw on tvOS) //tvOS下的设置
expectedLabelSize.width = MIN(expectedLabelSize.width, 16384.0f);
#endif
// Adjust to own height (make text baseline match normal label)
expectedLabelSize.height = self.bounds.size.height; //当前显示的高度
return expectedLabelSize;
}
2.设置偏移遮罩并设置启动滚动
- (void)scrollAwayWithInterval:(NSTimeInterval)interval delayAmount:(NSTimeInterval)delayAmount shouldReturn:(BOOL)shouldReturn {
// Check for conditions which would prevent scrolling
if (![self labelReadyForScroll]) {
return;
}
// Return labels to home (cancel any animations)
[self returnLabelToOriginImmediately];
// Call pre-animation method
[self labelWillBeginScroll];
// Animate 开启动画
[CATransaction begin];
// Set Duration
[CATransaction setAnimationDuration:(!shouldReturn ? CGFLOAT_MAX : 2.0 * (delayAmount + interval))];
// Create animation for gradient, if needed
if (self.fadeLength != 0.0f) {
CAKeyframeAnimation *gradAnim = [self keyFrameAnimationForGradientFadeLength:self.fadeLength
interval:interval
delay:delayAmount];
[self.layer.mask addAnimation:gradAnim forKey:@"gradient"]; //更加self.fadeLength遮罩添加动画
}
__weak __typeof__(self) weakSelf = self;
self.scrollCompletionBlock = ^(BOOL finished) {
if (!finished || !weakSelf) {
// Do not continue into the next loop
return;
}
// Call returned home method
[weakSelf labelReturnedToHome:YES];
// Check to ensure that:
// 1) We don't double fire if an animation already exists
// 2) The instance is still attached to a window - this completion block is called for
// many reasons, including if the animation is removed due to the view being removed
// from the UIWindow (typically when the view controller is no longer the "top" view)
if (self.window && ![weakSelf.subLabel.layer animationForKey:@"position"]) {
// Begin again, if conditions met
if (weakSelf.labelShouldScroll && !weakSelf.tapToScroll && !weakSelf.holdScrolling) {
[weakSelf scrollAwayWithInterval:interval delayAmount:delayAmount shouldReturn:shouldReturn];
}
}
};
// Create animation for position
CGPoint homeOrigin = self.homeLabelFrame.origin;
CGPoint awayOrigin = MLOffsetCGPoint(self.homeLabelFrame.origin, self.awayOffset);
NSArray *values = nil;
switch (self.marqueeType) {
case MLLeft:
case MLRight:
values = @[[NSValue valueWithCGPoint:homeOrigin], // Initial location, home
[NSValue valueWithCGPoint:homeOrigin], // Initial delay, at home
[NSValue valueWithCGPoint:awayOrigin], // Animation to away
[NSValue valueWithCGPoint:awayOrigin]]; // Delay at away
break;
default:
values = @[[NSValue valueWithCGPoint:homeOrigin], // Initial location, home
[NSValue valueWithCGPoint:homeOrigin], // Initial delay, at home
[NSValue valueWithCGPoint:awayOrigin], // Animation to away
[NSValue valueWithCGPoint:awayOrigin], // Delay at away
[NSValue valueWithCGPoint:homeOrigin]]; // Animation to home
break;
}
CAKeyframeAnimation *awayAnim = [self keyFrameAnimationForProperty:@"position"
values:values
interval:interval
delay:delayAmount];
// Add completion block
[awayAnim setValue:@(YES) forKey:kMarqueeLabelAnimationCompletionBlock];
// Add animation
[self.subLabel.layer addAnimation:awayAnim forKey:@"position"]; //根据position属性滚动动画
[CATransaction commit];
}
为了实现MLContinuous类型下的无限循环动画滚动,代码使用到了CAReplicatorLayer复制图层类:
// Configure replication //复制一份相同的图层,设置偏移值,每当前一个repliLayer滚出边界,则会重新生成一个layer,实现无限循环效果。
self.repliLayer.instanceCount = 2;
self.repliLayer.instanceTransform = CATransform3DMakeTranslation(-self.awayOffset, 0.0, 0.0);
扩展:
图层遮罩mask则是所谓的图层与遮罩的交集,只有当遮罩图层设置了不透明的背景颜色或者设置图片时图片透明通道为不完全透明时,图层才能显示出来,否则当前图片用户将不可看见。
1.当设置背景颜色时,整个图片将显示出来
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 300, 70, 70)];
imageView.image = [UIImage imageNamed:@"bg"];
imageView.layer.backgroundColor = [UIColor blackColor].CGColor;
imageView.backgroundColor = [UIColor blueColor];
[self.view addSubview:imageView];
CALayer* shapeLayer = [CALayer layer];
shapeLayer.frame = imageView.bounds;
shapeLayer.backgroundColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0].CGColor;//当设置alpha为0时,则图片出不来
imageView.layer.mask = shapeLayer;
2.当设置图片时,则将重叠部分显示出来
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 300, 70, 70)];
imageView.image = [UIImage imageNamed:@"bg"];
imageView.layer.backgroundColor = [UIColor blackColor].CGColor;
imageView.backgroundColor = [UIColor blueColor];
[self.view addSubview:imageView];
CALayer* shapeLayer = [CALayer layer];
shapeLayer.frame = imageView.bounds;
shapeLayer.contents = (id)[UIImage imageNamed:@"bgTransparent"].CGImage; //提示:注意图片的透明通道问题
imageView.layer.mask = shapeLayer;
以上则是简单的MLContinuous这种类型的原理分析,具体框架还使用了其它动画和图层进行操作,读者可自行研究,如有错误地地方,敬请谅解提出,谢谢!
具体提供两个简单例子:
https://github.com/JeffreyPiano/MarqueeLabelTestForPerson.git
https://github.com/JeffreyPiano/AnimationCollection.git