項目中有個需求,需要以跑馬燈的形勢滾動展示用戶的實時數據,跑馬燈需要有用戶頭像,內容的長度不固定,並且可以點擊,滾動效果還要足夠流暢,本着不重復造輪子的心理,在網上各種搜索,發現都沒法找到滿足需求的demo,沒辦法,本來(ˇˍˇ) 想~偷個懶來着,現在只有自己動手造輪子了。
整體思路大概就是在scrollView中添加多個view,讓這幾個view依次排列在在scrollView中,動態計算scrollView的frame,讓其寬度剛好是所有view的總寬度和,然后把scrollView的x初始值設置在屏幕以外,通過一個定時器,讓scrollView每隔一段時間就移動一定的距離,這個時間可以微調,確保不會出現滾動的時候抖動的問題,當scrollView滾出可見區域了,再把scrollView的frame恢復到初始值即可
先看下效果圖吧,由於博客園好像不支持gif圖片上傳,所有整了幾張靜態圖,如果想要完整效果和代碼的話,可以前往我的github https://github.com/qqcc1388/MarqueeViewDemo 查看demo源碼和動態效果
我的這個demo中分為3個部分
- HXQMarqueeView 用來顯示跑馬燈的顯示區域,接受滾動的數據源,並且手動控制動畫的開啟。
- HXQBoardView 跑馬燈中每組數據的顯示區域,這個視圖的長度是根據傳入文字的多少,動態計算的,如果文字或者頭像被點擊了,可以通過block將點擊的model傳遞到上一層
- HXQMarqueeModel 跑馬燈數據model 主要參數是文字內容和頭像參數(頭像是網絡圖片),設置完文字后,在setTitle這個方法中會動態的把文字的總寬度計算一遍,並賦值為titleWith,width的寬度為文字+頭像的總寬度
部分實現代碼
//
// HXQMarqueeView.m
// hxquan
//
// Created by Tiny on 2018/3/2.
// Copyright © 2018年 Tiny. All rights reserved.
//
#import "HXQMarqueeView.h"
#import "HXQMarqueeModel.h"
#import "HXQBoardView.h"
#import "UIView+Extionsiton.h"
@interface HXQMarqueeView ()
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) NSMutableArray *viewList;
@property (nonatomic, strong) NSArray *models;
@property (nonatomic, copy) void (^itelClick)(HXQMarqueeModel *);
@end
@implementation HXQMarqueeView
-(void)dealloc{
[self.timer invalidate];
self.timer = nil;
}
-(NSMutableArray *)viewList{
if (!_viewList) {
_viewList = [NSMutableArray array];
}
return _viewList;
}
-(NSTimer *)timer{
if (!_timer) {
_timer = [NSTimer timerWithTimeInterval:0.008f target:self selector:@selector(refreshProgress) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
}
return _timer;
}
-(instancetype)initWithFrame:(CGRect)frame{
if (self = [super initWithFrame:frame]) {
[self setupUI];
}
return self;
}
-(void)setupUI{
self.scrollView = [[UIScrollView alloc] initWithFrame:self.bounds];
self.scrollView.scrollEnabled = NO;
self.scrollView.showsVerticalScrollIndicator = NO;
self.scrollView.showsHorizontalScrollIndicator = NO;
[self addSubview:self.scrollView];
}
-(void)setItems:(NSArray *)items{
_models = items;
//移除動畫
[self.scrollView.layer removeAllAnimations];
//先移除之前的item
for (UIView *v in self.scrollView.subviews) {
if ([v isKindOfClass:[HXQBoardView class]]) {
[v removeFromSuperview];
}
}
[self.viewList removeAllObjects];
//創建新的item
HXQBoardView *last = nil;
CGFloat margin = 20;
for (int i = 0; i < items.count; i++) {
HXQMarqueeModel *model = items[i];
HXQBoardView * lb = [[HXQBoardView alloc] initWithFrame:CGRectMake(last.frame.origin.x + last.bounds.size.width + margin, 0, model.width, 44) Model:model];
__weak typeof(self) weakself = self;
lb.boardItemClick = ^(HXQMarqueeModel *xModel) {
if (weakself.itelClick) {
weakself.itelClick(xModel);
}
};
lb.tag = i;
[self.scrollView addSubview:lb];
[self.viewList addObject:lb];
last = lb;
}
//設置scrollView的contentSize
self.scrollView.contentSize = CGSizeMake(last.frame.origin.x+last.bounds.size.width, 0);
CGSize contetnsize = self.scrollView.contentSize;
self.scrollView.frame = CGRectMake(self.bounds.size.width,0,contetnsize.width+self.bounds.size.width, 44);
self.clipsToBounds = YES;
}
-(void)refreshProgress{
self.scrollView.x -=0.5 ;
if (self.scrollView.x <= -self.scrollView.contentSize.width) {
self.scrollView.x = self.bounds.size.width;
}
}
- (void)startAnimation {
if (!self.timer.isValid) {
[self.timer fire];
}
}
-(void) stopAnimation{ //結束動畫
if (self.timer.isValid) {
[self.timer invalidate];
self.timer = nil;
}
}
#pragma mark - Private
-(void)addMarueeViewItemClickBlock:(void (^)(HXQMarqueeModel *))block{
self.itelClick = block;
}
//demo使用起來也很簡單 只需要3行代碼即可(前提是數據源要准備好哦😯)
//創建跑馬燈
HXQMarqueeView *marqueeView = [[HXQMarqueeView alloc] initWithFrame:CGRectMake(0 100,self.view.bounds.size.width, 44)];
[self.view addSubview:marqueeView];
//初始化數據源
[marqueeView setItems:modelList];
//開始動畫
[marqueeView startAnimation];
//如果需要監聽點擊回調,請實現這個方法
[marqueeView addMarueeViewItemClickBlock:^(HXQMarqueeModel *model) {
NSLog(@"%@",model.title);
}];
swift版本(snapKit布局),更加簡潔完善
//
// HXQMarqueeView.swift
// hxquan-swift
//
// Created by Tiny on 2018/11/20.
// Copyright © 2018年 hxq. All rights reserved.
// 跑馬燈
import UIKit
class MarqueeModel: Equatable{
var title: String? //內容
var img: String? //頭像圖片 url
var textColor: UIColor = .black //字體默認顏色
var font: UIFont = .systemFont(ofSize: 14) //字體大小
var imageHolder: UIImage = HXQDefaultUserImage //頭像默認圖片
static func == (lhs: MarqueeModel, rhs: MarqueeModel) -> Bool {
return lhs.title == rhs.title &&
lhs.img == rhs.img &&
lhs.textColor == rhs.textColor &&
lhs.font == rhs.font &&
lhs.imageHolder == rhs.imageHolder
}
}
class MarqueeItem: UIView {
private var textLb: UILabel! //文字label
private var imgView: UIImageView! //圖片
/// 重寫setModel並賦值
fileprivate var model: MarqueeModel?{
didSet{
if model != nil {
textLb.text = model?.title
textLb.textColor = model!.textColor
textLb.font = model!.font
imgView.sd_setImage(with: URL(string: model!.img ?? ""), placeholderImage: model!.imageHolder)
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func setupUI(){
let gesture = UITapGestureRecognizer(target: self, action: #selector(itemClick))
addGestureRecognizer(gesture)
textLb = UILabel()
textLb.font = UIFont.systemFont(ofSize: 14)
textLb.textColor = UIColor.black
addSubview(textLb)
imgView = UIImageView()
imgView.layer.masksToBounds = true
addSubview(imgView)
imgView.snp.makeConstraints { (make) in
make.left.equalTo(5)
make.top.equalTo(5)
make.bottom.equalTo(-5)
make.width.equalTo(imgView.snp.height)
}
textLb.snp.makeConstraints { (make) in
make.centerY.equalToSuperview()
make.left.equalTo(imgView.snp.right).offset(5)
make.right.equalTo(-5)
}
}
/// item被點擊事件回調
fileprivate var itemDidTap:(() -> Void)?
@objc private func itemClick(){
//將事件傳遞出去
itemDidTap?()
}
override func layoutSubviews() {
super.layoutSubviews()
imgView.layer.cornerRadius = imgView.bounds.size.width*0.5
}
}
class HXQMarqueeView: UIView {
/// 初始化scrollView
private lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView(frame: .zero)
scrollView.scrollsToTop = false
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
return scrollView
}()
/// 初始化定時器
private lazy var timer: Timer = {[unowned self] in
let timer = Timer(timeInterval: 0.008, target: self, selector: #selector(startToMove), userInfo: nil, repeats: true)
RunLoop.current.add(timer, forMode: .common)
return timer
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func setupUI(){
layer.masksToBounds = true
addSubview(scrollView)
scrollView.snp.makeConstraints { (make) in
make.top.left.bottom.equalToSuperview()
make.width.equalToSuperview()
}
}
var marqueeHolder: String?
var marqueeFontSize: CGFloat = 14
var marqueeTextColor = UIColor.black
public var items = [MarqueeModel](){
didSet{
//判斷2次數據是否相同
if oldValue == items {
return
}
//關閉定時器
if timer.isValid {
timer.fireDate = Date.distantFuture
}
//移除scrollView中所有控件
for v in scrollView.subviews{
v.removeFromSuperview()
}
//顯示默認
if items.isEmpty{
//如果需要顯示默認值的話
if marqueeHolder != nil{
let lb = UILabel()
lb.textAlignment = .center
lb.text = marqueeHolder
lb.font = UIFont.systemFont(ofSize: marqueeFontSize)
lb.textColor = marqueeTextColor
scrollView.addSubview(lb)
lb.snp.makeConstraints { (make) in
make.height.equalToSuperview()
make.left.equalTo(20)
}
//更新scrollView的ContentSize
scrollView.snp.makeConstraints { (make) in
make.right.equalTo(lb.snp.right)
}
layoutIfNeeded()
scrollView.x = 0;
}
}else{
let margin: CGFloat = 10
let gap: CGFloat = 20
var last: MarqueeItem?
for (i,model) in items.enumerated(){
let item = MarqueeItem()
item.model = model
item.itemDidTap = { [unowned self] in
self.selectionBlock?(model,i)
}
scrollView.addSubview(item)
item.snp.makeConstraints { (make) in
if last == nil{
make.left.equalTo(margin)
}else{
make.left.equalTo(last!.snp.right).offset(gap)
}
make.height.equalToSuperview()
}
last = item
}
//更新scrollView的ContentSize
scrollView.snp.makeConstraints { (make) in
make.right.equalTo(last!.snp.right).offset(margin)
}
layoutIfNeeded()
//拿到contentSize重新更新scrollView約束
scrollView.snp.remakeConstraints { (make) in
make.width.equalTo(scrollView.contentSize.width)
make.top.bottom.equalToSuperview()
make.right.equalTo(last!.snp.right).offset(margin)
make.left.equalTo(self.snp.right)
}
//開始啟動定時器
if last != nil{
timer.fireDate = Date(timeIntervalSinceNow: 0)
}
}
}
}
private var selectionBlock: ((MarqueeModel,Int) -> Void)?
public func queeSelection(_ callBack: ((MarqueeModel,Int) -> Void)?){
selectionBlock = callBack
}
@objc private func startToMove(){
scrollView.x = scrollView.x - 0.5 ;
if scrollView.x <= -scrollView.contentSize.width {
scrollView.x = self.bounds.size.width;
}
}
deinit {
if timer.isValid {
timer.invalidate()
}
}
}
/// swift版本跑馬燈使用方法
//創建
let marquee = HXQMarqueeView()
view.addSubview(marquee)
//設置約束
marquee.snp.makeConstraints { (make) in
make.left.equalTo(20)
make.right.equalTo(-20)
make.top.equalTo(100)
make.height.equalTo(30)
}
//初始化數據源
var array = [MarqueeModel]()
for i in 0..<5 {
let item = MarqueeModel()
item.title = "我完事了,你們呢\(i)"
item.img = ""
item.textColor = COLOR_RANDOM()
item.font = HFont(12)
array.append(item)
}
//賦值
marquee.items = array
//監聽點擊
marquee.queeSelection { (model, index) in
print("\(model.title ?? "") + \(index)")
}
更多詳情請參考demo: https://github.com/qqcc1388/MarqueeViewDemo