mirror of
https://github.com/zhigang1992/GitHawk.git
synced 2026-04-24 04:05:16 +08:00
dynamic text resize on orientation change
This commit is contained in:
@@ -69,8 +69,8 @@ final class IssueCommentCodeBlockCell: UICollectionViewCell, ListBindable, Colla
|
||||
func bindViewModel(_ viewModel: Any) {
|
||||
guard let viewModel = viewModel as? IssueCommentCodeBlockModel else { return }
|
||||
|
||||
let contentSize = viewModel.code.textViewSize
|
||||
scrollView.contentSize = viewModel.code.textViewSize
|
||||
let contentSize = viewModel.code.textViewSize(contentView.bounds.width)
|
||||
scrollView.contentSize = contentSize
|
||||
|
||||
label.attributedText = viewModel.code.attributedText
|
||||
let textFrame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
|
||||
|
||||
@@ -17,14 +17,14 @@ private func bodyIsCollapsible(body: Any) -> Bool {
|
||||
}
|
||||
}
|
||||
|
||||
func IssueCollapsedBodies(bodies: [AnyObject]) -> (AnyObject, CGFloat)? {
|
||||
func IssueCollapsedBodies(bodies: [AnyObject], width: CGFloat) -> (AnyObject, CGFloat)? {
|
||||
let cap: CGFloat = 300
|
||||
// minimum height to collapse so expanding shows significant amount of content
|
||||
let minDelta = CollapseCellMinHeight * 3
|
||||
|
||||
var totalHeight: CGFloat = 0
|
||||
for body in bodies {
|
||||
let height = bodyHeight(viewModel: body)
|
||||
let height = bodyHeight(viewModel: body, width: width)
|
||||
totalHeight += height
|
||||
if bodyIsCollapsible(body: body),
|
||||
totalHeight > cap,
|
||||
|
||||
@@ -95,7 +95,9 @@ AttributedStringViewDelegate {
|
||||
sizeForViewModel viewModel: Any,
|
||||
at index: Int
|
||||
) -> CGSize {
|
||||
guard let context = self.collectionContext else { return .zero }
|
||||
guard let width = collectionContext?.containerSize.width
|
||||
else { return .zero }
|
||||
|
||||
let height: CGFloat
|
||||
if collapsed && (viewModel as AnyObject) === object?.collapse?.model {
|
||||
height = object?.collapse?.height ?? 0
|
||||
@@ -103,9 +105,10 @@ AttributedStringViewDelegate {
|
||||
let size = htmlSizes[viewModel.html] {
|
||||
height = size.height
|
||||
} else {
|
||||
height = bodyHeight(viewModel: viewModel)
|
||||
height = bodyHeight(viewModel: viewModel, width: width)
|
||||
}
|
||||
return CGSize(width: context.containerSize.width, height: height)
|
||||
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
|
||||
func sectionController(
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
func bodyHeight(viewModel: Any) -> CGFloat {
|
||||
func bodyHeight(viewModel: Any, width: CGFloat) -> CGFloat {
|
||||
if let viewModel = viewModel as? NSAttributedStringSizing {
|
||||
return viewModel.textViewSize.height
|
||||
return viewModel.textViewSize(width).height
|
||||
} else if let viewModel = viewModel as? IssueCommentCodeBlockModel {
|
||||
let inset = IssueCommentCodeBlockCell.scrollViewInset
|
||||
return viewModel.code.textViewSize.height + inset.top + inset.bottom
|
||||
return viewModel.code.textViewSize(0).height + inset.top + inset.bottom
|
||||
} else if viewModel is IssueCommentImageModel {
|
||||
return 200.0
|
||||
} else if viewModel is IssueCommentReactionViewModel {
|
||||
@@ -21,7 +21,7 @@ func bodyHeight(viewModel: Any) -> CGFloat {
|
||||
} else if viewModel is IssueCommentDetailsViewModel {
|
||||
return Styles.Sizes.gutter * 2 + Styles.Sizes.avatar.height
|
||||
} else if let viewModel = viewModel as? IssueCommentQuoteModel {
|
||||
return viewModel.quote.textViewSize.height
|
||||
return viewModel.quote.textViewSize(width).height
|
||||
} else if viewModel is IssueCommentHrModel {
|
||||
return 3.0 + IssueCommentHrCell.inset.top + IssueCommentHrCell.inset.bottom
|
||||
} else {
|
||||
|
||||
@@ -75,7 +75,7 @@ func createCommentModel(
|
||||
let details = IssueCommentDetailsViewModel(date: date, login: author.login, avatarURL: avatarURL)
|
||||
let bodies = commentModels(markdown: commentFields.body, width: width)
|
||||
let reactions = createIssueReactions(reactions: reactionFields)
|
||||
let collapse = IssueCollapsedBodies(bodies: bodies)
|
||||
let collapse = IssueCollapsedBodies(bodies: bodies, width: width)
|
||||
return IssueCommentModel(
|
||||
id: id,
|
||||
details: details,
|
||||
|
||||
@@ -17,9 +17,9 @@ final class IssueTitleSectionController: ListGenericSectionController<NSAttribut
|
||||
}
|
||||
|
||||
override func sizeForItem(at index: Int) -> CGSize {
|
||||
guard let context = collectionContext
|
||||
guard let width = collectionContext?.containerSize.width
|
||||
else { return .zero }
|
||||
return CGSize(width: context.containerSize.width, height: self.object?.textViewSize.height ?? 0)
|
||||
return CGSize(width: width, height: self.object?.textViewSize(width).height ?? 0)
|
||||
}
|
||||
|
||||
override func cellForItem(at index: Int) -> UICollectionViewCell {
|
||||
|
||||
@@ -83,7 +83,7 @@ final class NotificationCell: UICollectionViewCell {
|
||||
func bindViewModel(_ viewModel: Any) {
|
||||
guard let viewModel = viewModel as? NotificationViewModel else { return }
|
||||
titleLabel.text = "\(viewModel.owner)/\(viewModel.repo)"
|
||||
textLabel.attributedText = viewModel.title
|
||||
textLabel.attributedText = viewModel.title.attributedText
|
||||
dateLabel.setText(date: viewModel.date)
|
||||
reasonImageView.image = viewModel.type.icon?.withRenderingMode(.alwaysTemplate)
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ import IGListKit
|
||||
final class NotificationViewModel: ListDiffable {
|
||||
|
||||
let id: String
|
||||
let title: NSAttributedString
|
||||
let titleSize: CGSize
|
||||
let title: NSAttributedStringSizing
|
||||
let type: NotificationType
|
||||
let date: Date
|
||||
let read: Bool
|
||||
@@ -44,14 +43,11 @@ final class NotificationViewModel: ListDiffable {
|
||||
NSFontAttributeName: Styles.Fonts.body,
|
||||
NSForegroundColorAttributeName: Styles.Colors.Gray.dark.color
|
||||
]
|
||||
self.title = NSAttributedString(string: title, attributes: attributes)
|
||||
|
||||
let sizing = NSAttributedStringSizing(
|
||||
self.title = NSAttributedStringSizing(
|
||||
containerWidth: containerWidth,
|
||||
attributedText: self.title,
|
||||
attributedText: NSAttributedString(string: title, attributes: attributes),
|
||||
inset: NotificationCell.labelInset
|
||||
)
|
||||
self.titleSize = sizing.textViewSize
|
||||
}
|
||||
|
||||
// MARK: ListDiffable
|
||||
|
||||
@@ -38,6 +38,14 @@ FeedDelegate {
|
||||
feed.adapter.dataSource = self
|
||||
}
|
||||
|
||||
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
// coordinator.animate(alongsideTransition: { _ in
|
||||
// print("alongside")
|
||||
// }) { _ in
|
||||
// self.feed.collectionView.collectionViewLayout.invalidateLayout()
|
||||
// }
|
||||
}
|
||||
|
||||
// MARK: Private API
|
||||
|
||||
private func update(fromNetwork: Bool) {
|
||||
|
||||
@@ -19,10 +19,9 @@ final class RepoNotificationsSectionController: ListGenericSectionController<Not
|
||||
}
|
||||
|
||||
override func sizeForItem(at index: Int) -> CGSize {
|
||||
guard let context = collectionContext,
|
||||
let object = self.object
|
||||
guard let width = collectionContext?.containerSize.width
|
||||
else { return .zero }
|
||||
return CGSize(width: context.containerSize.width, height: object.titleSize.height)
|
||||
return CGSize(width: width, height: object?.title.textViewSize(width).height ?? 0)
|
||||
}
|
||||
|
||||
override func cellForItem(at index: Int) -> UICollectionViewCell {
|
||||
|
||||
@@ -9,6 +9,24 @@
|
||||
import UIKit
|
||||
import IGListKit
|
||||
|
||||
extension CGSize {
|
||||
|
||||
func snapped(scale: CGFloat) -> CGSize {
|
||||
var size = self
|
||||
size.width = ceil(size.width * scale) / scale
|
||||
size.height = ceil(size.height * scale) / scale
|
||||
return size
|
||||
}
|
||||
|
||||
func resized(inset: UIEdgeInsets) -> CGSize {
|
||||
var size = self
|
||||
size.width += inset.left + inset.right
|
||||
size.height += inset.top + inset.bottom
|
||||
return size
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final class NSAttributedStringSizing: NSObject, ListDiffable {
|
||||
|
||||
private let textContainer: NSTextContainer
|
||||
@@ -17,8 +35,6 @@ final class NSAttributedStringSizing: NSObject, ListDiffable {
|
||||
|
||||
let inset: UIEdgeInsets
|
||||
let attributedText: NSAttributedString
|
||||
let textViewSize: CGSize
|
||||
let textSize: CGSize
|
||||
let screenScale: CGFloat
|
||||
|
||||
init(
|
||||
@@ -39,8 +55,7 @@ final class NSAttributedStringSizing: NSObject, ListDiffable {
|
||||
self.inset = inset
|
||||
self.screenScale = screenScale
|
||||
|
||||
let insetWidth = containerWidth - inset.left - inset.right
|
||||
textContainer = NSTextContainer(size: CGSize(width: insetWidth, height: 0))
|
||||
textContainer = NSTextContainer()
|
||||
textContainer.exclusionPaths = exclusionPaths
|
||||
textContainer.maximumNumberOfLines = maximumNumberOfLines
|
||||
textContainer.lineFragmentPadding = lineFragmentPadding
|
||||
@@ -57,24 +72,29 @@ final class NSAttributedStringSizing: NSObject, ListDiffable {
|
||||
textStorage = NSTextStorage(attributedString: attributedText)
|
||||
textStorage.addLayoutManager(layoutManager)
|
||||
|
||||
// find the size of the text now that everything is configured
|
||||
let bounds = layoutManager.usedRect(for: textContainer)
|
||||
super.init()
|
||||
|
||||
var viewSize = bounds.size
|
||||
|
||||
// snap to pixel
|
||||
viewSize.width = ceil(viewSize.width * screenScale) / screenScale
|
||||
viewSize.height = ceil(viewSize.height * screenScale) / screenScale
|
||||
textSize = viewSize
|
||||
|
||||
// adjust for the text view inset (contentInset + textContainerInset)
|
||||
viewSize.width += inset.left + inset.right
|
||||
viewSize.height += inset.top + inset.bottom
|
||||
textViewSize = viewSize
|
||||
computeSize(containerWidth)
|
||||
}
|
||||
|
||||
// MARK: Public API
|
||||
|
||||
private var _textSize = [CGFloat: CGSize]()
|
||||
func textSize(_ width: CGFloat) -> CGSize {
|
||||
if let cache = _textSize[width] {
|
||||
return cache
|
||||
}
|
||||
return computeSize(width).size
|
||||
}
|
||||
|
||||
private var _textViewSize = [CGFloat: CGSize]()
|
||||
func textViewSize(_ width: CGFloat) -> CGSize {
|
||||
if let cache = _textViewSize[width] {
|
||||
return cache
|
||||
}
|
||||
return computeSize(width).viewSize
|
||||
}
|
||||
|
||||
func configure(textView: UITextView) {
|
||||
textView.attributedText = attributedText
|
||||
textView.contentInset = .zero
|
||||
@@ -94,18 +114,25 @@ final class NSAttributedStringSizing: NSObject, ListDiffable {
|
||||
layoutManager.addTextContainer(textContainer)
|
||||
}
|
||||
|
||||
private var _contents: CGImage? = nil
|
||||
func contents() -> CGImage? {
|
||||
guard _contents == nil else { return _contents }
|
||||
UIGraphicsBeginImageContextWithOptions(textSize, true, screenScale)
|
||||
private var _contents = [CGFloat: CGImage]()
|
||||
func contents(_ width: CGFloat) -> CGImage? {
|
||||
if let contents = _contents[width] {
|
||||
return contents
|
||||
}
|
||||
|
||||
let size = textSize(width)
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(size, true, screenScale)
|
||||
UIColor.white.setFill()
|
||||
UIBezierPath(rect: CGRect(origin: .zero, size: textSize)).fill()
|
||||
UIBezierPath(rect: CGRect(origin: .zero, size: size)).fill()
|
||||
let glyphRange = layoutManager.glyphRange(for: textContainer)
|
||||
layoutManager.drawBackground(forGlyphRange: glyphRange, at: .zero)
|
||||
layoutManager.drawGlyphs(forGlyphRange: glyphRange, at: .zero)
|
||||
let contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage
|
||||
UIGraphicsEndImageContext()
|
||||
_contents = contents
|
||||
|
||||
_contents[width] = contents
|
||||
|
||||
return contents
|
||||
}
|
||||
|
||||
@@ -118,6 +145,27 @@ final class NSAttributedStringSizing: NSObject, ListDiffable {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: Private API
|
||||
|
||||
@discardableResult
|
||||
func computeSize(_ width: CGFloat) -> (size: CGSize, viewSize: CGSize) {
|
||||
let insetWidth = width - inset.left - inset.right
|
||||
textContainer.size = CGSize(width: insetWidth, height: 0)
|
||||
|
||||
// find the size of the text now that everything is configured
|
||||
let bounds = layoutManager.usedRect(for: textContainer)
|
||||
|
||||
// snap to pixel
|
||||
let size = bounds.size.snapped(scale: screenScale)
|
||||
_textSize[width] = size
|
||||
|
||||
// adjust for the text view inset (contentInset + textContainerInset)
|
||||
let viewSize = size.resized(inset: inset)
|
||||
_textViewSize[width] = viewSize
|
||||
|
||||
return (size, viewSize)
|
||||
}
|
||||
|
||||
// MARK: ListDiffable
|
||||
|
||||
func diffIdentifier() -> NSObjectProtocol {
|
||||
@@ -125,9 +173,7 @@ final class NSAttributedStringSizing: NSObject, ListDiffable {
|
||||
}
|
||||
|
||||
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
|
||||
if self === object { return true }
|
||||
guard let object = object as? NSAttributedStringSizing else { return false }
|
||||
return textViewSize == object.textViewSize
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,9 +38,13 @@ final class AttributedStringView: UIView {
|
||||
|
||||
func configureAndSizeToFit(text: NSAttributedStringSizing) {
|
||||
self.text = text
|
||||
|
||||
let width = bounds.width
|
||||
layer.contentsScale = text.screenScale
|
||||
layer.contents = text.contents()
|
||||
frame = UIEdgeInsetsInsetRect(CGRect(origin: .zero, size: text.textViewSize), text.inset)
|
||||
layer.contents = text.contents(width)
|
||||
|
||||
let rect = CGRect(origin: .zero, size: text.textViewSize(width))
|
||||
frame = UIEdgeInsetsInsetRect(rect, text.inset)
|
||||
}
|
||||
|
||||
// MARK: Private API
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>44</string>
|
||||
<string>52</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>org-appextension-feature-password-management</string>
|
||||
|
||||
Reference in New Issue
Block a user