iOS中實現LED跑馬燈效果
實現原理是使用scrollView, 將需要滾動的label添加兩次到 scrollView的subView下面, 然后通過滾動scrollView來實現跑馬燈效果。
具體實現代碼如下:
// // KMScrollLabel.swift // StopSmokingPrograms // // Created by Fran on 15/11/2. // Copyright © 2015年 kimree. All rights reserved. // import UIKit // 文字滾動方向 enum KMScrollDirection: Int{ case Left = 0 case Right case Up case Down } class KMScrollLabel: UIScrollView { // label 是只讀的 // 在外部只修改label的text即可 private var label = UILabel() var textLabel: UILabel{ get{ return label } } // label是否在滑動 private var keepScroll = false // 動畫時間 var animationDuringTime:Double = 5 deinit{ KMLog("KMScrollLabel deinit") } convenience init(){ self.init(frame: CGRectZero) } override init(frame: CGRect) { super.init(frame: frame) KMLog("init frame") self.scrollEnabled = false self.showsHorizontalScrollIndicator = false self.showsVerticalScrollIndicator = false self.addSubview(label) } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) KMLog("init aDecoder") self.scrollEnabled = false self.showsHorizontalScrollIndicator = false self.showsVerticalScrollIndicator = false self.addSubview(label) } // MARK: - 開始滑動 func startScroll(direction: KMScrollDirection = .Left){ fitFrame(direction) // 當label不處於滑動狀態時, 才可以開始滑動, 避免動畫效果疊加 if !keepScroll{ keepScroll = true self.contentOffset = CGPointZero beginScroll(direction) } } // removeAllAnimations 之后會立刻執行 animation block 的 completion 部分, // 目前無法確定 completion 部分的參數總會是 false, 為了避免巧合, 需要配合 keepScroll 參數一起使用 func stopScroll(){ keepScroll = false self.layer.removeAllAnimations() } // MARK: - 具體怎樣滑動 func beginScroll(direction: KMScrollDirection = .Left){ let w = label.frame.size.width let h = label.frame.size.height switch direction{ case .Left: self.contentOffset.x = 0 UIView.animateWithDuration(animationDuringTime, delay: 0, options: UIViewAnimationOptions.CurveLinear, animations: { [weak self]() -> Void in self?.contentOffset.x = w }, completion: { [weak self](finished: Bool) -> Void in if finished && (self != nil && self!.keepScroll){ self!.contentOffset.x = 0 self!.beginScroll(direction) } }) case .Right: self.contentOffset.x = w UIView.animateWithDuration(animationDuringTime, delay: 0, options: UIViewAnimationOptions.CurveLinear, animations: { [weak self]() -> Void in self?.contentOffset.x = 0 }, completion: { [weak self](finished: Bool) -> Void in if finished && (self != nil && self!.keepScroll){ self!.contentOffset.x = w self!.beginScroll(direction) } }) case .Up: self.contentOffset.y = 0 UIView.animateWithDuration(animationDuringTime, delay: 0, options: UIViewAnimationOptions.CurveLinear, animations: { [weak self]() -> Void in self?.contentOffset.y = h }, completion: { [weak self](finished: Bool) -> Void in if finished && (self != nil && self!.keepScroll){ self!.contentOffset.y = 0 self!.beginScroll(direction) } }) case .Down: self.contentOffset.y = h UIView.animateWithDuration(animationDuringTime, delay: 0, options: UIViewAnimationOptions.CurveLinear, animations: { [weak self]() -> Void in self?.contentOffset.y = 0 }, completion: { [weak self](finished: Bool) -> Void in if finished && (self != nil && self!.keepScroll){ self!.contentOffset.y = h self!.beginScroll(direction) } }) } } // MARK: - 適配frame private func fitFrame(direction: KMScrollDirection){ // 按照水平方向移動的要求修改frame func fitFrameAtHorizontalDirection(){ let width = self.frame.size.width var height = self.frame.size.height label.numberOfLines = 1 label.lineBreakMode = .ByWordWrapping var size = label.sizeThatFits(CGSizeMake(CGFloat.max, height)) // scroll 的 height 不能小於 label 的 height if height < size.height{ self.frame.size.height = size.height height = size.height } // label 的 width 要加上 scroll width 的一半作為留白, 並且 label 的 width 不能小於 scroll 的 width size.width += width / 2.0 if size.width < width{ size.width = width } label.frame = CGRectMake(0, (height - size.height) / 2.0, size.width, size.height) let tempLabel = NSKeyedUnarchiver.unarchiveObjectWithData(NSKeyedArchiver.archivedDataWithRootObject(label)) as! UILabel tempLabel.frame.origin.x += size.width self.addSubview(tempLabel) self.contentSize = CGSizeMake(2 * size.width, height) } // 按照豎直方向移動的要求修改frame func fitFrameAtVerticalDirection(){ var width = self.frame.size.width let height = self.frame.size.height label.numberOfLines = 0 label.lineBreakMode = .ByWordWrapping let originalText = label.text // 獲取單個文字的寬度 label.text = "田" let singleSize = label.sizeThatFits(CGSizeZero) label.text = originalText var size = label.sizeThatFits(CGSizeMake(singleSize.width, CGFloat.max)) if width < size.width{ self.frame.size.width = size.width width = size.width } size.height += height / 2.0 if size.height < height{ size.height = height } label.frame = CGRectMake((width - size.width) / 2.0, 0, size.width, size.height) let tempLabel = NSKeyedUnarchiver.unarchiveObjectWithData(NSKeyedArchiver.archivedDataWithRootObject(label)) as! UILabel tempLabel.frame.origin.y += size.height self.addSubview(tempLabel) self.contentSize = CGSizeMake(width, 2 * size.height) } // 首先遍歷 scroll 的所有 subView, 將占位的 label 移除 for view in self.subviews{ if view != label{ view.removeFromSuperview() } } switch direction{ case .Left, .Right: fitFrameAtHorizontalDirection() case .Up, .Down: fitFrameAtVerticalDirection() } } /* // Only override drawRect: if you perform custom drawing. // An empty implementation adversely affects performance during animation. override func drawRect(rect: CGRect) { // Drawing code } */ }
注意: 如果在有navigationController的界面使用時,由於跑馬燈中使用到了UIScrollView,此時跑馬燈的Label顯示不正常,解決方法:
self.automaticallyAdjustsScrollViewInsets = false // 不想采用上訴方式時, 可以調整scrollView的contentInset來達到目的 // 需要在 kmScrollView.startScroll() 之后調用 // kmScrollView.contentInset.top = -64