使用 AutoLayout 在不同高度的两列内堆叠

Posted

技术标签:

【中文标题】使用 AutoLayout 在不同高度的两列内堆叠【英文标题】:Using AutoLayout to stack within two Columns of varying Heights 【发布时间】:2015-07-27 16:32:19 【问题描述】:

ios 8.1 为目标

我正在使用 AutoLayout 在 TableCell 中布置多个标签。其中一些标签是可选的,有些可以包装它们的文本。它们被分成两个“列”,这些列只是 TableCell 的 ContentView 中的两个 UIView。我的约束以编程方式应用。

第二次更新

如果没有下面的SwiftArchitect's answer,我将无法解决这个问题并接受他的回答。但是因为我的都是代码,在自定义表格单元中,我还添加了一个separate answer below

更新

为了阻止标签拉伸到大于所需大小,我之前将 SetContentHuggingPrioritySetContentCompressionResistancePriority 设置为 1000,因为我认为这相当于说“我想要标签将其内容拥抱到其确切高度,我不希望它被垂直压缩“ 正如您在下面的红色和粉色示例中看到的那样,AutoLayout 显然没有遵守此要求。

this.notesLabel.SetContentHuggingPriority(1000, UILayoutConstraintAxis.Vertical);
this.notesLabel.SetContentCompressionResistancePriority(1000, UILayoutConstraintAxis.Vertical);

我删除了这些优先级的设置,标签不再被压扁,这是我最初的问题。当然,现在某些标签的长度超出了它们需要的高度。

    为什么删除 Hugging 和 Compression 优先级可以解决我的问题 问题? 如何在不返回上一期的情况下使红框(红框不是稍后添加的单元格的一部分)中的文本不展开?

以下是设置压缩和拥抱优先级时的几张屏幕截图。背景颜色用于调试

一般问题是包含视图(紫色和红色)的大小调整为两者中较小的一个。正如您在顶部看到的那样,“优先级 3”正在被删除,因为左列容器不需要更高。

在下一个示例中,没有 Priority 标签,但 EventDate 被压缩。

【问题讨论】:

您的容器视图有哪些限制?即使您有 1000 套,并且外部视图已决定它需要更小,something 必须给予!捕获视图层次结构并查看运行时约束可能会为您提供所需的信息。 【参考方案1】:

以下答案已经过编写和测试。它适用于 iPhone 和 iPad,纵向和横向。最高的柱子获胜,而另一柱子只占用它需要的空间。如果需要,它甚至可以修改为垂直居中对象。它解决了垂直裁剪的标签问题,以及动态缩放。

初步建议

如果可以,请使用Storyboard。您可以使用最先进的 GUI 直观地测试所有约束。 不要不要修改拥抱、压缩甚至UILabel 高度:让每个标签垂直占据它需要的空间,并且只添加顶部和侧面锚点 使用额外的视图作为容器来定义每列的宽度。使用multiplier 得到,比如说2 三分之二和三分之一。 让这些视图计算其理想高度,方法是在最低标签的底部添加一个高度约束 (leftColumn.bottom 等于 minimumLeftLabel.bottom) 不要动态添加或删除视图;而是隐藏它们,以便它们保留相关的约束。

解决方案说明

为简单起见,我创建了 2 个子视图,每列 1 个,并将它们并排放置。它们锚定在顶部/左侧和顶部/右侧,计算它们的宽度,它们的高度来自它们各自的内容(*)。

左右子视图有一个 1/2 multiplier,我在其中添加了一个 2 像素的 constant 作为边距。这两列内的标签左右锚定(leading space 到容器,trailing space 到容器),边距为 8 像素。这可确保标签不会溢出其列。

    考虑UITableViewCell 的高度是2 个内列中最大的。换句话说,containerView.height >= left.height containerView.height >= right.height。 确保您没有删除任何不可见的标签。 view.hidden 不会破坏你的约束,这就是你想要的。 将每个UILabel 左右锚定到容器,最上面的容器也是如此,但每个后续的label.top 都应该锚定到它上方的.bottom。这样,您的内容就会流动。您可以根据需要添加边距。

(*) 最后一个关键是将每列的高度与列上的约束联系起来,使其等于该列最低标签的.bottom。在上面的示例中,您可以清楚地看到蓝色背景的右列比左列短。

虽然我看到您需要代码中的解决方案,但我在不到 15 分钟的时间内使用Storyboard 创建了我的示例。它不仅仅是一个概念,它是一个实际的实现。它只有 0 行代码,适用于所有 iOS 设备。顺便说一句,它也有 0 个错误

所有约束列表

注意 >= 散布在各处。它们是使您的列独立变高的关键。

LRNSLayoutContraint 几乎相同。

获取故事板here,以及详细文章there。

【讨论】:

我知道 XCode IDE 的强大功能,但几乎我们的整个 Xamarin.iOS 应用程序都是由代码构建的。我也有用于 L 和 R 列的容器。我正在删除一些视图,但那是在设置约束之前并驱动重用标识符。您能解释一下将Leading\Tailing 空间设置为左右约束之间的区别吗?我想我正在做你在“最终钥匙”中建议的事情。我将回顾我们的代码。谢谢 能否请您为那些 ">=" 约束提供 XCode 中的约束属性的屏幕截图。正如您从白色“背景”屏幕截图中看到的那样,我不再收到剪裁标签,也没有触摸 Hugging 或 Compression。您的解决方案的哪一部分可以防止标签占用如此多的空间? "container.bottom = lowerLabel.bottom" 只是看了看,我正在做相反的事情,我正在设置 "lowestLabel.bottom = container.bottom"。立即审核 今天很忙,正在整理其他问题。我已经开始编辑我们的约束,希望我会成功。你能分享你的故事板作为一个要点吗? gist.github.com 我没有收到任何 LayoutConstraint 警告。 container.bottom=lowestLabel.bottom 的约束,这个约束加到哪里有关系吗?这是一个 TableCell,我将它添加到单元格的 ContentView 中。它是我用作两列容器的 ContentView【参考方案2】:

我已经接受了SwiftArchitect's answer,但是看到我在使用基于代码的 TableCell 方法之后,我将添加一个单独的答案。没有他的帮助我 不可能走到这一步。

我正在使用 MVVMCross 和 Xamarin iOS,我的 TableCell 继承自 MvxTableViewCell

子视图的创建

从 Cell 的 ctor 我创建所有必要的 UILabel 并通过设置 view.TranslatesAutoresizingMaskIntoConstraints = false 关闭 AutoResizingMasks

同时我创建了两个 UIViews leftColumnContainer 和 rightColumnContainer。这些 TranslatesAutoresizingMaskIntoConstraints 再次设置为 false。

相关标签作为子视图添加到leftColumnContainerrightColumnContainer UIViews。然后将这两个容器作为 SubViews 添加到 TableCell 的 ContentView 中

this.ContentView.AddSubviews(this.leftColumnContainer, this.rightColumnContainer);
this.ContentView.TranslatesAutoresizingMaskIntoConstraints = true;

UILabel 都是通过 MVVMCross DelayBind 调用绑定的数据

设置布局约束(UpdateConstraints())

TableCell 的布局取决于单元格的数据,8 个标签中有 5 个是可选的,8 个标签中有 4 个需要支持文本换行

我要做的第一件事是将leftColumnContainer 的顶部和左侧固定到TableCell.ContentView。然后是'rightColumnContainer'的顶部和右侧到TableCell.ContentView。该设计要求右列小于左列,因此使用缩放来完成。我正在使用FluentLayout 来应用这些约束

this.ContentView.AddConstraints(
                this.leftColumnContainer.AtTopOf(this.ContentView),
                this.leftColumnContainer.AtLeftOf(this.ContentView, 3.0f),
                this.leftColumnContainer.ToLeftOf(this.rightColumnContainer),
                this.rightColumnContainer.AtTopOf(this.ContentView),
                this.rightColumnContainer.ToRightOf(this.leftColumnContainer),
                this.rightColumnContainer.AtRightOf(this.ContentView),
                this.rightColumnContainer.WithRelativeWidth(this.ContentView, 0.35f));

对 ToLeftOf 和 ToRight of 的调用将左列的右边缘和右列的左边缘彼此相邻放置

来自 SwiftArchitect 的解决方案的一个关键部分是将 TableCell 的 ContentView 的高度设置为 >= 到 leftColumnContainerrightColumnContainer 的高度。如何用 FluentLayout 做这些并不是很明显,所以它们是“速写的”

this.ContentView.AddConstraint(
              NSLayoutConstraint.Create(this.ContentView, NSLayoutAttribute.Height, NSLayoutRelation.GreaterThanOrEqual, this.leftColumnContainer, NSLayoutAttribute.Height, 1.0f, 5.0f));

            this.ContentView.AddConstraint(
              NSLayoutConstraint.Create(this.ContentView, NSLayoutAttribute.Height, NSLayoutRelation.GreaterThanOrEqual, this.rightColumnContainer, NSLayoutAttribute.Height, 1.0f, 5.0f));

然后我将每列中第一个标签的顶部、左侧和右侧约束到列容器。这是左列第一个标签的示例

this.leftColumnContainer.AddConstraints(
                this.categoryLabel.AtTopOf(this.leftColumnContainer, CellPadding),
                this.categoryLabel.AtRightOf(this.leftColumnContainer, CellPadding),
                this.categoryLabel.AtLeftOf(this.leftColumnContainer, CellPadding));

对于每个可选标签,我首先检查 MVVMCross DataContext 以查看它们是否可见。如果它们是可见的,则对 Left、Top 和 Right 应用类似的约束,并将 Top 约束到上面标签的底部。如果它们不可见,则会像这样从视图中删除

this.bodyText.RemoveFromSuperview();

如果您想知道这些单元将如何与 iOS 的 Cell Reuse 一起工作,我将在接下来介绍。

如果标签将成为列中的最后一个标签(这取决于数据),我将应用从 SwiftArcthiect 的答案中学习的其他关键

让[列]通过添加一个单列来计算它们的理想高度 到最低标签底部的高度约束(leftColumn.bottom 等于lowestLeftLabel.bottom)

处理细胞重用

有了如此复杂的约束集和许多可选单元格,我不想每次使用可能不同的可选标签重用单元格时都必须重新应用约束。为此,我在运行时构建和设置重用标识符。

TableSource 继承自 MvxTableViewSource。在覆盖的 GetOrCreateCellFor 我检查特定的重用标识符(正常使用),如果是 调用 DequeueReusableCell 但是在这种情况下,我遵循封装在自定义 Cell 类中的例程,该例程知道如何构建特定于数据的 id

protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
        
            MvxTableViewCell cell;

            if (this.reuseIdentifier != null)
            
                cell = (MvxTableViewCell)tableView.DequeueReusableCell(this.reuseIdentifier);    
            
            else
            
                // No single reuse identifier, defer to the cell for the identifer
                string identifier = this.itemCell.GetCellIdentifier(item);

                if (this.reuseIdentifiers.Contains(identifier) == false)
                
                    tableView.RegisterClassForCellReuse(this.tableCellType, identifier);
                    this.reuseIdentifiers.Add(identifier);
                

                cell = (MvxTableViewCell)tableView.DequeueReusableCell(identifier);    
            

            return cell;
        

以及构建 id 的调用

public string GetCellIdentifier(object item)
        
            StringBuilder cellIdentifier = new StringBuilder();

            var entry = item as EntryItemViewModelBase;

            cellIdentifier.AppendFormat("notes0", entry.Notes.HasValue() ? "yes" : "no");
            cellIdentifier.AppendFormat("_body0", !entry.Body.Any() ? "no" : "yes");
            cellIdentifier.AppendFormat("_priority0", entry.Priority.HasValue() ? "yes" : "no");
            cellIdentifier.AppendFormat("_prop10", entry.Prop1.HasValue() ? "yes" : "no");
            cellIdentifier.AppendFormat("_prop20", entry.Prop2.HasValue() ? "yes" : "no");
            cellIdentifier.AppendFormat("_warningq0", !entry.IsWarningQualifier ? "no" : "yes");
            cellIdentifier.Append("_MEIC");

            return cellIdentifier.ToString();
        

【讨论】:

【参考方案3】:

首先,我们不应该玩弄内容拥抱或压缩优先级,除非有一种情况需要您在没有选择的情况下进行更改。如果自动布局配置正确,苹果给出的默认 250:750 比例将适合 90% 的场景。 只有在引擎根据满足的约束调整视图大小发生冲突的极少数情况下,我们才应该更改拥抱/压缩优先级。

您最初的问题是您的标签被压扁了。 默认情况下,标签启用系统的固有内容大小,引擎将根据标签内容和文本大小默认决定标签的宽度和高度。 因此,如果您决定标签不扩展视图,那么您应该设置从标签右侧到视图的尾随约束。一般来说,它将被设置为“Equals”属性,这不符合我们的要求,因为我们的标签依赖于内在属性,我们不应该为标签提供标准宽度。因此,它应该是尾随属性的“大于或等于”属性,而不是“等于”。

在您的场景中,您不应修复标签的高度限制,并为任何所需的两行标签启用自动换行功能以及“行数”属性。

您知道您应该始终需要在右侧视图中显示“黄色标签”、“绿色标签”和“紫色标签”,而不管左侧视图中的“红色标签”中是否有一行或两行。

所以为单元格固定一个静态高度。 在右侧视图中, 修复“橙色”标签的顶部约束和“黄色”标签的底部约束。因此,中心“红色”标签将获得一个明确的高度,可以根据要求容纳一/两行。并为右侧视图提供足够的约束以满足您的要求。

如果这不能解决您的问题或对我的解决方案的任何讨论,请在下方评论。

【讨论】:

以上是关于使用 AutoLayout 在不同高度的两列内堆叠的主要内容,如果未能解决你的问题,请参考以下文章

两列布局,无需JavaScript即可重新排序项目

带有标题的两列 UICollectionView

使用引导程序使每列内的元素具有相同的高度

Bigquery - 两列内多个值的总和乘积

iOS Autolayout:处理不同的屏幕高度

堆叠不同高度的div