iOS 跑马灯效果

介绍

因业务需求,需要做一个跑马灯效果的公告栏,查了网上大部分制作原理,都不能满足也无需求,决定自己造轮子

效果

MarqueeGif.gif

需求

  • 如果文字太多,跑马灯效果;否则,无动画中间位置显示
  • 默认跑马灯10秒结束,后自动隐藏;短文一样10秒显隐
  • 随时更换内容,更换内容是时间重新计算
  • 点击关闭按钮,消失

原理

CoreAnimation动画实现,通过检测动画的开始与结束,进行业务处理

代码部分

.h文件

@class MarqueeView;
typedef NS_ENUM(NSInteger, MarqueeViewDirection) {
    MarqueeDirectionLeft,// 从右向左
    MarqueeDirectionRight // 从左向右
};

@protocol MarqueeViewDelegate <NSObject>
@optional
//  动画结束回调
- (void)marqueeView:(MarqueeView *)marqueeView animationDidStopFinished:(BOOL)fnished;
// 关闭
- (void)closeMarqueeView:(MarqueeView *)marqueeView;
@end

@interface MarqueeView : UIView

/// 代理
@property (nonatomic, weak) id <MarqueeViewDelegate> delegate;
/// 速度 default 1.0
@property (nonatomic, assign) CGFloat speed;
/// 动画时间     设置完后speed失去作用
@property (nonatomic, assign) CGFloat duration;
/// 方向  default MarqueeDirectionLeft
@property (nonatomic, assign) MarqueeViewDirection marqueeDirection;
/// 是否需要标题, 需要在添加视图之前设置  default NO
@property (nonatomic, assign) BOOL isNeedTitle;

- (instancetype)init NS_UNAVAILABLE;
/**
 添加内容视图

 @param view 内容视图
 */
- (void)addContentView:(UIView *)view;

/**
 开始动画
 */
- (void)startAnimation;

/**
 结束动画
 */
- (void)stopAnimation;

/**
 暂停动画
 */
- (void)pauseAnimation;

/**
 重新开始动画
 */
- (void)resumeAnimation;

.m文件

static CGFloat const kTitleWidth = 80;
static CGFloat const kCancleWidth = 30;

static CGFloat const kDefaultDuration = 10;

@interface MarqueeView ()<CAAnimationDelegate> {
    /// 整体控件宽度
    CGFloat _width;
    /// 整体控件高度
    CGFloat _height;
    
    /// 动画视图的宽度
    CGFloat _animationViewWidth;
    /// 动画视图的高度
    CGFloat _animationViewHeight;
    
    /// 内容视图
    UIView *_contentView;
    UIView *_bgView;
}
/// 背景视图
@property (nonatomic, strong) UIView *bgView;
/// 动画视图
@property (nonatomic, strong) UIView *animationView;
/// title视图
@property (nonatomic, strong) UIButton *titleBtn;
/// 关闭按钮
@property (nonatomic, strong) UIButton *closeBtn;
/// 是否重新赋值开始动画
@property (nonatomic, assign) BOOL isRestart;
@end

@implementation MarqueeView
#pragma mark ==================  init method  ==================
- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        _width = frame.size.width;
        _height = frame.size.height;
        
        self.layer.cornerRadius = _height / 2;
        self.layer.masksToBounds = YES;
        
        _isRestart = YES;
        _speed = 1.f;
        _marqueeDirection = MarqueeDirectionLeft;
        
        [self addSubview:self.bgView];
        [_bgView addSubview:self.animationView];
        [self addSubview:self.titleBtn];
        [self addSubview:self.closeBtn];
        
    }
    return self;
}
#pragma mark ==================  public method  ==================
- (void)addContentView:(UIView *)view {
    [_contentView removeFromSuperview];
    view.frame = view.bounds;
    _contentView = view;
    
    if (!_isNeedTitle) {
        self.animationView.frame = view.bounds;
        self.layer.cornerRadius = 0;
    } else {
        _titleBtn.frame = CGRectMake(0, 0, kTitleWidth, _height);
        _bgView.frame = CGRectMake(kTitleWidth + 10, 0, _width - kTitleWidth - kCancleWidth - 20, _height);
        _closeBtn.frame = CGRectMake(_width - kCancleWidth, 0, kCancleWidth, _height);
        self.animationView.frame = CGRectMake(0, 0, view.bounds.size.width, view.bounds.size.height);
    }
    if (_bgView.width >= view.width) {
        self.animationView.frame = CGRectMake((_bgView.width - view.width) / 2, 0, view.width, view.height);
    }
    [self.animationView addSubview:_contentView];
    _animationViewWidth = self.animationView.width;
    _animationViewHeight = self.animationView.height;
}
#pragma mark ==================  CAAnimationDelegate   ==================
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    if (_isRestart) {
        if (self.delegate && [self.delegate respondsToSelector:@selector(marqueeView:animationDidStopFinished:)]) {
            [self.delegate marqueeView:self animationDidStopFinished:flag];
        }
    }
    _isRestart = YES;
}
- (void)startAnimation {
    [self stopAnimation];
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(dismiss) object:nil];
    CGFloat bgWidth = CGRectGetWidth(_bgView.frame);
    if (bgWidth >= _animationViewWidth) {
        [self performSelector:@selector(dismiss) withObject:nil afterDelay:_duration ? : kDefaultDuration];
        return;
    }
    
    dispatch_block_t block = ^ {
        CGPoint pointRightCenter = CGPointMake(_animationViewWidth / 2, _animationViewHeight / 2);
        CGPoint pointLeftCenter = CGPointMake(- _animationViewWidth / 2, _animationViewHeight / 2);
        CGPoint fromPoint = self.marqueeDirection == MarqueeDirectionLeft ? pointRightCenter : pointLeftCenter;
        CGPoint toPoint = self.marqueeDirection == MarqueeDirectionLeft ? pointLeftCenter : pointRightCenter;
        
        self.animationView.center = fromPoint;
        UIBezierPath *movePath    = [UIBezierPath bezierPath];
        [movePath moveToPoint:fromPoint];
        [movePath addLineToPoint:toPoint];
        
        CAKeyframeAnimation *moveAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
        moveAnimation.path                 = movePath.CGPath;
        moveAnimation.removedOnCompletion  = YES;
        moveAnimation.duration             = _duration ? : _animationViewWidth / 30.f * (1 / self.speed);
        moveAnimation.delegate             = self;
        [self.animationView.layer addAnimation:moveAnimation forKey:@"animationViewPosition"];
    };
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), block);
}
- (void)stopAnimation {
    if (self.animationView.layer.animationKeys.count) {
        [self.animationView.layer removeAllAnimations];
        _isRestart = NO;
    }
}
- (void)pauseAnimation {
    [self pauseLayer:self.animationView.layer];
}
- (void)resumeAnimation {
    [self resumeLayer:self.animationView.layer];
}

- (void)pauseLayer:(CALayer*)layer {
    CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
    layer.speed               = 0.0;
    layer.timeOffset          = pausedTime;
}
- (void)resumeLayer:(CALayer*)layer {
    CFTimeInterval pausedTime     = layer.timeOffset;
    layer.speed                   = 1.0;
    layer.timeOffset              = 0.0;
    layer.beginTime               = 0.0;
    CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
    layer.beginTime               = timeSincePause;
}
- (void)dismiss {
    [self.animationView.layer removeAllAnimations];
    _isRestart = NO;
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(dismiss) object:nil];
    if (self.delegate && [self.delegate respondsToSelector:@selector(closeMarqueeView:)]) {
        [self.delegate closeMarqueeView:self];
    }
}
#pragma mark ==================  setter and getter method ==================
- (UIView *)bgView {
    if (!_bgView) {
        _bgView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, _width, _height)];
        _bgView.layer.masksToBounds = YES;
    }
    return _bgView;
}
- (UIView *)animationView {
    if (!_animationView) {
        _animationView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, _width, _height)];
    }
    return _animationView;
}
- (UIButton *)titleBtn {
    if (!_titleBtn) {
        UIButton *titleBtn = [[UIButton alloc] initWithFrame:CGRectMake(- kTitleWidth, 0, kTitleWidth, _height)];
        titleBtn.layer.cornerRadius = _height / 2;
        titleBtn.layer.masksToBounds = YES;
        [titleBtn setBackgroundColor:kDefTintColor];
        [titleBtn setTitle:@"公告:" forState:UIControlStateNormal];
        [titleBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        titleBtn.titleLabel.font = Font(14);
        [titleBtn setImage:Image(@"announcementSound") forState:UIControlStateNormal];
        _titleBtn = titleBtn;
    }
    return _titleBtn;
}
- (UIButton *)closeBtn {
    if (!_closeBtn) {
        UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(_width, 0, kCancleWidth, _height)];
        [btn setImage:Image(@"announcementClose") forState:UIControlStateNormal];
        [btn addTarget:self action:@selector(dismiss) forControlEvents:UIControlEventTouchUpInside];
        _closeBtn = btn;
    }
    return _closeBtn;
}
@end

使用

self.marqueeView = [[MarqueeView alloc] initWithFrame:CGRectMake(0, 0, 300, 30)];
        _marqueeView.backgroundColor = RGBACOLOR(0, 0, 0, 0.7);
        _marqueeView.duration = 10;
        _marqueeView.delegate = self;
        _marqueeView.center = self.view.center;
        [self.view addSubview:_marqueeView];
        _marqueeView.isNeedTitle = YES;
        [_marqueeView addContentView:[self createLabelWithText:@"XXXXXXXXXXXXXXXXXXXXX"]];
        [_marqueeView startAnimation];

/// 更换
- (void)clickAction:(UIButton *)btn {
    [_marqueeView stopAnimation];
    [_marqueeView addContentView:[self createLabelWithText:@"haha"]];
    [_marqueeView startAnimation];
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容