forked from shadowfacts/Tusker
Estimate height when resolving status collapse state
This commit is contained in:
parent
735659dee6
commit
8c27a9368f
|
@ -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
|
||||||
|
|
|
@ -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!)
|
||||||
|
|
|
@ -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,
|
||||||
|
@ -43,5 +47,15 @@ class StatusEditPollView: UIStackView {
|
||||||
addArrangedSubview(stack)
|
addArrangedSubview(stack)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue