iOS 抖音首页头部滑动标签的实现

抖音首页的头部滑动标签(通常称为"Segmented Control"或"Tab Bar")是一个常见的UI组件,可以通过以下几种方式实现:

1. 使用UISegmentedControl

最简单的实现方式是使用系统自带的UISegmentedControl

let segmentedControl = UISegmentedControl(items: ["推荐", "关注", "同城"])
segmentedControl.selectedSegmentIndex = 0 
segmentedControl.addTarget(self, action: #selector(segmentChanged(_:)), for: .valueChanged)
navigationItem.titleView = segmentedControl @objc func segmentChanged(_ sender: UISegmentedControl) {// 处理标签切换 print("Selected segment: \(sender.selectedSegmentIndex)")
}

2. 自定义实现(更接近抖音效果)

抖音的效果通常是水平滚动的标签栏,可以这样实现:

import UIKit class TikTokTabBar: UIView {private let scrollView = UIScrollView()private var buttons: [UIButton] = []private let indicator = UIView()private var currentIndex: Int = 0 var titles: [String] = [] {didSet {setupButtons()}}var onTabSelected: ((Int) -> Void)?override init(frame: CGRect) {super.init(frame: frame)setupUI()}required init?(coder: NSCoder) {super.init(coder: coder)setupUI()}private func setupUI() {scrollView.showsHorizontalScrollIndicator = false addSubview(scrollView)indicator.backgroundColor = .red scrollView.addSubview(indicator)}override func layoutSubviews() {super.layoutSubviews()scrollView.frame = bounds var x: CGFloat = 0 let buttonHeight = bounds.height - 4 let padding: CGFloat = 20 for (index, button) in buttons.enumerated() {let width = button.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: buttonHeight)).width + padding * 2 button.frame = CGRect(x: x, y: 0, width: width, height: buttonHeight)x += width if index == currentIndex {indicator.frame = CGRect(x: button.frame.minX + padding, y: buttonHeight, width: button.frame.width - padding * 2, height: 3)}}scrollView.contentSize = CGSize(width: x, height: bounds.height)}private func setupButtons() {buttons.forEach { $0.removeFromSuperview() }buttons.removeAll()for (index, title) in titles.enumerated() {let button = UIButton(type: .custom)button.setTitle(title, for: .normal)button.setTitleColor(index == 0 ? .white : .lightGray, for: .normal)button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)button.tag = index scrollView.addSubview(button)buttons.append(button)}}@objc private func buttonTapped(_ sender: UIButton) {selectTab(at: sender.tag, animated: true)onTabSelected?(sender.tag)}func selectTab(at index: Int, animated: Bool) {guard index >= 0 && index < buttons.count else { return }let button = buttons[index]currentIndex = index UIView.animate(withDuration: animated ? 0.25 : 0) {self.buttons.forEach {$0.setTitleColor($0.tag == index ? .white : .lightGray, for: .normal)}self.indicator.frame = CGRect(x: button.frame.minX + 20, y: button.frame.height, width: button.frame.width - 40, height: 3)// 确保选中的标签可见 let visibleRect = CGRect(x: button.frame.minX - 30, y: 0, width: button.frame.width + 60, height: self.scrollView.frame.height)self.scrollView.scrollRectToVisible(visibleRect, animated: animated)}}
}

3. 结合PageViewController实现完整效果

要实现抖音首页的完整效果(滑动标签同时控制页面切换),可以结合UIPageViewController

class TikTokHomeViewController: UIViewController {private let tabBar = TikTokTabBar()private var pageViewController: UIPageViewController!private var viewControllers: [UIViewController] = []override func viewDidLoad() {super.viewDidLoad()// 设置标签栏 tabBar.titles = ["推荐", "关注", "同城"]tabBar.onTabSelected = { [weak self] index in self?.selectPage(at: index, animated: true)}navigationItem.titleView = tabBar // 设置页面控制器 pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)pageViewController.delegate = self pageViewController.dataSource = self // 添加子控制器 viewControllers = [RecommendationViewController(),FollowingViewController(),NearbyViewController()]addChild(pageViewController)view.addSubview(pageViewController.view)pageViewController.didMove(toParent: self)pageViewController.setViewControllers([viewControllers[0]], direction: .forward, animated: false)}private func selectPage(at index: Int, animated: Bool) {guard index >= 0 && index < viewControllers.count else { return }let direction: UIPageViewController.NavigationDirection = index > tabBar.currentIndex ? .forward : .reverse pageViewController.setViewControllers([viewControllers[index]], direction: direction, animated: animated)tabBar.selectTab(at: index, animated: animated)}
}extension TikTokHomeViewController: UIPageViewControllerDelegate, UIPageViewControllerDataSource {func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {guard let index = viewControllers.firstIndex(of: viewController), index > 0 else { return nil }return viewControllers[index - 1]}func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {guard let index = viewControllers.firstIndex(of: viewController), index < viewControllers.count - 1 else { return nil }return viewControllers[index + 1]}func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {if completed, let currentVC = pageViewController.viewControllers?.first, let index = viewControllers.firstIndex(of: currentVC) {tabBar.selectTab(at: index, animated: true)}}
}

高级优化

1. 动画效果:可以添加更流畅的滑动动画和指示器动画
2. 字体缩放:选中的标签可以放大字体,未选中的缩小
3. 预加载:预加载相邻的页面以提高响应速度
4. 性能优化:对于大量标签,实现重用机制

下面是优化的具体实现

1. 平滑滑动动画与指示器效果优化

实现思路

  • 监听UIScrollView的滚动偏移量
  • 根据偏移量动态计算指示器位置和宽度
  • 实现标签颜色渐变效果

代码实现

// 在TikTokTabBar类中添加以下方法 
private var lastContentOffset: CGFloat = 0 func scrollViewDidScroll(_ scrollView: UIScrollView) {guard scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating else { return }let offsetX = scrollView.contentOffset.x let scrollViewWidth = scrollView.bounds.width let progress = (offsetX / scrollViewWidth) - CGFloat(currentIndex)// 防止快速滑动时progress超出范围 let clampedProgress = max(-1, min(1, progress))updateTabAppearance(progress: clampedProgress)updateIndicatorPosition(progress: clampedProgress)lastContentOffset = offsetX 
}private func updateTabAppearance(progress: CGFloat) {let absProgress = abs(progress)for (index, button) in buttons.enumerated() {// 当前标签和下一个标签 if index == currentIndex || index == currentIndex + (progress > 0 ? 1 : -1) {let isCurrent = index == currentIndex let targetIndex = isCurrent ? (progress > 0 ? currentIndex + 1 : currentIndex - 1) : currentIndex guard targetIndex >= 0 && targetIndex < buttons.count else { continue }let targetButton = buttons[targetIndex]// 颜色渐变 let currentColor = UIColor.white let targetColor = UIColor.lightGray let color = isCurrent ? currentColor.interpolate(to: targetColor, progress: absProgress) : targetColor.interpolate(to: currentColor, progress: absProgress)button.setTitleColor(color, for: .normal)// 字体缩放 let minScale: CGFloat = 0.9 let maxScale: CGFloat = 1.1 let scale = isCurrent ? maxScale - (maxScale - minScale) * absProgress : minScale + (maxScale - minScale) * absProgress button.transform = CGAffineTransform(scaleX: scale, y: scale)} else {// 其他标签保持默认状态 button.setTitleColor(.lightGray, for: .normal)button.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)}}
}private func updateIndicatorPosition(progress: CGFloat) {guard currentIndex >= 0 && currentIndex < buttons.count else { return }let currentButton = buttons[currentIndex]var nextIndex = currentIndex + (progress > 0 ? 1 : -1)nextIndex = max(0, min(buttons.count - 1, nextIndex))let nextButton = buttons[nextIndex]let absProgress = abs(progress)// 计算指示器位置和宽度 let currentFrame = currentButton.frame let nextFrame = nextButton.frame let originX = currentFrame.minX + (nextFrame.minX - currentFrame.minX) * absProgress let width = currentFrame.width + (nextFrame.width - currentFrame.width) * absProgress indicator.frame = CGRect(x: originX + 20,y: currentFrame.height,width: width - 40,height: 3 )
}// UIColor扩展,用于颜色插值 
extension UIColor {func interpolate(to color: UIColor, progress: CGFloat) -> UIColor {var fromRed: CGFloat = 0, fromGreen: CGFloat = 0, fromBlue: CGFloat = 0, fromAlpha: CGFloat = 0 var toRed: CGFloat = 0, toGreen: CGFloat = 0, toBlue: CGFloat = 0, toAlpha: CGFloat = 0 self.getRed(&fromRed, green: &fromGreen, blue: &fromBlue, alpha: &fromAlpha)color.getRed(&toRed, green: &toGreen, blue: &toBlue, alpha: &toAlpha)let red = fromRed + (toRed - fromRed) * progress let green = fromGreen + (toGreen - fromGreen) * progress let blue = fromBlue + (toBlue - fromBlue) * progress let alpha = fromAlpha + (toAlpha - fromAlpha) * progress return UIColor(red: red, green: green, blue: blue, alpha: alpha)}
}

2. 字体缩放效果优化

实现思路

  • 根据滑动进度动态调整标签字体大小
  • 当前选中标签放大,相邻标签适当缩小
  • 其他标签保持最小尺寸

代码实现
上面的updateTabAppearance方法已经包含了字体缩放逻辑,这里补充字体缩放的具体参数:

// 在updateTabAppearance方法中添加以下参数 
let minScale: CGFloat = 0.9   // 最小缩放比例 
let maxScale: CGFloat = 1.1  // 最大缩放比例 
let scale = isCurrent ? maxScale - (maxScale - minScale) * absProgress : minScale + (maxScale - minScale) * absProgress button.transform = CGAffineTransform(scaleX: scale, y: scale)

3. 页面预加载机制

实现思路

  • 预加载当前页面相邻的页面
  • 使用UIPageViewController的缓存机制
  • 监听滑动方向提前准备内容

代码实现

// 在TikTokHomeViewController中添加预加载逻辑 
private var pendingIndex: Int?
private var direction: UIPageViewController.NavigationDirection = .forward func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {if let pendingVC = pendingViewControllers.first,let index = viewControllers.firstIndex(of: pendingVC) {pendingIndex = index }
}func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {if completed, let pendingIndex = pendingIndex {currentIndex = pendingIndex tabBar.selectTab(at: currentIndex, animated: true)// 预加载相邻页面 preloadAdjacentPages()}pendingIndex = nil 
}private func preloadAdjacentPages() {// 预加载前一个页面 if currentIndex > 0 {let previousIndex = currentIndex - 1 if let previousVC = pageViewController.dataSource?.pageViewController(pageViewController,viewControllerBefore: viewControllers[currentIndex]) {// 确保视图已加载 _ = previousVC.view }}// 预加载后一个页面 if currentIndex < viewControllers.count - 1 {let nextIndex = currentIndex + 1 if let nextVC = pageViewController.dataSource?.pageViewController(pageViewController,viewControllerAfter: viewControllers[currentIndex]) {// 确保视图已加载 _ = nextVC.view }}
}// 修改selectPage方法以支持方向判断 
private func selectPage(at index: Int, animated: Bool) {guard index >= 0 && index < viewControllers.count else { return }direction = index > currentIndex ? .forward : .reverse pageViewController.setViewControllers([viewControllers[index]], direction: direction, animated: animated) { [weak self] _ in self?.preloadAdjacentPages()}currentIndex = index tabBar.selectTab(at: index, animated: animated)
}

4. 性能优化与标签重用

实现思路

  • 对于大量标签,实现重用机制
  • 只保留可视区域附近的标签
  • 动态加载和卸载标签

代码实现

// 在TikTokTabBar中添加重用逻辑 
private let reusableQueue = NSMutableSet()
private var visibleButtons = 
private var allTitles = func setTitles(_ titles: [String]) {allTitles = titles updateVisibleButtons()
}private func updateVisibleButtons() {// 计算当前可见范围 let visibleRange = calculateVisibleRange()// 移除不再可见的按钮 for (index, button) in visibleButtons {if !visibleRange.contains(index) {button.removeFromSuperview()reusableQueue.add(button)visibleButtons.removeValue(forKey: index)}}// 添加新可见的按钮 for index in visibleRange {if visibleButtons[index] == nil {let button = dequeueReusableButton()configureButton(button, at: index)scrollView.addSubview(button)visibleButtons[index] = button }}// 更新布局 setNeedsLayout()
}private func calculateVisibleRange() -> ClosedRange<Int> {let contentOffsetX = scrollView.contentOffset.x let visibleWidth = scrollView.bounds.width // 计算第一个和最后一个可见的索引 var startIndex = 0 var endIndex = allTitles.count - 1 // 这里可以添加更精确的计算逻辑 // 例如根据按钮宽度和偏移量计算 // 扩展可见范围,预加载左右各2个 startIndex = max(0, startIndex - 2)endIndex = min(allTitles.count - 1, endIndex + 2)return startIndex...endIndex 
}private func dequeueReusableButton() -> UIButton {if let button = reusableQueue.anyObject() as? UIButton {reusableQueue.remove(button)return button }return UIButton(type: .custom)
}private func configureButton(_ button: UIButton, at index: Int) {button.setTitle(allTitles[index], for: .normal)button.setTitleColor(index == currentIndex ? .white : .lightGray, for: .normal)button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)button.tag = index 
}// 在scrollViewDidScroll中调用updateVisibleButtons 
func scrollViewDidScroll(_ scrollView: UIScrollView) {updateVisibleButtons()// 其他滚动逻辑...
}

5. 综合优化与细节处理

5.1 弹性效果限制

// 在TikTokTabBar中 
scrollView.bounces = false 
scrollView.alwaysBounceHorizontal = false 

5.2 点击动画效果

@objc private func buttonTapped(_ sender: UIButton) {// 点击动画 UIView.animate(withDuration: 0.1, animations: {sender.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)}) { _ in UIView.animate(withDuration: 0.1) {sender.transform = .identity }}selectTab(at: sender.tag, animated: true)onTabSelected?(sender.tag)
}

5.3 性能优化提示

// 在TikTokTabBar的初始化中 
layer.shouldRasterize = true 
layer.rasterizationScale = UIScreen.main.scale 

5.4 内存管理优化

// 在视图控制器中 
deinit {scrollView.delegate = nil 
}

总结

通过以上高级优化实现,你可以获得一个接近抖音效果的滑动标签栏,具有以下特点:

  1. 平滑的滑动动画和指示器过渡效果
  2. 动态字体缩放和颜色渐变
  3. 高效的页面预加载机制
  4. 优化的性能与内存管理
  5. 标签重用机制支持大量标签

这些优化可以显著提升用户体验,使滑动更加流畅,响应更加迅速,同时保持良好的内存使用效率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/news/909065.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ThreadLocal实现原理

ThreadLocal 是 Java 中实现线程封闭&#xff08;Thread Confinement&#xff09;的核心机制&#xff0c;它通过为每个线程创建变量的独立副本来解决多线程环境下的线程安全问题。 Thread └── ThreadLocalMap (threadLocals) // 每个线程持有的专属Map├── Entry[] tab…

【笔记】结合 Conda任意创建和配置不同 Python 版本的双轨隔离的 Poetry 虚拟环境

如何结合 Conda 任意创建和配置不同 Python 版本的双轨隔离的Poetry 虚拟环境&#xff1f; 在 Python 开发中&#xff0c;为不同项目配置独立且适配的虚拟环境至关重要。结合 Conda 和 Poetry 工具&#xff0c;能高效创建不同 Python 版本的 Poetry 虚拟环境&#xff0c;接下来…

defineAsyncComponent

下面,我们来系统的梳理关于 defineAsyncComponent 懒加载 的基本知识点: 一、异步组件核心概念 1.1 什么是异步组件? 异步组件是 Vue 中一种按需加载组件的机制,允许将组件代码拆分为独立的 chunk,在需要时再从服务器加载。这种技术能显著提升应用初始加载速度。 1.2 为…

ANeko v1.0.3 | 在手机里养只宠物猫 实时互动 动画细腻

ANeko是一款专为喜欢猫咪的用户设计的互动养宠应用。它让你在手机屏幕上拥有一只可爱的猫咪动画&#xff0c;这只猫咪会实时跟随你的手指触摸轨迹&#xff0c;带来生动有趣的互动体验。该应用不仅保留了用户熟悉的交互式猫动画&#xff0c;还结合了现代高清图形技术&#xff0c…

人工智能AI

AI 简介 AI 使我们能够生成可以改进卫生保健的出色软件,让人能够克服生理上的不便,改进智能基础结构,创造令人惊叹的娱乐体验,甚至拯救地球! 什么是 AI? 简而言之,AI 就是一种模仿人类行为和能力的软件。 关键工作负载包括: 机器学习 - 它通常是 AI 系统的基础,也是…

Vue 中 data 选项:对象 vs 函数

Vue 中 data 选项&#xff1a;对象 vs 函数 在 Vue 开发中&#xff0c;data 选项可以使用对象或函数形式&#xff0c;了解它们的使用场景非常重要。下面我将通过一个直观的示例来展示两者的区别和适用场景。 <!DOCTYPE html> <html lang"zh-CN"> <h…

python打卡第49天

知识点回顾&#xff1a; 通道注意力模块复习空间注意力模块CBAM的定义 CBAM 注意力模块介绍 从 SE 到 CBAM&#xff1a;注意力机制的演进 之前我们介绍了 SE&#xff08;Squeeze-and-Excitation&#xff09;通道注意力模块&#xff0c;其本质是对特征进行增强处理。现在&#…

iOS和桌面双端抓包实战经验总结:Sniffmaster与常见工具组合解析

近几年&#xff0c;移动端和桌面端的网络调试工作变得越来越“棘手”。过去一个代理证书搞定的场景&#xff0c;现在常常被HTTPS加密、双向验证、App安全策略给难住。特别是涉及到iOS平台时&#xff0c;很多传统抓包方案都不再适用。作为一名在多个平台开发和测试的程序员&…

cloudstudio腾讯云:matplotlib 设置中文字体

检查可用字体&#xff1a; import matplotlib.font_manager as fm fonts [f.name for f in fm.fontManager.ttflist] print(fonts) # 查看系统中可用的字体列表# 列出所有中文字体文件 !fc-list :langzh没有中文字体&#xff0c;需要下载 !sudo apt-get install fonts-wqy-m…

Django中的ORM的使用步骤----以MySQL为例

1 以纯Python的形式创建项目虚拟环境 2 命令安装Django 3 在当前虚拟环境目录下命令创建Django项目 4 命令创建app 注&#xff1a; 若想将创建的子应用存放到指定目录&#xff0c;如app&#xff0c; 那么需要先手动创建app目录&#xff0c;再手动创建子应用目录&#xff0c;如o…

Rust 学习笔记:通过 Send 和 Sync trait 实现可扩展并发性

Rust 学习笔记&#xff1a;通过 Send 和 Sync trait 实现可扩展并发性 Rust 学习笔记&#xff1a;通过 Send 和 Sync trait 实现可扩展并发性Send trait&#xff1a;允许在线程之间转移所有权Sync trait&#xff1a;允许多线程访问手动实现 Send 和 Sync 是不安全的练习题 Rust…

【C++】第十一节—一文详解vector(使用+杨辉三角+深度剖析+模拟实现+细节详细补充)

Hi&#xff0c;我是云边有个稻草人&#xff0c;偶尔中二的C领域博主^(*&#xffe3;(oo)&#xffe3;)^&#xff0c;与你分享专业知识—— C_本篇博客所属专栏—持续更新中—欢迎订阅喔 目录 一、vector的介绍及使用 1.1 vector的介绍 1.2 vector的使用 &#xff08;1&…

华为智选携手IAM:突破技术边界,重塑智慧健康家居新时代

华为智选与IAM的联动创研&#xff0c;是科技与健康两大领域深度结合的推动者&#xff0c;更是健康智能家电创新的引领者。他们不再只是产品的制造商&#xff0c;而是生活方式的革新者——用创新科技重构健康生活&#xff0c;用智慧生态重塑家居体验。在这场深度的跨界融合中&am…

基于cornerstone3D的dicom影像浏览器 第三十一章 从PACS服务加载图像

文章目录 前言一、两个服务接口1. 查询检查接口2. 查询图像接口 二、查询界面组件三、修改归档总结 前言 "基于cornerstone3D的dicom影像浏览器"系列文章中都是加载本地文件夹的的dicom图像。 作为一个合格的dicom影像浏览器需要对接PACS服务端&#xff0c;从PACS服…

STM32+rt-thread判断是否联网

一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…

基于React Native开发HarmonyOS 5.0医疗类应用

随着HarmonyOS 5.0的发布和React Native技术的成熟&#xff0c;开发者现在可以利用React Native框架为HarmonyOS平台构建高性能的跨平台医疗应用。 一、技术选型与优势 1.React Native HarmonyOS的组合优势 &#xff08;1&#xff09;跨平台能力​​&#xff1a;React Nati…

姜伟生《统计至简》

姜伟生《统计至简》 系列丛书之一 这套书图真漂亮&#xff0c;字间距也大&#xff0c;特别合适直接作为课件。但是理论上弱&#xff0c;有的地方算法也get不点上。适合初学者&#xff0c;因为能看图说话&#xff1b;又不适合初学者&#xff0c;因为没有解析、没有分析。 这学…

滚动—横向滚动时,如何直接滚动到对应的内容板块

使用scrollIntoView方法方法解读 scrollIntoView 是 HTML 元素&#xff08;HTMLElement&#xff09;的一个方法。当调用该方法时&#xff0c;它会尝试将调用它的元素滚动到浏览器的可视区域内。这个方法特别适用于处理页面上的滚动行为&#xff0c;比如让用户能够快速定位到页面…

HTML5 定位网页元素

1. 定位&#xff08;position&#xff09; position&#xff1a;static&#xff08;标准&#xff09; position&#xff1a;relative&#xff08;相对定位&#xff09; 偏移量的方向 相对定位的规律 浮动元素设置相对定位 position&#xff1a;absolute&#xff08;绝对…

分类数据集 - 植物分类数据集下载

数据集介绍&#xff1a;植物分类数据集&#xff0c;真实场景高质量图片数据&#xff1b;适用实际项目应用&#xff1a;自然场景植物分类项目&#xff0c;以及作为通用分类数据集场景数据的补充&#xff1b;数据集类别&#xff1a;标注说明&#xff1a;采用文件夹来区分不同的目…