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

Using AutoLayout to stack within two Columns of varying Heights

目标 iOS 8.1

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

第二次更新

没有 below I would not have solved this and have accepted his answer. However because mine is all in code, in a custom tablecell, I have also added a

更新

为了阻止标签拉伸到比所需尺寸更大的尺寸,我之前将 SetContentHuggingPrioritySetContentCompressionResistancePriority 设置为 1000,因为我相信这相当于说“我希望 Label 将其内容紧贴到它的确切高度,我不希望它被垂直压缩” 正如您在下面的红色和粉色示例中所见,AutoLayout 显然没有遵守此请求。

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

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

  1. 为什么删除 Hugging 和 Compression 优先级会修复我的问题 问题?
  2. 如何在不返回上一期的情况下让红框(红框不是后来添加的单元格的一部分)中的文本不展开?

这里有几个屏幕截图,展示了设置压缩和拥抱优先级后的效果。背景颜色用于调试

普遍的问题是包含视图(紫色和红色)将自身调整为两者中较小的一个。正如您在顶部看到的那样,"Priority 3" 正在被剪切,因为左侧的列容器不需要更高。

在下一个示例中,没有优先级标签,但 EventDate 被压缩了。

首先,我们不应该玩弄内容-拥抱或压缩优先级,除非在没有其他选择的情况下需要更改。苹果默认的250:750比例,如果自动布局配置得当,可以满足90%的场景。 只有在引擎根据满足的约束调整视图大小存在冲突的极少数情况下,我们才应该更改 hugging/compression 优先级。

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

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

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

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

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

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

初步建议

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

解决方案说明

为简单起见,我创建了 2 个子视图,每列 1 个,并将它们并排放置。它们锚定在 top/left 和 top/right 上,它们的宽度是计算出来的,它们的高度是从它们各自的内容中得出的(*)。

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

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

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

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

所有约束列表

注意到处都是 >=。它们是让您的专栏独立高大的关键。

NSLayoutContraint 对于 LR 实际上是相同的。

获取故事板here, and a detailed article there

我已经接受 但是看到我正在寻找基于代码的 TableCell 方法,我将添加一个单独的答案。没有他的帮助我 不可能走到这一步。

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

创建子视图

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

关闭 AutoResizingMasks

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

相关标签作为子视图添加到 leftColumnContainerrightColumnContainer UIView。然后将这两个容器作为子视图添加到 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 的调用将左列的右边缘和右列的左边缘并排放置

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

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 以查看它们是否可见。如果它们是可见的,则对左、上和右应用类似的约束,顶部被约束到上面标签的底部。如果它们不可见,则会像这样从视图中移除

this.bodyText.RemoveFromSuperview();

如果您想知道这些单元将如何与 iOSs 单元重用一起工作,我将在接下来介绍。

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

Let [the columns] calculate their ideal height by adding a single height constraint to the bottom of the lowest label (leftColumn.bottom Equal lowestLeftLabel.bottom)

处理单元重用

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

TableSource 继承自 MvxTableViewSource。在覆盖的 GetOrCreateCellFor 中,我检查特定的 reuseIdentifier(正常使用),如果是的话 调用 DequeueReusableCell 但是在这种情况下,我遵循封装在自定义 Cell class 中的例程,该例程知道如何构建特定于数据的 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("notes{0}", entry.Notes.HasValue() ? "yes" : "no");
            cellIdentifier.AppendFormat("_body{0}", !entry.Body.Any() ? "no" : "yes");
            cellIdentifier.AppendFormat("_priority{0}", entry.Priority.HasValue() ? "yes" : "no");
            cellIdentifier.AppendFormat("_prop1{0}", entry.Prop1.HasValue() ? "yes" : "no");
            cellIdentifier.AppendFormat("_prop2{0}", entry.Prop2.HasValue() ? "yes" : "no");
            cellIdentifier.AppendFormat("_warningq{0}", !entry.IsWarningQualifier ? "no" : "yes");
            cellIdentifier.Append("_MEIC");

            return cellIdentifier.ToString();
        }