Instagram / IGListKit

A data-driven UICollectionView framework for building fast and flexible lists.
https://instagram.github.io/IGListKit/
MIT License
12.88k stars 1.54k forks source link

Cell expanding animation #1345

Open Recouse opened 5 years ago

Recouse commented 5 years ago

New issue checklist

I tried to make cell expanding animation, but got this: https://imgur.com/a/DtCEHis

I saw implementation in examples with updating labels frame in layoutSubviews (mine with constraints and self-sizing cells), it works a bit better, but I think there should be another way.

I want it to work like in Instagram app in photo descriptions)

Recouse commented 5 years ago

Bump

Recouse commented 5 years ago

Does anyone have a solution to this problem?

Ziewvater commented 5 years ago

@Recouse, do you have more info on how you got to this result? Were you following a guide to help you get here? Without much more info about what's going on here I can't really give you any useful help.

Recouse commented 5 years ago

I used expanding cell code from examples. But I wanted to do this with self-sizing cells.

AuthorDescriptionCell.swift:

import UIKit

class AuthorDescriptionCell: UICollectionViewCell {
    static let insets = UIEdgeInsets(top: 0, left: Global.UI.edgeInset, bottom: 0, right: Global.UI.edgeInset)
    static let font = UIFont.systemFont(ofSize: 14)

    static var singleLineHeight: CGFloat {
        return font.lineHeight
    }

    var dataSource: Author? {
        didSet {
            updateData()
        }
    }

    var shouldUpdateSize: Bool = false

    let descriptionTextView: ReadMoreTextView = {
        let textView = ReadMoreTextView()
        textView.textColor = Asset.Colors.dark.color
        textView.font = .systemFont(ofSize: 14)
        textView.shouldTrim = true
        textView.maximumNumberOfLines = 3
        textView.contentInset = .zero

        let readMoreStyle: [NSAttributedString.Key: Any] = [
            .font: UIFont.systemFont(ofSize: 14, weight: .medium),
            .foregroundColor: Asset.Colors.clearBlue.color
        ]

        textView.attributedReadMoreText = NSAttributedString(string: " Ko‘proq", attributes: readMoreStyle)
        textView.attributedReadLessText = NSAttributedString(string: " Kamroq", attributes: readMoreStyle)

        return textView
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        contentView.addSubview(descriptionTextView)
        descriptionTextView.snp.makeConstraints {
            $0.top.equalToSuperview()
            $0.left.right.equalToSuperview().offset(Global.UI.edgeInset).inset(Global.UI.edgeInset)
            $0.bottom.equalToSuperview().priority(250)
        }

        descriptionTextView.readMoreDelegate = self
        descriptionTextView.onSizeChange = { [unowned self] _ in
            guard self.shouldUpdateSize else { return }

            self.shouldUpdateSize = false
            self.delegate?.sizeChanged()
        }
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        setNeedsLayout()
        layoutIfNeeded()

        let size = contentView.systemLayoutSizeFitting(layoutAttributes.size)
        var newFrame = layoutAttributes.frame
        newFrame.size.height = ceil(size.height)
        layoutAttributes.frame = newFrame

        return layoutAttributes
    }

    func updateData() {
        guard let author = dataSource else { return }

        descriptionTextView.text = author.biography
    }

    private func updateSize() {
        let bounds = contentView.bounds
        descriptionTextView.frame = bounds.inset(by: AuthorDescriptionCell.insets)
    }

    static func textHeight(_ text: String, width: CGFloat) -> CGFloat {
        let constrainedSize = CGSize(width: width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)
        let attributes = [ NSAttributedString.Key.font: font ]
        let options: NSStringDrawingOptions = [.usesFontLeading, .usesLineFragmentOrigin]
        let bounds = (text as NSString).boundingRect(with: constrainedSize, options: options, attributes: attributes, context: nil)
        return ceil(bounds.height) + insets.top + insets.bottom
    }
}

extension AuthorDescriptionCell: ReadMoreTextViewDelegate {
    func textWasCollapsed() {
        shouldUpdateSize = true
    }

    func textWasExpanded() {
        shouldUpdateSize = true
    }
}

AuthorDescriptionSectionController.swift:

import UIKit
import IGListKit

class AuthorDescriptionSectionController: ListSectionController {
    var object: KeyedModel<Author>?

    override init() {
        super.init()

        inset = UIEdgeInsets(top: 14, left: 0, bottom: 24, right: 0)
    }

    override func didUpdate(to object: Any) {
        self.object = object as? KeyedModel<Author>
    }

    override func sizeForItem(at index: Int) -> CGSize {
        let width = collectionContext!.containerSize.width
        let height = AuthorDescriptionCell.singleLineHeight * 3

        return CGSize(width: width, height: height)
    }

    override func cellForItem(at index: Int) -> UICollectionViewCell {
        guard let cell = collectionContext?.dequeueReusableCell(
            of: AuthorDescriptionCell.self,
            for: self,
            at: index
            ) as? AuthorDescriptionCell else {
                fatalError()
        }

        cell.dataSource = object?.model
        cell.delegate = self

        return cell
    }

    func toggle() {
        collectionContext?.invalidateLayout(for: self)
    }
}

extension AuthorDescriptionSectionController: BookDescriptionCellDelegate {
    func sizeChanged() {
        toggle()
    }
}
Recouse commented 5 years ago

Bump

sjang42 commented 4 years ago

No any progress?

Recouse commented 4 years ago

Nope, still don’t know how to solve this.

joetam commented 4 years ago

@Recouse do you have a minimal example that reproduces this problem? It'd help if it's more narrowed down

Recouse commented 4 years ago

@joetam I posted an example here https://github.com/Instagram/IGListKit/issues/1345#issuecomment-518505712

luyizhang commented 4 years ago

@Recouse the animation looks like it's funky because after collectionContext?.invalidateLayout(for: self) is called in the section controller, the size for the cell is the same as the previous layout (AuthorDescriptionCell.singleLineHeight * 3) and THEN preferredLayoutAttributesFitting is triggering a resize.

Here is an IGListKit example of an expanding section controller.

Based on the above example, here are some changes I recommend:

  1. add a local expanded property in the section controller and toggle this property before calling invalidateLayout in the section controller
  2. in the section controller's sizeForItem, calculate the desired height for your cell based on the expanded state. (i.e. height = expanded ? AuthorDescriptionCell.textHeight(text:width:) : AuthorDescriptionCell.singleLineHeight * 3 (this can be cleaned up even further by passing expanded to textHeight and doing the calculation in there))
  3. wrap invalidateLayout in an animation block
  4. remove the cell's preferredLayoutAttributesFitting (you don't really need this in most cases with IGListKit)
  5. simplify your delegates - you can get rid of the ReadMoreTextViewDelegate (and the cell's shouldUpdateSize), and instead assign a tap target to "read more" in the collection view cell. when user has tapped read more, directly trigger the cell's delegate to invalidateLayout from the section controller.
  6. after updating the cell height, make sure to also update descriptionTextView's maximum number of lines as needed

Hope the above helps!