iOS视频列表开发总结

2023-11-06

最近开发了一个较大的需求,即视频列表,特点是每个视频卡片高度不一致,包含不同的元素,若卡片长度超过一屏,还需要将底部的操作栏悬浮。可以上滑下滑自动切换到下一个播放.在这里插入图片描述

整体实现

UITableView作为容器,每一个Item都是一个视频。卡片高度使用自动布局计算,宽度跟手机屏幕宽度相等。

踩到的坑

  1. 自定义UITableviewCell或者是UICollectionviewCell时,添加子view要加到contentview上,若直接加到cell的view上,则点击事件会失效。
func setUPUi() {
        backgroundColor = UIColor.clear
        self.selectionStyle = .none
        let width = WowUtil.shared.getDeviceSize().0
        <!--一定要使用contentview-->
        self.contentView.addSubview(topBarContainer)
        topBarContainer.snp.makeConstraints { make in
            make.top.left.right.equalToSuperview()
            make.height.equalTo(VideoDetailCellTopBar.topBarHeight)
        }
}
  1. 约束一定要完整,第一个view的top和卡片的contentview的顶部对齐,最后一个view的bottom和contentview的bottom对齐。
  2. 如果卡片的某些View因为数据的不同而展示或者不展示,需要将View移除,同时将有约束关联的View的约束remake一次。
  3. 当使用reloaddata或者insert等collectionview相关操作时,或者页面旋转时,需要先设置下预估高度(contentoffset.y/rownumber),否则会出现列表跳动的情况。其根本原因是这些操作的前后,collectionview都是保证contentoffset不变,所以会根据contentoffset.y/estimateheight来决定要滚动到什么地方去。注意:estimatedHeight不能为负数,所以需要判断大于0再设置。
if portrait != self.isPortrait {
            self.isPortrait = portrait
            if let row = curHighLightIndexPath?.row , row > 0 {
                if collectionView.contentOffset.y > 0 {
                    collectionView.estimatedRowHeight = collectionView.contentOffset.y / CGFloat(row)
                }
            }
        }
  1. UItableview和UIcollectionview的随手指滚动到下一个or上一个的api实现方式:实现scrollViewWillEndDragging代理,根据滑动速度来判断是否需要吸顶,根据滑动方向来判断滑动到哪一个卡片。通过tableview.indexPathsForVisibleRows来获取当前页面的可见卡片,然后通过tableview.rectForRow(indexPath)来获取指定卡片在tableview中的位置,并把这个位置设置给targetContentOffset就能保证tableview滚动到指定位置了。
 func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        if abs(velocity.y) > changePageVelocityThreshold,let curVisibleIndexPaths = collectionView.indexPathsForVisibleRows { // 加速度超过阈值,才需要启动自动定位到上一页or下一页
            if velocity.y > 0 {
                //                手指从下往上滑
                if curVisibleIndexPaths.count > 1 {
                    //                    直接取第二个
                    //                    dedebugPrint("多个:\(screenRectItemLayoutAttrs[1].frame.origin.y)")
                    if let cell = collectionView.cellForRow(at: curVisibleIndexPaths[1]) {
                        targetContentOffset.pointee =  cell.frame.origin
                        curHighLightIndexPath = curVisibleIndexPaths[1]
                    }
                } else if curVisibleIndexPaths.count == 1 {
                    let curIndexPath = curVisibleIndexPaths.first!
//                                        debugPrint("单个,indexCount:\(curIndexPath.count)")
                    let count = collectionView.numberOfRows(inSection: 0)
                    if curIndexPath.row < (count - 1) {
                        let nextIndexPath = IndexPath(row: curIndexPath.row + 1, section: curIndexPath.section)
                        let nextRect = collectionView.rectForRow(at: nextIndexPath)
                        targetContentOffset.pointee = nextRect.origin
                        curHighLightIndexPath = nextIndexPath
                    }
                }
            } else if velocity.y < 0 {
                // 手指从上往下滑
                if curVisibleIndexPaths.count > 1 {
                    let topCardRect = collectionView.rectForRow(at: curVisibleIndexPaths[0])
                    
                    let topItemLayout = topCardRect.y
                    let topItemHeight = topCardRect.height
                    if collectionView.contentOffset.y > (topItemLayout + topItemHeight / 2) {
                        //    最顶部的卡片没有完全展示,并且未展示的部分超过卡片整体高度的1/2时,将最顶部卡片作为下一个展示的目标;否则将最顶部卡片的上一个卡片作为下一个展示的目标。
                        targetContentOffset.pointee = topCardRect.origin
                        self.curHighLightIndexPath = curVisibleIndexPaths[0]
                        // debugPrint("手指从上往下滑:if分支\(topItemLayout)")
                    } else {
                        let curIndexPath = curVisibleIndexPaths.first!
                        if curIndexPath.row > 0 {
                            let previousIndexPath = IndexPath(row: curIndexPath.row - 1, section: curIndexPath.section)
                            let previousCell =  collectionView.rectForRow(at: previousIndexPath)
                            targetContentOffset.pointee = previousCell.origin
                            self.curHighLightIndexPath = previousIndexPath
                            //debugPrint("手指从上往下滑:else分支\(previousCell.origin)")
                        }
                    }
                } else if curVisibleIndexPaths.count == 1 {
                    let curIndexPath = curVisibleIndexPaths.first!
                    if curIndexPath.row > 0 {
                        let previousIndexPath = IndexPath(row: curIndexPath.row - 1, section: curIndexPath.section)
                        let previousCell =  collectionView.rectForRow(at: previousIndexPath)
                        targetContentOffset.pointee = previousCell.origin
                        self.curHighLightIndexPath = previousIndexPath
//                          debugPrint("手指从上往下滑:else分支\(previousCell.origin)")
                    }
                }
            }
        } else {
            calCurrentHighlightItem()
        }
        
    }

6、UItableview卡片复用。项目中是在卡片绑定数据的时候开始预加载视频,卡片只有在即将可见的时候才会绑定数据,如果当前卡片特别长,那么下一个卡片就不会提前加载视频数据,处理方式就是我们手动预加载下一个卡片(当检测到屏幕内只有一个卡片时),并保存到一个map中,map的key是indexpath,value就是构建的卡片,在tableview的cellforindexpath回调中,先判断缓存map中是否存在指定卡片,有的话直接取。

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        <!--优先从缓存map中取cell-->
        if let cell = preBuildedCells[indexPath] {
            debugPrint("prebuildCell:getCell.\(indexPath.row)")
            preBuildedCells[indexPath] = nil
            return cell
        }
        return buildCell(indexPath)
    }
    
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        prebuildCell(scrollView)
        self.previousContentOffsetY = scrollView.contentOffset.y
    }    
    
 func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        if !decelerate {
            prebuildCell(scrollView)
            self.previousContentOffsetY = scrollView.contentOffset.y
        }
    }    
    <!--构建下一个Cell-->
func prebuildCell(_ scrollView: UIScrollView) {
        if let visibleIndexPaths = collectionView.indexPathsForVisibleRows, visibleIndexPaths.count == 1 {
            let count = collectionView.numberOfRows(inSection: 0)
            let curRow = visibleIndexPaths.first!.row
            let newContentOffsetY = scrollView.contentOffset.y
            if newContentOffsetY >= previousContentOffsetY, curRow < (count - 1) {
                let preBuildIndexPath = IndexPath(row: curRow + 1, section: 0)
//                debugPrint("prebuildCell:\(curRow + 1)")
                let cell = self.buildCell(preBuildIndexPath)
                <!--放到缓存map中-->
                preBuildedCells[preBuildIndexPath] = cell
            } else if newContentOffsetY < previousContentOffsetY, curRow > 0 {
                let preBuildIndexPath = IndexPath(row: curRow - 1, section: 0)
//                debugPrint("prebuildCell:\(curRow - 1)")
                let cell = self.buildCell(preBuildIndexPath)
                preBuildedCells[preBuildIndexPath] = cell
            }
        }
    }    

7、Uitableview的卡片的某个子View存在吸顶or吸底操作,实现方式:实现UIScrollview的滚动代理scrollViewDidScroll,在其中不断得检测当前屏幕内的可见卡片数量,并执行如下逻辑。

func checkBtmBarFloatStatus() { 
  let visibleCells =  self.collectionView.visibleCells
  let cell = visibleCells.first
  if visibleCells.count == 1 && cell.frame.origin.y + cell.frame.height - contentOffset > collectionView.frame.height   {
    <!-- 需要悬浮底部栏 -->
  } else if visibleCells.count == 2   {
     let firstCell = visibleCells.first
     let secondCell = visibleCells[1]
     if firstCell只露出了一点点&&secondCell.height>collectionView.height {
       <!-- 需要悬浮底部栏 -->
     } else {
       <!-- 恢复底部栏 -->
     }
  } else {
    <!-- 恢复底部栏 -->
  }      
}

func 悬浮底部栏() {
   /// 解决方案:需要悬浮底部栏时,把btmbar从当前cell中抠出来放到ViewController的根View上;不需要悬浮底部栏时,把btmbar加回到cell上。这里面还有个问题,因为btmbar的背景是透明的,当把btmbar放到根view上后,导致会露出底部UITableView的内容,解决方案是利用UIView的clipbounds属性,将UITableView加到一个容器View中,这个容器的高度为屏幕高度-底部栏高度,通过动态改变容器View的clipbounds属性,实现对UITableview的底部栏内容显示和不显示来解决问题。
}

8、视频预加载操作。iOS的系统apiAVPlayer有一个preroll方法就是用来预加载的,预加载的效果非常明显。未启动预加载时,开始加载->开始播放耗时1s,启用预加载后,耗时变为0.5s.

public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        guard context == &observerContext else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
            return
        }
        if keyPath == "status" {
            if let status = change?[.newKey] as? Int, status == AVPlayer.Status.readyToPlay.rawValue, YDVideoPlayer.openPreroll, player.rate == 0, self.prerollUrl != self.url {
                self.prerollUrl = self.url
               
                player.preroll(atRate: 1) { result in
                    debugPrint("YDVideoPlayer: preroll result url:\(self.url?.lastPathComponent)")
                }
            }
        }
        updateStatus()
    }

9、代码的整体架构设计。
卡片的子view很多,并且各个子view的交互比较复杂,可以抽象一个baseview来用模板模式来简化代码,并且把model数据直接设置给View,不要想着只塞入该View需要的字段,因为后续log上报或者一些其他交互行为很有可能依赖model本身。如下:

open class VideoDetailBaseView: UIView {
    private(set) var model:WowDetailModel?
    init() {
        super.init(frame:.zero)
        setUpUI()
    }
    required public init(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    /// 生成Cell的时候调用
    func setUpUI() {
        
    }
    /// 更新cell数据的时候调用
    func updateUI() {
        
    }
    /// 更新model时重置一些状态
    func resetState() {
        
    }
    func setData(_ model:WowDetailModel?,updateUI:Bool = false) {
        self.model = model
        self.resetState()
        if updateUI {
            self.updateUI()
        }
    }
    func startHighLight(){
    }
    
    func stopHighLight(){    
    }
}

10、视频播放的问题。AvPlayer播放是耗时的,不要直接在主线程中使用,需要将播放器的操作已经相关状态更新全部集中到一个队列中,保证状态的一致。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

iOS视频列表开发总结 的相关文章

随机推荐

  • 林园价值交易策略

    文章目录 选股策略 林园6条炒股 心经 选股策略 选股时可以考虑在低市盈率 高分红的绩优龙头股和确定性高的小盘股中选 所选择的上市公司的财务指标需符合七大标准 每股盈利不低于0 3元 净利润不少于7000万元 毛利率在20 以上 净资产回报
  • 病毒反调试跟踪

    跟踪一个反调试巨多的病毒样本 1 调用 QueryPerformanceCounter反调试 这个API调用了封装ZwQueryPerformanceCounter系统调用的ntdll NtQueryPerformanceCounter 0
  • 什么是mvvm模式

    MVVM 是把 MVC 的 Controller 和 MVP 的 Presenter 改成了 ViewModel View 的变化会 动更新到 ViewModel ViewModel 的变化也会 动同步到 View 上显示 这种 动 同步是
  • vue-cli3.0 多核编译ts及内存配置(解决项目过大编译内存溢出)

    主要配置webpack插件ForkTsChecker 在vue config js中配置 module exports configureWebpack config gt 多核启动编译及内存提升 const data config plu
  • 区块链将如何改变服装、改变时尚?

    越来越多的服装公司开始注意到在某些情况下 它们需要改变自己的商业模式 以耐克为例 他们不再把自己定位成一家服装公司 相反 他们说自己是一家科技公司 碰巧生产服装 他们的衣服和鞋子通常都装有传感器 用于跟踪心率 英里数或卡路里燃烧情况 这是因
  • MySQL - 表字段的默认值约束

    设置表字段的默认值 DEFAULT 当为数据库表中插入一条新记录时 如果没有为某个字段赋值 数据库系统就会自动为这个字段插入默认值 为了达到这种效果 可通过SQL语句关键字DEFAULT来设置 设置数据库表中某字段的默认值非常简单 可以在M
  • Vue UI 组件库大起底 element VS iview VS ...

    最近接触了几个开源项目 发现大家都在用iview框架 趁机整理一下自己接触过的几个基于Vue js的UI组件库 Element 一套为开发者 设计师和产品经理准备的基于 Vue 2 0 的桌面端组件库 由饿了么前端开源的UI框架 主要用于开
  • Java多线程实现抢票

    1 1抢票系统 多人抢票 package Demo8 多个线程同时操作一个对象 买车票例子 public class TestThread4 implements Runnable private int ticket nums 10 Ov
  • Couldn't read row 0, col -1 from CursorWindow. Make sure the Cursor is initialized correctly before

    在做项目时碰到一个问题 就是一直报 java lang IllegalStateException Couldn t read row 0 col 1 from CursorWindow Make sure the Cursor is in
  • QML设计登陆界面

    QML设计登陆界面 本文博客链接 http blog csdn net jdh99 作者 jdh 转载请注明 环境 主机 WIN7 开发环境 Qt5 2 说明 用QML设计一个应用的登陆界面 效果图 源代码 main qml javascr
  • python数据处理中的日期转换处理中的to_datetime()函数(一)

    python使用的是 jupyter notebook 话不多说 直接说说主要内容吧 一 函数简介 我们可以通过输入 import pandas as pd help pd to datetime 可以得到to datetime函数的相关作
  • 第三方登录之微信扫码登录

    文章目录 1 申请微信接入 2 项目环境搭建 3 后端Controller接口 4 HTML页面代码 5 测试结果 6 补充说明 小伙伴们有各种疑问可以去参考官方文档进行详细的学习下 微信开发文档 此次介绍的将是前后端不分离的微信扫码登录
  • 2022微信小程序填充昵称头像 open-type=“chooseAvatar“

    2021年7月份之后 微信开始加强对微信用户个人信息的安全防控 收回了相关服务端接口 微信后面也推出了前端填写昵称头像的方法 官方代码如下
  • gcc 与 g++ 的区别

    gcc 和 g GCC GNU Compiler Collection GUN 编译器集合 g 是 GCC 中的 GUN C Compiler C 编译器 在已编译好的 C 或者 C 代码的前提下 GCC 编译器已提供了调用接口 可以通过执
  • matlab循环每次循环都把结果输出来,怎么把循环的每一步结果都保存下来啊

    本帖最后由 芒点DK 于 2016 12 9 11 04 编辑 各位前辈请看我的代码 clear workspaces clear clc define variables rmin 0 rmax 8 N 9 dt 1 t 0 tmax 1
  • Flutter: 为字体增加渐变色描边

    文章目录 写在前面 内容 实现描边 实现渐变 一些调整 参考 写在前面 实现如下图的效果 这个数字的内部和外部都有渐变色 内容 实现描边 在网上搜索一轮 可以看到通过用 Stack 来让两个 Text叠加 并对上一个 Text设置外部描边
  • Java接口和多态

    Java接口 Java中的接口是一种定义了一组方法签名的抽象类型 它提供了一种方式来定义类之间的协议 即类应该实现哪些方法 在本教程中 我们将探讨Java中接口的使用和实现 步骤1 定义接口 我们首先需要定义一个接口 接口使用interfa
  • 27 KVM管理系统资源-管理虚拟CPU份额

    文章目录 27 KVM管理系统资源 管理虚拟CPU份额 27 1 概述 27 2 操作步骤 27 KVM管理系统资源 管理虚拟CPU份额 27 1 概述 虚拟化环境下 同一主机上的多个虚拟机竞争使用物理CPU 为了防止某些虚拟机占用过多的物
  • 什么是ARM TCM内存

    什么是ARM处理器上的TCM内存 它是一个驻留在处理器旁边的专用内存 还是一个配置为TCM 的RAM区域 如果它是专用内存 为什么我们可以配置它的位置和大小 TCM Tightly Coupled Memory是一个 或多个 小的专用内存区
  • iOS视频列表开发总结

    最近开发了一个较大的需求 即视频列表 特点是每个视频卡片高度不一致 包含不同的元素 若卡片长度超过一屏 还需要将底部的操作栏悬浮 可以上滑下滑自动切换到下一个播放 整体实现 UITableView作为容器 每一个Item都是一个视频 卡片高