【问题标题】:Self-sizing cells and dynamic size controls for iOSiOS 的自定大小单元格和动态大小控件
【发布时间】:2016-06-13 06:28:20
【问题描述】:

问题定义

我正在尝试构建一个行为类似于UILabel 的自定义控件。我应该能够将这样的控件放置在自调整大小的表格单元格中,它应该:

  • 包装它的内容(就像 UILabel 和 numberOfLines=0 一样)
  • 自动扩展自定尺寸的单元格高度尺寸
  • 处理设备旋转
  • 不需要 UITableCellView 或 ViewControll 中的任何特殊代码来实现此功能(UILabel 不需要任何特殊代码)。

研究

我做的第一件事很简单。我决定观察 UILabel 是如何工作的。我做了以下操作:

  • 创建了一个包含自行调整大小的单元格的表格
  • 创建了一个自定义单元格,放入 UILabel(其中有 numberOfLines=0)
  • 创建约束以确保 UILabel 占据整个单元格
  • 继承 UILabel 并覆盖一堆方法以查看其行为方式

我检查了以下内容

  • 纵向运行(标签在多行上正确显示)并且单元格高度正确
  • 旋转它。表格的宽度和高度已更新,它们也是正确的。

我观察到它表现良好。它不需要任何特殊代码,我看到了系统渲染它的(一些)调用的顺序。

部分解决方案

@Wingzero 在下面写了一个部分解决方案。它创建正确大小的单元格。

但是,他的解决方案有两个问题:

  • 它使用“self.superview.bounds.size.width”。如果您的控件占据整个单元格,则可以使用此选项。但是,如果单元格中还有其他使用约束的内容,则此类代码将无法正确计算宽度。

  • 它根本不处理旋转。我很确定它不会处理其他调整大小事件(有一堆不太常见的调整大小事件 - 例如状态栏在电话中变大等)。

你知道如何解决这个案例的这些问题吗? 我发现了一堆关于构建更多静态自定义控件和在自调整单元格中使用预构建控件的文章。

但是,我还没有找到任何可以解决这两个问题的解决方案。

【问题讨论】:

  • 不使用自动布局? :)
  • @Wingzero。我想写一个像 UILabel 一样的组件。它可以放置在自调整大小的单元格中并正确包装其内容(同时扩展此单元格大小的单元格的高度)。这差不多。顺便提一句。 UILabel intristicContentSize 并不完全与框架无关。它使用preferredLayoutWidth(几乎是frame.width)来确定它应该如何布局。
  • @VictorRonin,是的,我认为你的方法是正确的,你说你在计算 intristicContentSize 时遇到了麻烦,不知道如何获得宽度?那么你的 UIView 里面是什么?,当你覆盖固有尺寸时,你应该已经知道宽度了。
  • @Wingzero。您是指 UIView 的宽度还是我的内容。如果您指的是 UIView 的宽度,那是我描述的 #2。调用intrinsicContentSize 时,框架将是错误的(IB 构建器值与实际运行时值)。如果您指的是我的内容的宽度,那么是的,我知道。但是,与文本相同,它可以表示为 900 pt 用于 1 行 ext 或 300 pt 用于 3 行文本。我需要返回多行以让 iOS 制作一个正确高度的单元格。这需要我知道 UIView / 单元格宽度。
  • @VictorRonin 没错,它涉及更多工作。但是您是否尝试过使用 CGRectDivide/CGRectInset 或它们的 Swift 等价物进行布局?这比处理边/pos(x,y) 的工作量要少得多。

标签: ios uitableview uiview


【解决方案1】:

我必须使用答案部分来发表我的想法并继续前进,尽管这可能不是你的答案,因为我不完全理解是什么阻碍了你,因为我认为你已经知道内在大小,仅此而已。

基于 cmets,我尝试使用文本属性创建视图并覆盖内在:

头文件,后来发现maxPreferredWidth没有完全使用,所以忽略它:

@interface LabelView : UIView

IB_DESIGNABLE
@property (nonatomic, copy) IBInspectable NSString *text;
@property (nonatomic, assign) IBInspectable CGFloat maxPreferredWidth;

@end

.m 文件:

#import "LabelView.h"

@implementation LabelView

-(void)setText:(NSString *)text {
    if (![_text isEqualToString:text]) {
        _text = text;
        [self invalidateIntrinsicContentSize];
    }
}

-(CGSize)intrinsicContentSize {
    CGRect boundingRect = [self.text boundingRectWithSize:CGSizeMake(self.superview.bounds.size.width, CGFLOAT_MAX)
                                           options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading
                                        attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:16]}
                                           context:nil];
    return boundingRect.size;
}

@end

还有一个带有 xib 的 UITableViewCell:

头文件:

@interface LabelCell : UITableViewCell

@property (weak, nonatomic) IBOutlet LabelView *labelView;

@end

.m 文件:

@implementation LabelCell

- (void)awakeFromNib {
    [super awakeFromNib];
}

@end

xib,很简单,就是顶部、底部、前导、尾随约束:

所以运行它,根据文本的边界矩形,单元格的高度是不同的,在我的例子中,我有两个文本要循环:1.“哈哈”,2.“asdf”{重复多次以创建一个长字符串}

所以奇数单元格的高度为 19,偶数单元格的高度为 58:

这就是你要找的东西吗?

我的想法:

UITableView 的单元格的宽度总是和表格视图一样,所以这就是宽度。 UICollectionView 可能存在更多问题,但关键是我们将计算它并返回它就足够了。

演示项目:https://github.com/liuxuan30/StackOverflow-DynamicSize
(我根据我的旧项目进行了更改,其中有一些图像,忽略那些。)

【讨论】:

  • 我需要仔细研究一下。两个 cmets:有趣的是“self.superview.bounds.size.width”对你有用。我的印象是直到游戏后期才确定单元大小。一方面,这可能是一个“足够好”的捷径。另一方面,对于 UILabel,您可以设置约束(因此它将仅占据单元格的一部分)。此解决方案将无法处理它,因为它仅使用超级视图(单元格)宽度。
  • @VictorRonin这个工具叫Reveal,不是设计工具而是ui调试工具,买了绝对不会后悔。
  • @VictorRonin 单元格会像我记得的那样布局好几次。对于第二个,你的意思是它的一部分?如果它不能占据单元格的全部高度,则内容拥抱/阻力可能会有所帮助
  • @VictorRonin 你解决了吗?
  • 考虑到你是唯一一个解决这个问题的人,我会奖励你。但是,仍然没有答案的问题是如何根据单元格中可能存在的各种约束(如果单元格中还有其他控件)获得控件宽度
【解决方案2】:

这是一个满足您的要求的解决方案,它也是 IBDesignable,因此它可以在 Interface Builder 中实时预览。此类将布置一系列正方形(总数等于 IBInspectable count 属性)。默认情况下,它只会将它们全部排成一排。但是,如果您将wrap IBInspectable 属性设置为On,它将包裹正方形并根据其约束宽度增加其高度(如带有numberOfLines == 0 的UILabel)。在自调整大小的表格视图单元格中,这将具有推出顶部和底部以适应自定义视图的包装固有尺寸的效果。

代码:

import Foundation
import UIKit


@IBDesignable class WrappingView : UIView {

    private class InnerWrappingView : UIView {

        private var lastPoint:CGPoint = CGPointZero
        private var wrap = false
        private var count:Int = 100
        private var size:Int = 8
        private var spacing:Int = 3

        private func calculatedSize() -> CGSize {
            lastPoint = CGPoint(x:-(size + spacing), y: 0)
            for _ in 0..<count {
                var nextPoint:CGPoint!
                if wrap {
                    nextPoint = lastPoint.x + CGFloat(size + spacing + size) <= bounds.width ? CGPoint(x: lastPoint.x + CGFloat(size + spacing), y: lastPoint.y) : CGPoint(x: 0, y: lastPoint.y + CGFloat(size + spacing))
                } else {
                    nextPoint = CGPoint(x: lastPoint.x + CGFloat(size + spacing), y: lastPoint.y)
                }
                lastPoint = nextPoint
            }
            return CGSize(width: wrap ? bounds.width : lastPoint.x + CGFloat(size), height: lastPoint.y + CGFloat(size))
        }

        override func layoutSubviews() {
            super.layoutSubviews()
            guard bounds.size != calculatedSize() || subviews.count == 0 else {
                return
            }
            for subview in subviews {
                subview.removeFromSuperview()
            }
            lastPoint = CGPoint(x:-(size + spacing), y: 0)
            for _ in 0..<count {
                let square = createSquareView()
                var nextPoint:CGPoint!
                if wrap {
                    nextPoint = lastPoint.x + CGFloat(size + spacing + size) <= bounds.width ? CGPoint(x: lastPoint.x + CGFloat(size + spacing), y: lastPoint.y) : CGPoint(x: 0, y: lastPoint.y + CGFloat(size + spacing))
                } else {
                    nextPoint = CGPoint(x: lastPoint.x + CGFloat(size + spacing), y: lastPoint.y)
                }
                square.frame = CGRect(origin: nextPoint, size: square.bounds.size)
                addSubview(square)
                lastPoint = nextPoint
            }
            let newframe = CGRect(origin: frame.origin, size: calculatedSize())
            frame = newframe
            invalidateIntrinsicContentSize()
            setNeedsLayout()
        }


        private func createSquareView() -> UIView {
            let square = UIView(frame: CGRect(x: 0, y: 0, width: size, height: size))
            square.backgroundColor = UIColor.blueColor()
            return square
        }

        override func intrinsicContentSize() -> CGSize {
            return calculatedSize()
        }
    }

    @IBInspectable var count:Int = 500 {
        didSet {
            innerView.count = count
            layoutSubviews()
        }
    }

    @IBInspectable var size:Int = 8 {
        didSet {
            innerView.size = size
            layoutSubviews()
        }
    }

    @IBInspectable var spacing:Int = 3 {
        didSet {
            innerView.spacing = spacing
            layoutSubviews()
        }
    }

    @IBInspectable var wrap:Bool = false {
        didSet {
            innerView.wrap = wrap
            layoutSubviews()
        }
    }

    private var _innerView:InnerWrappingView! {
        didSet {
            clipsToBounds = true
            addSubview(_innerView)
            _innerView.clipsToBounds = true
            _innerView.frame = bounds
            _innerView.wrap = wrap
            _innerView.translatesAutoresizingMaskIntoConstraints = false
            _innerView.leftAnchor.constraintEqualToAnchor(leftAnchor).active = true
            _innerView.rightAnchor.constraintEqualToAnchor(rightAnchor).active = true
            _innerView.topAnchor.constraintEqualToAnchor(topAnchor).active = true
            _innerView.bottomAnchor.constraintEqualToAnchor(bottomAnchor).active = true
            _innerView.setContentCompressionResistancePriority(750, forAxis: .Vertical)
            _innerView.setContentHuggingPriority(251, forAxis: .Vertical)
        }
    }

    private var innerView:InnerWrappingView! {
        if _innerView == nil {
            _innerView = InnerWrappingView()
        }
        return _innerView
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        if innerView.bounds.width != bounds.width {
            innerView.frame = CGRect(origin: CGPointZero, size: CGSize(width: bounds.width, height: 0))
        }
        innerView.layoutSubviews()
        if innerView.bounds.height != bounds.height {
            invalidateIntrinsicContentSize()
            superview?.layoutIfNeeded()
        }
    }

    override func intrinsicContentSize() -> CGSize {
        return innerView.calculatedSize()
    }
}

在我的示例应用程序中,我将表格视图设置为将包含此自定义视图的每行的单元格出列,并将自定义视图的 count 属性设置为 20 * indexPath 的行。自定义视图被限制为单元格宽度的 50%,因此在横向和纵向之间移动时其宽度会自动更改。因为每个连续的表格单元格都包裹着越来越长的方格串,所以每个单元格的大小都会自动变得越来越高。

运行时是这样的(包括旋转演示):

【讨论】:

  • 丹尼尔。这真太了不起了。非常感谢。我一直在寻找这个。让我为你奖励赏金。如果您不介意,您可以做两件事:通过代码添加一些 cmets(我将能够从一个工作示例中弄清楚,但它会帮助其他将要研究它的人)。此外,还有一个小错误。第一次启动时,每个单元格显示错误的方格数(只有在旋转后才开始显示正确的方格)。
  • 顺便说一句。让它可设计真的很不错:)
  • 好棒的,虽然有点复杂,但我觉得核心部分还是IntrinsicContentSize()的概念
【解决方案3】:

以@Wingzero 的另一个答案为基础,布局是一个复杂的问题...

提到的maxPreferredWidth 很重要,并且与UILabelpreferredMaxLayoutWidth 相关。该属性的重点是告诉标签不要只是一条长线,而是在宽度达到该值时更愿意换行。因此,在计算内在尺寸时,您将使用 preferredMaxLayoutWidth(如果设置)或视图宽度的最小值作为最大宽度。

另一个关键方面是invalidateIntrinsicContentSize,每当发生变化并意味着需要新的布局时,视图应该调用它自己。

UILabel 不处理旋转 - 它不知道。视图控制器负责检测和处理旋转,通常是在触发新的布局运行之前使布局无效并更新视图大小。标签(和其他视图)只是用来处理生成的布局。作为旋转的一部分,您(即视图控制器)可以更改 preferredMaxLayoutWidth,例如在横向布局中允许更大的宽度是有意义的。

【讨论】:

  • 这里有多个问题。应该设置一些preferredMaxLayoutWidth。我可以在我的课堂上创建这样的属性。但是,不清楚何时需要设置它(特别是要确保它与其他约束计算的大小相匹配)。我同意 UILabel 不直接订阅设备方向更改。但是,它会覆盖一些 UIView 方法以查看布局已更改。你可以在 UITableView 中放一个标签,你会注意到你不需要任何额外的代码来处理旋转。
  • @Wingzero 自定义控件不覆盖这些方法,因此看不到布局已更改(由于设备方向更改)
  • 如果您使用的是表格视图控制器,则仅使用零个附加代码处理旋转 - 因为它包含调整表格大小并运行新布局(或具体来说,表格高度计算)的代码。 .. 设计单元格内容时需要设置preferredMaxLayoutWidth
  • 我相信 UITableView (vs UITableViewController) 也可以处理旋转。意味着它运行新的布局。所以,如果你有通用的 UIViewController。将 UITableView 放入其中并放入带有 UILabel 的单元格,然后它也可以在没有任何特殊代码的情况下工作。
  • 当它的框架发生变化时,表格视图本身可能会使其布局无效
【解决方案4】:

您是否正在寻找这样的东西^^?单元格具有动态高度以方便 UILabel 的内容,并且没有任何代码来计算大小/宽度/高度 - 只是一些限制。

本质上,左侧的标签具有单元格的顶部、底部和前导边距,右侧标签具有单元格的尾随边距。只需要一个标签?然后忽略右侧标签,并将左侧标签配置为单元格的尾随约束。

如果您需要多行标签,请进行配置。将 numberOfLines 设置为 2、3 或 0,由您决定。

您不需要计算表格视图单元格的高度,自动布局会为您计算;但是你需要让它知道,通过告诉它使用自动尺寸:self.tableView.rowHeight = UITableViewAutomaticDimension,或者在tableView:heightForRowAtIndexPath: 中返回它。您还可以在tableView:estimatedHeightForRowAtIndexPath: 中告诉表格视图“粗略”估计以获得更好的性能。

还是不行?将相关 UILabel 的 Content Compression Resistance Priority - Vertical 设置为 1000 / 必需,以便标签的内容会尽力“抵抗压缩”,并且 numberOfLines 配置将被完全确认。

它会旋转吗?尝试观察旋转(有方向更改通知),然后更新布局(setNeedsLayout)。

还是不行?更多阅读:Using Auto Layout in UITableView for dynamic cell layouts & variable row heights

【讨论】:

  • 您正在谈论制作一个自定大小的单元格以及如何在表格中使用它们。我正在寻找一种方法来创建具有动态高度的 自定义组件,该高度可以在自调整大小的单元格中正常工作。
  • @VictorRonin 所以你是说,单元格已经以某种方式具有自我调整大小的逻辑,并且你想在单元格中拥有一个大小正确的组件?如果是这种情况,您仍然可以有一个 UIView 与单元格的顶部/底部/前导/尾随约束,以便相应地调整组件的大小。抱歉,如果这不是您想要的。
  • 我认为您需要仔细阅读我的问题并检查其他答案。单元格已经自行调整大小(您可以在此处阅读有关它们的信息-developer.apple.com/library/ios/documentation/UserExperience/… 调整单元格会询问其中的视图它们应该是什么大小(这与通过约束时单元格将强制视图时的常见布局非常不同)是某个特定的大小)。因此,这是一个自定义控制作业来确定它需要的大小。
  • 你说的绝对是真的,我也是这么说的——单元格内的组件应该告诉单元格它应该是什么大小,并且组件根据1)计算它们的内在内容来做到这一点大小,或 2) 通过在它们之间和对单元格设置约束。这里的其他答案是试图计算内在大小,但我认为这不一定是这样做的方式。如果您在一个单元格中有 5 个视图,并且您正确配置了约束,则单元格将遵守约束并动态调整自身大小(尤其是其高度)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-08
  • 1970-01-01
  • 2014-05-06
  • 2015-03-20
  • 1970-01-01
  • 2016-02-01
相关资源
最近更新 更多