超有梗AVFoundation總結(jié)

一篇較好的學(xué)習(xí)文章

AVFoundation Tutorial: Adding Overlays and Animations to Videos

AVFoundation的一些應(yīng)用

音視頻合成

超有梗1.0編輯頁重做了,我也對以前代碼做了優(yōu)化,現(xiàn)在錄音、音樂、音效的添加,都視作一個(gè)音塊。具體功能大家可以在AppStore下來自己看看??

首先看一下model

代碼里都添加了詳細(xì)注釋

class MediaBrick: NSObject {
    
    // MARK: - 音塊一共有4種,原視頻、錄音、音樂、音效
    enum MediaType {
        case video
        case record
        case music
        case soundEffect
    }
    
    var type: MediaType!
    /// 最早開始時(shí)間,由于音塊可以拖動(dòng)范圍,這個(gè)其實(shí)是最早時(shí)間的限制
    var startTime: TimeInterval = 0
    // (1)
    /// 最晚結(jié)束時(shí)間,由于音塊可以拖動(dòng)范圍,這個(gè)其實(shí)是最晚時(shí)間的限制
    var endTime: TimeInterval = 0
    /// 被編輯的開始時(shí)間
    let modifiedStartTimeVarible = Variable<TimeInterval>(0)
    /// 被編輯的結(jié)束時(shí)間
    let modifiedEndTimeVarible = Variable<TimeInterval>(0)
    
    /// 用于計(jì)算 以上時(shí)間都是基于視頻時(shí)間
    var videoDuration: TimeInterval = 0
    /// 這是一個(gè)view state,本來放在model是不合適的,不過這樣很方便讀取和傳遞
    var collectionViewContentWidth: CGFloat = 0
    
    /// 媒體文件的沙盒路徑
    var fileUrl: URL?
    /// 該段媒體文件的音量
    var preferredVolume: Float = 1
    
    /// 用于 type == .record
    var pitchType: PitchType = .original
    
    /// 用于 type == .music, 已經(jīng)被裁剪過
    var musicAsset: AVAsset?
    
    /// 用于 type == .soundEffect
    let soundEffectIconUrlVariable = Variable<URL?>(nil)
    
    // MARK: - 處理UI邏輯
    let isFoldVariable = Variable<Bool>(false)
    let isSelectedVariable = Variable<Bool>(false)
    let deleteSubject = PublishSubject<Void>()
    let beganModifyTimeSubject = PublishSubject<Void>()
    let endModifyTimeSubject = PublishSubject<Void>()
    
    /// 控制是否需要合成
    var isNeedCompose: Bool = true
    
    deinit {
        print("\(description) deinit")
    }
    
    /// 一個(gè)新的對象,只復(fù)制了4個(gè)時(shí)間,僅用于計(jì)算和處理UI
    func copy() -> MediaBrick {
        let mediaBrick = MediaBrick()
        mediaBrick.startTime = startTime
        mediaBrick.endTime = endTime
        mediaBrick.modifiedStartTimeVarible.value = modifiedStartTimeVarible.value
        mediaBrick.modifiedEndTimeVarible.value = modifiedEndTimeVarible.value
        return mediaBrick
    }
}

(1)Variable對象是RxSwift對象,它本身有存儲(chǔ)功能,例如:

let modifiedStartTimeVarible = Variable<TimeInterval>(0)
modifiedStartTimeVarible.value = 1
print(modifiedStartTimeVarible.value)

可以使用modifiedStartTimeVarible.value來讀寫值。

合成

    // 主要是把音頻合在視頻上,所以視頻的處理會(huì)有一些不同,傳參的時(shí)候把視頻的model和其他音頻的model分開了
    static func compose(videoBrick: MediaBrick, audioBricks: [MediaBrick]) -> (AVMutableComposition, AVMutableAudioMix)? {

        // 這個(gè)是最后的合成對象,新建的時(shí)候相當(dāng)于是一張白紙,準(zhǔn)備往上面畫畫
        let composition = AVMutableComposition()
        // 這個(gè)是控制最后的composition的音量的,一般來說都會(huì)被設(shè)計(jì)成composition的屬性,但iOS設(shè)計(jì)成了2個(gè)對象
        let audioMix = AVMutableAudioMix()
        // 初始化該屬性為一個(gè)空數(shù)組,之后可以直接往數(shù)組里添加對象
        audioMix.inputParameters = []
        
        // 如果沒有視頻文件,return nil并且記錄失敗
        guard let fileUrl = videoBrick.fileUrl else {
            logFail(mediaBrick: videoBrick)
            return nil
        }
        let videoAsset = AVAsset(url: fileUrl)
        
        // 視頻的全長范圍
        let range = CMTimeRange(start: kCMTimeZero, duration: videoAsset.duration)
        
        // 因?yàn)樾陆ǖ腸omposition是空的,先把原視頻的視軌添加上去
        // 依次獲取視頻資源的視軌originVideoAssetTrack;創(chuàng)建composition新加的視軌originVideoCompotionTrack
        guard let originVideoAssetTrack = videoAsset.tracks(withMediaType: .video).first,
            let originVideoCompotionTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid) else {
            logFail(mediaBrick: videoBrick)
            return nil
        }
        do {
            // 將originVideoCompotionTrack填滿originVideoAssetTrack的內(nèi)容
            try originVideoCompotionTrack.insertTimeRange(range, of: originVideoAssetTrack, at: kCMTimeZero)
        } catch {
            logFail(mediaBrick: videoBrick, error: error)
            return nil
        }
        // 到此添加完畢
        
        // 添加原視頻的音軌,音軌可能有多個(gè),先檢查沒有音軌return nil并且記錄失敗
        let audioTracks = videoAsset.tracks(withMediaType: .audio)
        guard audioTracks.count != 0 else {
            logFail(mediaBrick: videoBrick)
            return nil
        }
        // 所有被新建的originAudioCompositionTrack需要持有起來,之后被重合的音軌需要?jiǎng)h除原音音軌
        var originAudioCompositionTracks: [AVMutableCompositionTrack] = []
        for originAudioAssetTrack in audioTracks {
            // 循環(huán)里和上面的邏輯一樣
            guard let originAudioCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: kCMPersistentTrackID_Invalid) else {
                logFail(mediaBrick: videoBrick)
                continue
            }
            do {
                try originAudioCompositionTrack.insertTimeRange(range, of: originAudioAssetTrack, at: kCMTimeZero)
                originAudioCompositionTracks.append(originAudioCompositionTrack)
            } catch {
                logFail(mediaBrick: videoBrick, error: error)
                continue
            }
        }
        
        // 到此準(zhǔn)備工作做完了,現(xiàn)在composition已經(jīng)和原視頻文件具有相同的視軌和音軌了
        
        // 開始合成錄音、音樂、音效
        for audioBrick in audioBricks {
            
            var mediaAsset: AVAsset!
            switch audioBrick.type! {
            case .record:
                
                // 獲取本地錄音資源文件,從pcm轉(zhuǎn)到aac,并且完成變音功能
                guard let fileUrl = getAACFileUrl(recordBrick: audioBrick) else { continue }
                mediaAsset = AVAsset(url: fileUrl)
                
            case .music:

                // 因?yàn)橐魳房梢韵染庉?,?yōu)先取編輯之后的資源文件,再去原音樂資源文件
                if let asset = audioBrick.musicAsset {
                    mediaAsset = asset
                } else if let fileUrl = audioBrick.fileUrl {
                    mediaAsset = AVAsset(url: fileUrl)
                } else {
                    continue
                }
                
            case .soundEffect:
                
                // 獲取本地音效資源文件
                guard let fileUrl = audioBrick.fileUrl else { continue }
                mediaAsset = AVAsset(url: fileUrl)
                
            default:
                continue
            }
            
            // 和上面的總體邏輯一樣,獲取資源文件的音軌,添加composition的音軌
            for audioAssetTrack in mediaAsset.tracks(withMediaType: .audio) {
                guard let audioCompositionTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) else {
                    logFail(mediaBrick: audioBrick)
                    continue
                }
                
                // 然后把資源文件的音軌插入到composition的音軌
                // 但是這些音頻文件主要是在插入時(shí)間上有不同,原音軌用全范圍即可,這里用到的范圍會(huì)比較多
                // 一些范圍檢查
                let modifiedStartTime = max(audioBrick.modifiedStartTimeVarible.value, 0)
                let modifiedEndTime = min(audioBrick.modifiedEndTimeVarible.value, videoAsset.duration.seconds)
                guard modifiedStartTime < modifiedEndTime else { continue }
                
                // 參照音頻文件的時(shí)間,是該音頻資源內(nèi)部的時(shí)間
                // 被編輯的時(shí)間 - 最早時(shí)間,即是內(nèi)部的時(shí)間,這里使用的時(shí)間是CMTime
                let startTimeByAudio = CMTime(seconds: modifiedStartTime - audioBrick.startTime, preferredTimescale: audioAssetTrack.naturalTimeScale)
                // 這段音頻的總時(shí)長
                let audioDuration = CMTime(seconds: modifiedEndTime - modifiedStartTime, preferredTimescale: audioAssetTrack.naturalTimeScale)
                // 根據(jù)上面兩個(gè)時(shí)間,做出CMTimeRange
                let rangeByAudio = CMTimeRangeMake(startTimeByAudio, audioDuration)
                
                // 參照視頻文件的時(shí)間
                let startTimeByVideo = CMTime(seconds: modifiedStartTime, preferredTimescale: audioAssetTrack.naturalTimeScale)
                
                do {
                    // 開始填充audioCompositionTrack,將上面準(zhǔn)備好的參數(shù)填入
                    try audioCompositionTrack.insertTimeRange(rangeByAudio, of: audioAssetTrack, at: startTimeByVideo)
                } catch {
                    logFail(mediaBrick: audioBrick, error: error)
                    continue
                }
                
                // 這是控制這段音頻音量的代碼
                let inputParameter = AVMutableAudioMixInputParameters(track: audioCompositionTrack)
                inputParameter.setVolume(audioBrick.preferredVolume, at: kCMTimeZero)
                audioMix.inputParameters.append(inputParameter)
                
                // 如果是錄音和音樂,需要把原音軌對應(yīng)的聲音去掉,所以去掉對應(yīng)的范圍
                if audioBrick.type! != .soundEffect {
                    // replace origin audio to empty
                    let removeRange = CMTimeRangeMake(startTimeByVideo, audioDuration)
                    originAudioCompositionTracks.forEach {
                        $0.removeTimeRange(removeRange)
                        $0.insertEmptyTimeRange(removeRange)
                    }
                }
            }
        }
        // 返回的composition和audioMix,會(huì)被用在AVPlayer上進(jìn)行播放
        return (composition, audioMix)
    }

裁剪

// 視頻支持裁剪功能,第一個(gè)參數(shù)其實(shí)是上面compose方法產(chǎn)生的composition,同時(shí)需要視頻的model來獲取裁剪時(shí)間
    static func crop(asset: AVMutableComposition, videoBrick: MediaBrick) -> (AVMutableComposition, AVMutableVideoComposition?)? {
        
        // 同樣是新建一個(gè)空的composition
        let composition = AVMutableComposition()
        
        // 范圍檢查
        let startTime = videoBrick.modifiedStartTimeVarible.value
        let endTime = videoBrick.modifiedEndTimeVarible.value
        guard startTime < endTime else { return nil }
        
        // 這里和之前類似,將視頻資源的視軌插入到composition新加的視軌上
        guard let videoAssetTrack = asset.tracks(withMediaType: .video).first,
            let videoCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid) else {
            logFail(mediaBrick: videoBrick)
            return nil
        }
        // 區(qū)別是范圍的取值,范圍取成裁剪后的范圍,裁剪功能就完成了
        let startCMTime = CMTime(seconds: startTime, preferredTimescale: videoAssetTrack.naturalTimeScale)
        let endCMTime = CMTime(seconds: endTime, preferredTimescale: videoAssetTrack.naturalTimeScale)
        let range = CMTimeRange(start: startCMTime, end: endCMTime)
        do {
            try videoCompositionTrack.insertTimeRange(range, of: videoAssetTrack, at: kCMTimeZero)
        } catch {
            logFail(mediaBrick: videoBrick, error: error)
            return nil
        }
        
        // 這里是對豎直視頻的處理,如果視頻的方向不對,需要矯正(用手機(jī)豎直拍攝的視頻方向就不對)
        // 下面的代碼看做是固定處理代碼吧
        // (其實(shí)所有視軌插入都需要這段代碼,不過目前用來合成的視頻方向都是正確的,而自己上傳的視頻都會(huì)先被裁剪、矯正,再進(jìn)入編輯頁)
        var videoComposition: AVMutableVideoComposition?
        if videoAssetTrack.preferredTransform != .identity {
            
            let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoCompositionTrack)
            // (1)
            let transform = videoAssetTrack.ks.transform
            layerInstruction.setTransform(transform, at: startCMTime)
            
            let instruction = AVMutableVideoCompositionInstruction()
            instruction.timeRange = range
            instruction.layerInstructions = [layerInstruction]
            
            videoComposition = AVMutableVideoComposition()
            // (2)
            videoComposition!.renderSize = videoAssetTrack.ks.renderSize
            videoComposition!.frameDuration = CMTime(value: 1, timescale: 30)
            videoComposition!.instructions = [instruction]
        }
        
        // 下面和之前的邏輯類似,根據(jù)范圍裁剪
        for audioAssetTrack in asset.tracks(withMediaType: .audio) {
            guard let audioCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: kCMPersistentTrackID_Invalid) else {
                logFail(mediaBrick: videoBrick)
                continue
            }
            let startCMTime = CMTime(seconds: startTime, preferredTimescale: audioAssetTrack.naturalTimeScale)
            let endCMTime = CMTime(seconds: endTime, preferredTimescale: audioAssetTrack.naturalTimeScale)
            let range = CMTimeRange(start: startCMTime, end: endCMTime)
            do {
                try audioCompositionTrack.insertTimeRange(range, of: audioAssetTrack, at: kCMTimeZero)
            } catch {
                logFail(mediaBrick: videoBrick, error: error)
                continue
            }
        }
        // 返回的composition、videoComposition會(huì)在導(dǎo)出的時(shí)候使用
        return (composition, videoComposition)
    }

(1)(2)帶有.ks.的寫法都是自己添加的extension,具體代碼如下,主要是根據(jù)視頻的方向調(diào)整寬高

extension Kuso where T: AVAssetTrack {
    
    var renderSize: CGSize {
        let preferredTransform = base.preferredTransform
        let width = floor(base.naturalSize.width)
        let height = floor(base.naturalSize.height)
        
        if preferredTransform.b != 0 {
            return CGSize(width: height, height: width)
        } else {
            return CGSize(width: width, height: height)
        }
    }
    
    var transform: CGAffineTransform {
        let preferredTransform = base.preferredTransform
        let width = floor(base.naturalSize.width)
        let height = floor(base.naturalSize.height)
        
        if preferredTransform.b == 1 { // home在左
            return CGAffineTransform(translationX: height, y: 0).rotated(by: CGFloat.pi/2)
        } else if preferredTransform.b == -1 { // home在右
            return CGAffineTransform(translationX: 0, y: width).rotated(by: CGFloat.pi/2 * 3)
        } else { // home在上
            return CGAffineTransform(translationX: width, y: height).rotated(by: CGFloat.pi)
        }
    }
    
    var appropriateExportPreset: String {
        
        if renderSize.width <= 640 {
            return AVAssetExportPreset640x480
        } else if renderSize.width <= 960 {
            return AVAssetExportPreset960x540
        } else if renderSize.width <= 1280 {
            return AVAssetExportPreset1280x720
        } else {
            return AVAssetExportPreset1920x1080
        }
    }
}

導(dǎo)出

    // 完成視頻編輯后,需要把內(nèi)存里的composition audioMix videoComposition都導(dǎo)出到沙盒,存儲(chǔ)起來,用來上傳
    static func exportComposedVideo(composition: AVComposition, audioMix: AVAudioMix? = nil, videoComposition: AVVideoComposition? = nil) -> Observable<URL> {
        return Observable<URL>.create({ (observer) -> Disposable in
            
            // 根據(jù)視軌的分辨率取得合適的導(dǎo)出分辨率
            let exportPreset = composition.ks.appropriateExportPreset
            
            // 獲取兼容性的exportSession
            // (1)
            guard let exportSession = AVAssetExportSession.ks.compatibleSession(asset: composition, priorPresetName: exportPreset) else {
                return Disposables.create()
            }
            // 根據(jù)時(shí)間戳新建一個(gè)視頻文件路徑
            let outputUrl = FileManager.ks.newEditVideoUrl
            
            // 設(shè)置exportSession的參數(shù)
            exportSession.audioMix = audioMix
            exportSession.videoComposition = videoComposition
            exportSession.outputFileType = .mp4
            exportSession.outputURL = outputUrl
            exportSession.shouldOptimizeForNetworkUse = true
            exportSession.exportAsynchronously { [weak exportSession] in
                guard let es = exportSession else {
                    return
                }
                switch es.status {
                case .completed:
                    // 成功則發(fā)出最終的url
                    observer.onNext(outputUrl)
                case .failed:
                    // 失敗則拋出錯(cuò)誤
                    if let error = es.error {
                        logFail(error: error)
                        observer.onError(error)
                    }
                default:
                    break
                }
            }
            return Disposables.create {
                // 如果這個(gè)observer被取消了,也把正在export的session取消掉
                exportSession.cancelExport()
            }
        })
            // 很隨意的異步一下,其實(shí)意義不大
            .observeOn(MainScheduler.asyncInstance)
    }

(1)其實(shí)就是按照分辨率等級(jí)依次取合適的AVAssetExportSession

let defaultPresets = [AVAssetExportPreset1280x720, AVAssetExportPreset960x540, AVAssetExportPreset640x480, AVAssetExportPresetMediumQuality, AVAssetExportPresetLowQuality]

extension Kuso where T == AVAssetExportSession {
    
    static func compatibleSession(asset: AVAsset, priorPresetName: String) -> AVAssetExportSession? {
        
        if let es = T(asset: asset, presetName: priorPresetName) {
            return es
        } else {
            
            let compatiblePresets = T.exportPresets(compatibleWith: asset)
            for defaultPreset in defaultPresets {
                guard compatiblePresets.contains(defaultPreset) else {
                    continue
                }
                return T(asset: asset, presetName: defaultPreset)
            }
            return nil
        }
    }
}

添加水印、文字等


    static func addWatermark(fileUrl: URL) -> Observable<URL> {
        
        return Observable<URL>.create { (observer) -> Disposable in
            
            // 都是從空的composition開始
            let composition = AVMutableComposition()

            // 資源文件、視頻范圍
            let videoAsset = AVAsset(url: fileUrl)
            let range = CMTimeRange(start: kCMTimeZero, end: videoAsset.duration)
            
            // 獲取資源文件視軌,創(chuàng)建新的待添加的視軌
            guard let videoAssetTrack = videoAsset.tracks(withMediaType: .video).first,
                let videoCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid) else {
                    return Disposables.create()
            }
            do {
                // 將videoCompostionTrack填滿videoAssetTrack的內(nèi)容
                try videoCompositionTrack.insertTimeRange(range, of: videoAssetTrack, at: kCMTimeZero)
            } catch {
                observer.onError(error)
                return Disposables.create()
            }
            
            // 加水印需要使用AVMutableVideoCompositionLayerInstruction
            let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoCompositionTrack)
            // 如果方向不對 矯正
            if videoAssetTrack.preferredTransform != .identity {
                let transform = videoAssetTrack.ks.transform
                layerInstruction.setTransform(transform, at: kCMTimeZero)
            }
            // 固定寫法
            let instruction = AVMutableVideoCompositionInstruction()
            instruction.timeRange = range
            instruction.layerInstructions = [layerInstruction]
            
            let videoComposition = AVMutableVideoComposition()
            videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
            let renderSize = videoAssetTrack.ks.renderSize
            videoComposition.renderSize = renderSize
            videoComposition.instructions = [instruction]
            
            // 加水印的層級(jí)分為3個(gè)layer parentLayer作為底 videoLayer上放的是視頻 還有一個(gè)watermarkLayer上放置水印或者其他自定義類容 例如文字
            let parentLayer = CALayer()
            let videoLayer = CALayer()
            parentLayer.addSublayer(videoLayer)
            [parentLayer, videoLayer].forEach{
                $0.frame = CGRect(origin: .zero, size: renderSize)
            }
            // 固定寫法
            videoComposition.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: videoLayer, in: parentLayer)
            // 3個(gè)layer從下到上依次為 parentLayer videoLayer watermarkLayer,后2個(gè)layer的順序可以根據(jù)需求交換,改變size大小等
            // 這里創(chuàng)建的watermarkLayer已經(jīng)被添加了一些CoreAnimation,這樣加出來的水印就可以動(dòng)了
            let watermarkLayer = self.createWatermarkLayer(parentSize: renderSize)
            parentLayer.addSublayer(watermarkLayer)
            
            // 兼容某些質(zhì)量很差的視頻,把導(dǎo)出參數(shù)降低,AVAssetExportPresetMediumQuality其實(shí)是一種很兼容,視頻很模糊的選項(xiàng)
            var exportPreset: String!
            let minFrameDuration = videoAssetTrack.minFrameDuration
            if minFrameDuration.seconds < 0.001 {
                exportPreset = AVAssetExportPresetMediumQuality
            } else {
                exportPreset = videoAssetTrack.ks.appropriateExportPreset
            }
            
            // 對音頻做上面的類似操作,只需要加進(jìn)去即可
            for originAudioAssetTrack in videoAsset.tracks(withMediaType: .audio) {
                guard let audioCompositionTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) else {
                    continue
                }
                do {
                    try audioCompositionTrack.insertTimeRange(range, of: originAudioAssetTrack, at: kCMTimeZero)
                } catch {
                    observer.onError(error)
                    return Disposables.create()
                }
            }
            
            // 導(dǎo)出到沙盒
            guard let exportSession = AVAssetExportSession.ks.compatibleSession(asset: composition, priorPresetName: exportPreset) else {
                return Disposables.create()
            }
            // 根據(jù)時(shí)間戳新建一個(gè)水印目錄下的文件
            let outputUrl = FileManager.ks.newWatermarkVideoUrl
            
            exportSession.videoComposition = videoComposition
            exportSession.outputFileType = .mp4
            exportSession.outputURL = outputUrl
            exportSession.shouldOptimizeForNetworkUse = true
            
            // exportSession有progress可以讀取,但是不能kvo或者有回調(diào)通知,只能加個(gè)timer來讀取進(jìn)度
            let timer = Timer(timeInterval: 0.05, repeats: true, block: { [weak exportSession] (timer) in
                guard let es = exportSession else {
                    return
                }
                let progress = Double(es.progress) * 0.49 + 0.5
                self.progressHandler?(progress)
                if es.progress == 1 {
                    timer.invalidate()
                }
            })
            RunLoop.current.add(timer, forMode: RunLoopMode.commonModes)
            timer.fire()
            
            exportSession.exportAsynchronously { [weak exportSession] in
                guard let es = exportSession else {
                    return
                }
                switch es.status {
                case .completed:
                    // 成功后發(fā)出最后的url
                    observer.onNext(outputUrl)
                case .failed:
                    // 有錯(cuò)誤則發(fā)出錯(cuò)誤
                    if let error = es.error {
                        observer.onError(error)
                    }
                default:
                    break
                }
            }
            return Disposables.create {
                // 如果該操作被取消,停掉timer和exportSession
                timer.invalidate()
                exportSession.cancelExport()
            }
        }
    }
    
    /* 下面是創(chuàng)建layer的坐標(biāo)、大小計(jì)算,以及動(dòng)畫添加 */
    static func createWatermarkLayer(parentSize: CGSize) -> CALayer {
        // 坐標(biāo)軸原點(diǎn)為0,0 右上角為 +,+
        let multiper = max(parentSize.width, parentSize.height)/1080 * 2.3
        
        let layerSize = CGSize(width: multiper * 95, height: multiper * 61)
        let layerStartPosition = CGPoint(x: layerSize.width/2, y: parentSize.height - layerSize.height/2)
        let layerEndPosition = CGPoint(x: parentSize.width - layerSize.width/2, y: layerSize.height/2)
        let layer = CALayer()
        layer.frame = CGRect(origin: .zero, size: layerSize)
        layer.position = layerStartPosition
        addPositionAnimation(layer: layer, startPosition: layerStartPosition, endPosition: layerEndPosition)
        
        let logoSize = CGSize(width: multiper * 90, height: multiper * 50)
        let logoPosition = CGPoint(x: logoSize.width/2, y: 11 * multiper + logoSize.height/2)
        let logoLayer = CALayer()
        logoLayer.frame = CGRect(origin: .zero, size: logoSize)
        logoLayer.position = logoPosition
        addContentsAnimation(layer: logoLayer)
        layer.addSublayer(logoLayer)
        
        let idSize = CGSize(width: layerSize.width, height: multiper * 16.5)
        let idPosition = CGPoint(x: idSize.width/2 - 11.5 * multiper, y: 5 * multiper + idSize.height/2)
        let idLayer = CATextLayer()
        idLayer.frame = CGRect(origin: .zero, size: idSize)
        idLayer.position = idPosition
        idLayer.string = "ID: \(userId.description)"
        idLayer.foregroundColor = UIColor.white.cgColor
        idLayer.fontSize = 12 * multiper
        idLayer.font = CGFont.init(UIFont.boldSystemFont(ofSize: idLayer.fontSize).fontName as CFString)
        idLayer.alignmentMode = kCAAlignmentRight
        layer.addSublayer(idLayer)
        return layer
    }
    
    static func addPositionAnimation(layer: CALayer, startPosition: CGPoint, endPosition: CGPoint) {
        let keyframe = CAKeyframeAnimation(keyPath: "position")
        keyframe.values = [startPosition, endPosition]
        keyframe.duration = 10
        keyframe.isRemovedOnCompletion = false
        keyframe.fillMode = kCAFillModeForwards
        keyframe.beginTime = AVCoreAnimationBeginTimeAtZero
        keyframe.calculationMode = kCAAnimationDiscrete
        layer.add(keyframe, forKey: "position")
    }
    
    static func addContentsAnimation(layer: CALayer) {
        
        let imgs = (0...21).map { idx -> CGImage in
            let name = "wm_\(idx)"
            return UIImage(named: name)!.cgImage!
        }
        layer.contents = imgs.first
        
        let keyframe = CAKeyframeAnimation(keyPath: "contents")
        keyframe.duration = 1
        keyframe.values = imgs
        keyframe.repeatCount = .greatestFiniteMagnitude
        keyframe.isRemovedOnCompletion = false
        keyframe.beginTime = AVCoreAnimationBeginTimeAtZero
        layer.add(keyframe, forKey: "contents")
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容