Estimate status cell heights

This commit is contained in:
Justin Mazzocchi 2021-01-18 16:46:38 -08:00
parent b164a6265e
commit 083b52c802
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
10 changed files with 209 additions and 5 deletions

View file

@ -25,4 +25,17 @@ extension CollectionItem {
return ConversationListCell.self
}
}
func estimatedHeight(width: CGFloat, identification: Identification) -> CGFloat {
switch self {
case let .status(status, configuration):
return StatusView.estimatedHeight(
width: width,
identification: identification,
status: status,
configuration: configuration)
default:
return UITableView.automaticDimension
}
}
}

View file

@ -4,6 +4,15 @@ import Mastodon
import UIKit
extension String {
func height(width: CGFloat, font: UIFont) -> CGFloat {
(self as NSString).boundingRect(
with: CGSize(width: width, height: .greatestFiniteMagnitude),
options: .usesLineFragmentOrigin,
attributes: [.font: font],
context: nil)
.height
}
func countEmphasizedAttributedString(count: Int, highlighted: Bool = false) -> NSAttributedString {
let countRange = (self as NSString).range(of: String.localizedStringWithFormat("%ld", count))

View file

@ -110,7 +110,9 @@ class TableViewController: UITableViewController {
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return UITableView.automaticDimension }
return cellHeightCaches[tableView.frame.width]?[item] ?? UITableView.automaticDimension
return cellHeightCaches[tableView.frame.width]?[item]
?? item.estimatedHeight(width: tableView.readableContentGuide.layoutFrame.width,
identification: identification)
}
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Combine
import Mastodon
import UIKit
import ViewModels
@ -80,6 +81,20 @@ final class AttachmentsView: UIView {
}
extension AttachmentsView {
static func estimatedHeight(width: CGFloat,
identification: Identification,
status: Status,
configuration: CollectionItem.StatusConfiguration) -> CGFloat {
let height: CGFloat
if status.displayStatus.mediaAttachments.count == 1,
let aspectRatio = status.mediaAttachments.first?.aspectRatio {
height = width / max(CGFloat(aspectRatio), 16 / 9)
} else {
height = width / (16 / 9)
}
return height
}
var shouldAutoplay: Bool {
guard !isHidden, let viewModel = viewModel, viewModel.shouldShowAttachments else { return false }

View file

@ -41,6 +41,12 @@ final class PollOptionButton: UIButton {
}
}
extension PollOptionButton {
static func estimatedHeight(width: CGFloat, title: String) -> CGFloat {
title.height(width: width, font: .preferredFont(forTextStyle: .callout))
}
}
private extension PollOptionButton {
static let titleEdgeInsets = UIEdgeInsets(top: 0, left: .compactSpacing, bottom: 0, right: .compactSpacing)
}

View file

@ -74,6 +74,14 @@ final class PollResultView: UIView {
}
}
extension PollResultView {
static func estimatedHeight(width: CGFloat, title: String) -> CGFloat {
title.height(width: width, font: .preferredFont(forTextStyle: .callout))
+ .compactSpacing
+ 4 // progress view height
}
}
private extension PollResultView {
private static var percentFormatter: NumberFormatter = {
let percentageFormatter = NumberFormatter()

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Combine
import Mastodon
import UIKit
import ViewModels
@ -112,6 +113,34 @@ final class PollView: UIView {
}
extension PollView {
static func estimatedHeight(width: CGFloat,
identification: Identification,
status: Status,
configuration: CollectionItem.StatusConfiguration) -> CGFloat {
if let poll = status.displayStatus.poll {
var height: CGFloat = 0
let open = !poll.expired && !poll.voted
for option in poll.options {
height += open ? PollOptionButton.estimatedHeight(width: width, title: option.title)
: PollResultView.estimatedHeight(width: width, title: option.title)
height += .defaultSpacing
}
if open {
height += .minimumButtonDimension + .defaultSpacing
}
height += .minimumButtonDimension / 2
return height
} else {
return 0
}
}
}
private extension PollView {
func initialSetup() {
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
@ -146,11 +175,20 @@ extension PollView {
bottomStackView.addArrangedSubview(UIView())
let voteButtonHeightConstraint = voteButton.heightAnchor.constraint(equalToConstant: .minimumButtonDimension)
let refreshButtonHeightConstraint = refreshButton.heightAnchor.constraint(
equalToConstant: .minimumButtonDimension / 2)
refreshButtonHeightConstraint.priority = .justBelowMax
refreshButtonHeightConstraint.priority = .justBelowMax
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.topAnchor.constraint(equalTo: topAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor)
stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
voteButtonHeightConstraint,
refreshButtonHeightConstraint
])
}
}

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Kingfisher
import Mastodon
import UIKit
import ViewModels
@ -44,6 +45,23 @@ final class CardView: UIView {
}
}
extension CardView {
static func estimatedHeight(width: CGFloat,
identification: Identification,
status: Status,
configuration: CollectionItem.StatusConfiguration) -> CGFloat {
if status.displayStatus.card != nil {
return round(UIFont.preferredFont(forTextStyle: .headline).lineHeight
+ UIFont.preferredFont(forTextStyle: .subheadline).lineHeight
+ UIFont.preferredFont(forTextStyle: .footnote).lineHeight
+ .defaultSpacing * 2
+ .compactSpacing * 2)
} else {
return 0
}
}
}
private extension CardView {
// swiftlint:disable:next function_body_length
func initialSetup() {

View file

@ -1,5 +1,6 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Mastodon
import UIKit
import ViewModels
@ -49,7 +50,7 @@ final class StatusBodyView: UIView {
attachmentsView.isHidden = viewModel.attachmentViewModels.isEmpty
attachmentsView.viewModel = viewModel
pollView.isHidden = viewModel.pollOptions.isEmpty
pollView.isHidden = viewModel.pollOptions.isEmpty || !viewModel.shouldShowContent
pollView.viewModel = viewModel
cardView.viewModel = viewModel.cardViewModel
@ -69,6 +70,64 @@ final class StatusBodyView: UIView {
}
}
extension StatusBodyView {
static func estimatedHeight(width: CGFloat,
identification: Identification,
status: Status,
configuration: CollectionItem.StatusConfiguration) -> CGFloat {
let contentFont = UIFont.preferredFont(forTextStyle: configuration.isContextParent ? .title3 : .callout)
var height: CGFloat = 0
var contentHeight = status.displayStatus.content.attributed.string.height(
width: width,
font: contentFont)
if status.displayStatus.card != nil {
contentHeight += .compactSpacing
contentHeight += CardView.estimatedHeight(
width: width,
identification: identification,
status: status,
configuration: configuration)
}
if status.displayStatus.poll != nil {
contentHeight += .defaultSpacing
contentHeight += PollView.estimatedHeight(
width: width,
identification: identification,
status: status,
configuration: configuration)
}
if status.displayStatus.spoilerText.isEmpty {
height += contentHeight
} else {
height += status.displayStatus.spoilerText.height(width: width, font: contentFont)
height += .compactSpacing
height += NSLocalizedString("status.show-more", comment: "").height(
width: width,
font: .preferredFont(forTextStyle: .headline))
if configuration.showContentToggled && !identification.identity.preferences.readingExpandSpoilers {
height += .compactSpacing
height += contentHeight
}
}
if !status.displayStatus.mediaAttachments.isEmpty {
height += .compactSpacing
height += AttachmentsView.estimatedHeight(
width: width,
identification: identification,
status: status,
configuration: configuration)
}
return height
}
}
extension StatusBodyView: UITextViewDelegate {
func textView(
_ textView: UITextView,

View file

@ -2,6 +2,7 @@
// swiftlint:disable file_length
import Kingfisher
import Mastodon
import UIKit
import ViewModels
@ -56,6 +57,36 @@ final class StatusView: UIView {
}
}
extension StatusView {
static func estimatedHeight(width: CGFloat,
identification: Identification,
status: Status,
configuration: CollectionItem.StatusConfiguration) -> CGFloat {
var height = CGFloat.defaultSpacing * 2
let bodyWidth = width - .defaultSpacing - .avatarDimension
if status.reblog != nil || configuration.isPinned {
height += UIFont.preferredFont(forTextStyle: .caption1).lineHeight + .compactSpacing
}
if configuration.isContextParent {
height += .avatarDimension + .minimumButtonDimension * 2.5 + .hairline * 2 + .compactSpacing * 4
} else {
height += UIFont.preferredFont(forTextStyle: .headline).lineHeight
+ .compactSpacing + .minimumButtonDimension / 2
}
height += StatusBodyView.estimatedHeight(
width: bodyWidth,
identification: identification,
status: status,
configuration: configuration)
+ .compactSpacing
return height
}
}
extension StatusView: UIContentView {
var configuration: UIContentConfiguration {
get { statusConfiguration }
@ -142,6 +173,7 @@ private extension StatusView {
mainStackView.addArrangedSubview(nameAccountContainerStackView)
mainStackView.addArrangedSubview(bodyView)
bodyView.tag = 666
contextParentTimeLabel.font = .preferredFont(forTextStyle: .footnote)
contextParentTimeLabel.adjustsFontForContentSizeCategory = true
@ -264,7 +296,10 @@ private extension StatusView {
avatarButton.leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor),
avatarButton.topAnchor.constraint(equalTo: avatarImageView.topAnchor),
avatarButton.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor),
avatarButton.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor)
avatarButton.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor),
contextParentTimeApplicationStackView.heightAnchor.constraint(
greaterThanOrEqualToConstant: .minimumButtonDimension / 2),
interactionsStackView.heightAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension)
])
}
@ -374,7 +409,8 @@ private extension StatusView {
if isContextParent {
button.heightAnchor.constraint(equalToConstant: .minimumButtonDimension).isActive = true
} else {
button.heightAnchor.constraint(greaterThanOrEqualToConstant: 0).isActive = true
button.heightAnchor.constraint(
greaterThanOrEqualToConstant: .minimumButtonDimension / 2).isActive = true
}
}