Estimate height when resolving status collapse state

This commit is contained in:
Shadowfacts 2023-05-13 15:00:03 -04:00
parent 735659dee6
commit 8c27a9368f
11 changed files with 93 additions and 29 deletions

View File

@ -13,6 +13,7 @@ protocol Configurable {
func configure(_ closure: (T) -> Void) -> T func configure(_ closure: (T) -> Void) -> T
} }
extension Configurable where Self: UIView { extension Configurable where Self: UIView {
@inline(__always)
func configure(_ closure: (Self) -> Void) -> Self { func configure(_ closure: (Self) -> Void) -> Self {
closure(self) closure(self)
return self return self

View File

@ -117,8 +117,8 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
} }
_ = state.resolveFor(status: edit, height: { _ = state.resolveFor(status: edit, height: {
layoutIfNeeded() let width = self.bounds.width - 2*16
return contentContainer.visibleSubviewHeight return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: width)
}) })
collapseButton.isHidden = !state.collapsible! collapseButton.isHidden = !state.collapsible!
contentContainer.setCollapsed(state.collapsed!) contentContainer.setCollapsed(state.collapsed!)

View File

@ -9,7 +9,9 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
class StatusEditPollView: UIStackView { class StatusEditPollView: UIStackView, StatusContentPollView {
private var titleLabels: [EmojiLabel] = []
init() { init() {
super.init(frame: .zero) super.init(frame: .zero)
@ -25,6 +27,7 @@ class StatusEditPollView: UIStackView {
func updateUI(poll: StatusEdit.Poll?, emojis: [Emoji]) { func updateUI(poll: StatusEdit.Poll?, emojis: [Emoji]) {
arrangedSubviews.forEach { $0.removeFromSuperview() } arrangedSubviews.forEach { $0.removeFromSuperview() }
titleLabels = []
for option in poll?.options ?? [] { for option in poll?.options ?? [] {
// the edit poll doesn't actually include the multiple value // the edit poll doesn't actually include the multiple value
@ -33,6 +36,7 @@ class StatusEditPollView: UIStackView {
let label = EmojiLabel() let label = EmojiLabel()
label.text = option.title label.text = option.title
label.setEmojis(emojis, identifier: Optional<String>.none) label.setEmojis(emojis, identifier: Optional<String>.none)
titleLabels.append(label)
let stack = UIStackView(arrangedSubviews: [ let stack = UIStackView(arrangedSubviews: [
icon, icon,
label, label,
@ -44,4 +48,14 @@ class StatusEditPollView: UIStackView {
} }
} }
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
var height: CGFloat = 0
height += CGFloat(arrangedSubviews.count - 1) * 4
let labelWidth = effectiveWidth /* checkbox size: */ - 20 /* spacing: */ - 8
for titleLabel in titleLabels {
height += titleLabel.sizeThatFits(CGSize(width: labelWidth, height: UIView.layoutFittingCompressedSize.height)).height
}
return height
}
} }

View File

@ -22,6 +22,15 @@ class AttachmentsContainerView: UIView {
let attachmentStacks: NSHashTable<UIStackView> = .weakObjects() let attachmentStacks: NSHashTable<UIStackView> = .weakObjects()
var moreView: UIView? var moreView: UIView?
private var aspectRatioConstraint: NSLayoutConstraint? private var aspectRatioConstraint: NSLayoutConstraint?
private(set) var aspectRatio: CGFloat = 16/9 {
didSet {
if aspectRatio != aspectRatioConstraint?.multiplier {
aspectRatioConstraint?.isActive = false
aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: aspectRatio)
aspectRatioConstraint!.isActive = true
}
}
}
var blurView: UIVisualEffectView? var blurView: UIVisualEffectView?
var hideButtonView: UIVisualEffectView? var hideButtonView: UIVisualEffectView?
@ -93,7 +102,8 @@ class AttachmentsContainerView: UIView {
fillView(attachmentView) fillView(attachmentView)
sendSubviewToBack(attachmentView) sendSubviewToBack(attachmentView)
accessibilityElements.append(attachmentView) accessibilityElements.append(attachmentView)
if let attachmentAspectRatio = attachmentView.attachmentAspectRatio { if Preferences.shared.showUncroppedMediaInline,
let attachmentAspectRatio = attachmentView.attachmentAspectRatio {
aspectRatio = attachmentAspectRatio aspectRatio = attachmentAspectRatio
} }
case 2: case 2:
@ -266,18 +276,7 @@ class AttachmentsContainerView: UIView {
accessibilityElements.append(moreView) accessibilityElements.append(moreView)
} }
if Preferences.shared.showUncroppedMediaInline { self.aspectRatio = aspectRatio
if aspectRatioConstraint?.multiplier != aspectRatio {
aspectRatioConstraint?.isActive = false
aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: aspectRatio)
aspectRatioConstraint!.isActive = true
}
} else {
if aspectRatioConstraint == nil {
aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: 16/9)
aspectRatioConstraint!.isActive = true
}
}
} else { } else {
self.isHidden = true self.isHidden = true
} }

View File

@ -11,18 +11,18 @@ import Pachyderm
class PollOptionView: UIView { class PollOptionView: UIView {
private let unselectedBackgroundColor = UIColor(white: 0.5, alpha: 0.25) private static let unselectedBackgroundColor = UIColor(white: 0.5, alpha: 0.25)
private(set) var label: EmojiLabel!
private(set) var checkbox: PollOptionCheckboxView? private(set) var checkbox: PollOptionCheckboxView?
init(poll: Poll, option: Poll.Option, mastodonController: MastodonController) { init(poll: Poll, option: Poll.Option, mastodonController: MastodonController) {
super.init(frame: .zero) super.init(frame: .zero)
let minHeight: CGFloat = 35 let minHeight: CGFloat = 35
layer.cornerRadius = 0.1 * minHeight layer.cornerRadius = 0.1 * minHeight
layer.cornerCurve = .continuous layer.cornerCurve = .continuous
backgroundColor = unselectedBackgroundColor backgroundColor = PollOptionView.unselectedBackgroundColor
let showCheckbox = poll.ownVotes?.isEmpty == false || !poll.effectiveExpired let showCheckbox = poll.ownVotes?.isEmpty == false || !poll.effectiveExpired
if showCheckbox { if showCheckbox {
@ -31,7 +31,7 @@ class PollOptionView: UIView {
addSubview(checkbox!) addSubview(checkbox!)
} }
let label = EmojiLabel() label = EmojiLabel()
label.translatesAutoresizingMaskIntoConstraints = false label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0 label.numberOfLines = 0
label.font = .preferredFont(forTextStyle: .callout) label.font = .preferredFont(forTextStyle: .callout)
@ -91,7 +91,6 @@ class PollOptionView: UIView {
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
minHeightConstraint, minHeightConstraint,
label.topAnchor.constraint(equalTo: topAnchor, constant: 4), label.topAnchor.constraint(equalTo: topAnchor, constant: 4),
label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4), label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
label.trailingAnchor.constraint(equalTo: percentLabel.leadingAnchor, constant: -4), label.trailingAnchor.constraint(equalTo: percentLabel.leadingAnchor, constant: -4),

View File

@ -77,6 +77,16 @@ class PollOptionsView: UIControl {
accessibilityElements = options accessibilityElements = options
} }
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
var height: CGFloat = 0
height += CGFloat(options.count - 1) * stack.spacing
for option in options {
// this isn't the actual width, but it's close enough for the estimate
height += option.label.sizeThatFits(CGSize(width: effectiveWidth, height: UIView.layoutFittingCompressedSize.height)).height
}
return height
}
private func selectOption(_ option: PollOptionView) { private func selectOption(_ option: PollOptionView) {
if poll.multiple { if poll.multiple {
option.checkbox?.isChecked.toggle() option.checkbox?.isChecked.toggle()

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
class StatusPollView: UIView { class StatusPollView: UIView, StatusContentPollView {
private static let formatter: DateComponentsFormatter = { private static let formatter: DateComponentsFormatter = {
let f = DateComponentsFormatter() let f = DateComponentsFormatter()
@ -140,6 +140,11 @@ class StatusPollView: UIView {
voteButton.isEnabled = false voteButton.isEnabled = false
} }
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
guard let poll else { return 0 }
return optionsView.estimateHeight(effectiveWidth: effectiveWidth) + infoLabel.sizeThatFits(UIView.layoutFittingExpandedSize).height
}
private func checkedOptionsChanged() { private func checkedOptionsChanged() {
voteButton.isEnabled = optionsView.checkedOptionIndices.count > 0 voteButton.isEnabled = optionsView.checkedOptionIndices.count > 0
} }

View File

@ -398,6 +398,11 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
reblogsCountButton.configuration = reblogsConfig reblogsCountButton.configuration = reblogsConfig
} }
func estimateContentHeight() -> CGFloat {
let width = bounds.width - 2*16
return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: width)
}
func updateUIForPreferences(status: StatusMO) { func updateUIForPreferences(status: StatusMO) {
baseUpdateUIForPreferences(status: status) baseUpdateUIForPreferences(status: status)
} }

View File

@ -46,6 +46,7 @@ protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate
func updateUIForPreferences(status: StatusMO) func updateUIForPreferences(status: StatusMO)
func updateStatusState(status: StatusMO) func updateStatusState(status: StatusMO)
func estimateContentHeight() -> CGFloat
} }
// MARK: UI Configuration // MARK: UI Configuration
@ -117,11 +118,13 @@ extension StatusCollectionViewCell {
replyButton.isEnabled = mastodonController.loggedIn replyButton.isEnabled = mastodonController.loggedIn
favoriteButton.isEnabled = mastodonController.loggedIn favoriteButton.isEnabled = mastodonController.loggedIn
let didResolve = statusState.resolveFor(status: status) { let didResolve = statusState.resolveFor(status: status, height: self.estimateContentHeight)
// layout so that we can take the content height into consideration when deciding whether to collapse // let didResolve = statusState.resolveFor(status: status) {
layoutIfNeeded() //// // layout so that we can take the content height into consideration when deciding whether to collapse
return contentContainer.visibleSubviewHeight //// layoutIfNeeded()
} //// return contentContainer.visibleSubviewHeight
// return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: )
// }
if didResolve { if didResolve {
if statusState.collapsible! && showStatusAutomatically { if statusState.collapsible! && showStatusAutomatically {
statusState.collapsed = false statusState.collapsed = false

View File

@ -8,7 +8,11 @@
import UIKit import UIKit
class StatusContentContainer<ContentView: ContentTextView, PollView: UIView>: UIView { protocol StatusContentPollView: UIView {
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat
}
class StatusContentContainer<ContentView: ContentTextView, PollView: StatusContentPollView>: UIView {
private var useTopSpacer = false private var useTopSpacer = false
private let topSpacer = UIView().configure { private let topSpacer = UIView().configure {
@ -25,9 +29,10 @@ class StatusContentContainer<ContentView: ContentTextView, PollView: UIView>: UI
$0.isSelectable = false $0.isSelectable = false
} }
private static var cardViewHeight: CGFloat { 90 }
let cardView = StatusCardView().configure { let cardView = StatusCardView().configure {
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
$0.heightAnchor.constraint(equalToConstant: 90), $0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight),
]) ])
} }
@ -132,4 +137,22 @@ class StatusContentContainer<ContentView: ContentTextView, PollView: UIView>: UI
zeroHeightConstraint.isActive = collapsed zeroHeightConstraint.isActive = collapsed
} }
// used only for collapsing automatically based on height, doesn't need to be accurate
// just roughly inline with the content height
func estimateVisibleSubviewHeight(effectiveWidth: CGFloat) -> CGFloat {
var height: CGFloat = 0
height += contentTextView.sizeThatFits(CGSize(width: effectiveWidth, height: UIView.layoutFittingCompressedSize.height)).height
if !cardView.isHidden {
height += StatusContentContainer.cardViewHeight
}
if !attachmentsView.isHidden {
height += effectiveWidth / attachmentsView.aspectRatio
}
if !pollView.isHidden {
let pollHeight = pollView.estimateHeight(effectiveWidth: effectiveWidth)
height += pollHeight
}
return height
}
} }

View File

@ -629,6 +629,11 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
baseUpdateStatusState(status: status) baseUpdateStatusState(status: status)
} }
func estimateContentHeight() -> CGFloat {
let width = bounds.width /* leading spacing: */ - 16 /* avatar: */ - 50 /* spacing: 8 */ - 8 /* trailing spacing: */ - 16
return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: width)
}
private func updateTimestamp() { private func updateTimestamp() {
guard let mastodonController, guard let mastodonController,
let status = mastodonController.persistentContainer.status(for: statusID) else { let status = mastodonController.persistentContainer.status(for: statusID) else {