当前位置: 首页 > news >正文

UIScrollView与UIStackView的完美组合打造灵活滚动布局

优雅实现水平滚动工具栏:UIScrollView与UIStackView的完美组合

本文将介绍如何通过UIScrollView与UIStackView的黄金组合,实现一个灵活高效的水平滚动工具栏。这种组合方案具有以下优势:

  1. 自动布局:UIScrollView提供滚动容器,UIStackView管理动态布局
  2. 命令模式:采用协议化设计实现按钮与业务逻辑解耦
  3. 动态扩展:支持按钮的灵活增减
  4. 性能优化:原生控件组合带来流畅滚动体验
  5. 代码简洁:告别复杂的布局计算

核心实现原理

1. 视图层级结构
ButtonBarView
└── UIScrollView
    └── UIStackView (Horizontal)
        └── 自定义按钮视图(SLVerticalButtonView)×N
2. 关键布局策略
// 核心约束设置
// StackView 约束到 ScrollView 的 ContentLayoutGuide
[_stackView.topAnchor constraintEqualToAnchor:_scrollView.contentLayoutGuide.topAnchor],
[_stackView.leadingAnchor constraintEqualToAnchor:_scrollView.contentLayoutGuide.leadingAnchor],
[_stackView.trailingAnchor constraintEqualToAnchor:_scrollView.contentLayoutGuide.trailingAnchor],
[_stackView.bottomAnchor constraintEqualToAnchor:_scrollView.contentLayoutGuide.bottomAnchor],
        
// StackView 高度与 ScrollView 的 FrameLayoutGuide 一致
[_stackView.heightAnchor constraintEqualToAnchor:_scrollView.frameLayoutGuide.heightAnchor]
3. 动态内容更新
- (void)setCommands:(NSArray<id<Command>> *)commands {
    // 清空旧视图
    // 动态生成新按钮
    // 更新布局
}
// 更新布局
- (void)updateStackViewWidthIfNeeded {
    [self.scrollView layoutIfNeeded]; // 立即布局以获取准确宽度
    CGFloat contentWidth = self.stackView.frame.size.width;
    CGFloat scrollViewWidth = self.scrollView.bounds.size.width;
    
    if (contentWidth < scrollViewWidth && self.commands.count > 0) {
        if (!self.stackViewWidthConstraint) {
            // 添加约束使stackView宽度等于scrollView的宽度
            self.stackViewWidthConstraint = [self.stackView.widthAnchor constraintEqualToAnchor:self.scrollView.frameLayoutGuide.widthAnchor];
            self.stackViewWidthConstraint.priority = UILayoutPriorityRequired;
            [self.stackViewWidthConstraint setActive:YES];
            [self.scrollView layoutIfNeeded]; // 应用新约束
        }
    } else {
        if (self.stackViewWidthConstraint) {
            [self.stackViewWidthConstraint setActive:NO];
            self.stackViewWidthConstraint = nil;
            [self.scrollView layoutIfNeeded];
        }
    }
}

完整实现代码(Objective-C)

//====================
// 命令协议
//====================
@protocol Command <NSObject>
@property (strong, nonatomic) ExtendCompositional *descriptor;
- (void)execute;
@end

//====================
// 命令工厂协议
//====================
@protocol CommandFactory <NSObject>
- (NSArray<id<Command>> *)commandsForContext:(UIViewController *)context;
@end

//====================
// 基础命令抽象类
//====================
@interface Command : NSObject <Command>
@property (weak, nonatomic) UIViewController *context;
+ (instancetype)commandWithContext:(UIViewController *)context;
@end

@implementation Command
@synthesize descriptor = _descriptor;
+ (instancetype)commandWithContext:(UIViewController *)context {
    Command *cmd = [self new];
    cmd.context = context;
    return cmd;
}

- (void)execute { /* 子类实现 */ }
@end

//====================
// 工具栏视图
//====================
@interface ButtonBarView : UIView<UIScrollViewDelegate>
@property (strong, nonatomic) UIScrollView *scrollView;
@property (strong, nonatomic) UIStackView *stackView;
@property (strong, nonatomic) NSArray<id<Command>> *commands;
@end

@interface ButtonBarView ()
@property (nonatomic, strong) NSLayoutConstraint *stackViewWidthConstraint;
@end

@implementation ButtonBarView
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self setupScrollSystem];
    }
    return self;
}


- (void)setupScrollSystem {
    _scrollView = [[UIScrollView alloc] initWithFrame:self.bounds];
    _scrollView.showsHorizontalScrollIndicator = NO;
    _scrollView.delegate = self;
    
    _stackView = [[UIStackView alloc] init];
    _stackView.axis = UILayoutConstraintAxisHorizontal;
    _stackView.alignment = UIStackViewAlignmentFill;
    _stackView.distribution = UIStackViewDistributionFillEqually;
    _stackView.spacing = 20;
    
    [_scrollView addSubview:_stackView];
    [self addSubview:_scrollView];
    
    // 布局约束
    _stackView.translatesAutoresizingMaskIntoConstraints = NO;
    _scrollView.translatesAutoresizingMaskIntoConstraints = NO;
    
    [NSLayoutConstraint activateConstraints:@[
        [_scrollView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
        [_scrollView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
        [_scrollView.topAnchor constraintEqualToAnchor:self.topAnchor],
        [_scrollView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
        
        // StackView 约束到 ScrollView 的 ContentLayoutGuide
        [_stackView.topAnchor constraintEqualToAnchor:_scrollView.contentLayoutGuide.topAnchor],
        [_stackView.leadingAnchor constraintEqualToAnchor:_scrollView.contentLayoutGuide.leadingAnchor],
        [_stackView.trailingAnchor constraintEqualToAnchor:_scrollView.contentLayoutGuide.trailingAnchor],
        [_stackView.bottomAnchor constraintEqualToAnchor:_scrollView.contentLayoutGuide.bottomAnchor],
        
        // StackView 高度与 ScrollView 的 FrameLayoutGuide 一致
        [_stackView.heightAnchor constraintEqualToAnchor:_scrollView.frameLayoutGuide.heightAnchor]
    ]];
}
- (void)setCommands:(NSArray<id<Command>> *)commands {
    _commands = commands;
    // 清空旧按钮
    [_stackView.arrangedSubviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
    
    // 动态生成按钮
    [commands enumerateObjectsUsingBlock:^(id<Command> command, NSUInteger idx, BOOL *stop) {
        SLVerticalButtonView *button = [[SLVerticalButtonView alloc] initWithFrame:CGRectZero target:command action:@selector(execute)];
        button.image = command.descriptor.image;
        button.title = command.descriptor.title;
        button.identifier = command.descriptor.identifier;
        [button setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
        [self.stackView addArrangedSubview:button];
    }];
    [self updateStackViewWidthIfNeeded];
}
- (void)layoutSubviews {
    [super layoutSubviews];
    [self updateStackViewWidthIfNeeded];
}
- (void)updateStackViewWidthIfNeeded {
    [self.scrollView layoutIfNeeded]; // 立即布局以获取准确宽度
    CGFloat contentWidth = self.stackView.frame.size.width;
    CGFloat scrollViewWidth = self.scrollView.bounds.size.width;
    
    if (contentWidth < scrollViewWidth && self.commands.count > 0) {
        if (!self.stackViewWidthConstraint) {
            // 添加约束使stackView宽度等于scrollView的宽度
            self.stackViewWidthConstraint = [self.stackView.widthAnchor constraintEqualToAnchor:self.scrollView.frameLayoutGuide.widthAnchor];
            self.stackViewWidthConstraint.priority = UILayoutPriorityRequired;
            [self.stackViewWidthConstraint setActive:YES];
            [self.scrollView layoutIfNeeded]; // 应用新约束
        }
    } else {
        if (self.stackViewWidthConstraint) {
            [self.stackViewWidthConstraint setActive:NO];
            self.stackViewWidthConstraint = nil;
            [self.scrollView layoutIfNeeded];
        }
    }
}
@end

使用示例

1. 创建命令工厂
@interface CommandFactory : NSObject <CommandFactory>
@end
@implementation CommandFactory
- (NSArray<id<Command>> *)commandsForContext:(UIViewController *)context {
    return @[
        [AddPhotoCommand commandWithContext:context],
        [AddTextCommand commandWithContext:context],
        [AddBackgroundCommand commandWithContext:context],
        [AddFilterCommand commandWithContext:context]
    ];
}

@end
2. 在ViewController中使用
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建工具栏
    ButtonBarView *toolbar = [[ButtonBarView alloc] initWithFrame:CGRectMake(0, 100, self.view.bounds.size.width, 80)];
    
    // 获取命令列表
    id<CommandFactory> factory = [CommandFactory new];
    toolbar.commands = [factory commandsForContext:self];
    
    [self.view addSubview:toolbar];
}
3. 自定义按钮示例(SLVerticalButtonView)
// 实现垂直排列的图标+文字按钮
@implementation SLVerticalButtonView

- (instancetype)initWithFrame:(CGRect)frame target:(id)target action:(SEL)action {
    self = [super initWithFrame:frame];
    // 添加图片视图和文字标签
    // 设置点击事件
    [self addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
    return self;
}
@end

关键点解析

  1. 布局锚点选择

    • contentLayoutGuide:决定可滚动内容范围
    • frameLayoutGuide:限制可视区域范围
  2. 性能优化

    • 复用UIStackView的自动布局
    • 避免复杂图层结构
  3. 扩展性设计

    • 遵循命令模式
    • 支持动态命令配置

效果展示

水平滚动工具栏将自动根据内容多少:

  • 内容少时:居中显示
  • 内容多时:支持左右滑动
  • 自动维持统一高度

这种实现方案完美结合了UIScrollView的滚动能力和UIStackView的自动布局优势,特别适合需要动态生成水平排列元素的场景,避免选择UICollectionView层级结构比较多的视图。通过命令模式的引入,使得每个按钮的行为可以灵活配置,极大提高了代码的可维护性和扩展性。

相关文章:

  • 【项目】视频点播
  • Html常用代码
  • Apache SeaTunnel 人物专访 | 张东浩:从使用者到Committer的开源历程
  • 第七步:简单爬虫与网页测试
  • 【达梦数据库】代理用户的使用
  • 网页制作11-html,css,javascript初认识のCCS样式列表(下)
  • SD-WAN解决方案架构(SD WAN Solution Architecture)
  • 如何确保爬虫遵守1688的使用协议
  • HTML——标题标签与段落标签
  • Mac安装jdk教程
  • JavaWeb6、Servlet
  • Android 低功率蓝牙之BluetoothGattCallback回调方法详解
  • Android 低功率蓝牙之BluetoothGattCharacteristic详解
  • 极狐GitLab 17.9 正式发布,40+ DevSecOps 重点功能解读【一】
  • “深入浅出”系列之Linux篇:(12)C++网络编程
  • nvm 让 Node.js 版本切换更灵活
  • 记录一些面试遇到的问题
  • Linux系统之配置HAProxy负载均衡服务器
  • powermock,mock使用笔记
  • 重生之我在 CSDN 学习 KMP 算法
  • 网文书单|推荐4本网文,可以当作《绍宋》代餐
  • 哈马斯与以色列在多哈举行新一轮加沙停火谈判
  • 解锁儿时愿望!潘展乐战胜孙杨,全国冠军赛男子400自夺冠
  • 北邮今年本科招生将首次突破四千人,新增低空技术与工程专业
  • 国税总局上海市税务局回应刘晓庆被举报涉嫌偷漏税:正依法依规办理
  • 互降关税后,从中国至美国的集装箱运输预订量飙升近300%