IGListKit icon indicating copy to clipboard operation
IGListKit copied to clipboard

Cell expanding animation

Open Recouse opened this issue 6 years ago • 10 comments
trafficstars

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 avatar Jul 19 '19 13:07 Recouse

Bump

Recouse avatar Jul 27 '19 15:07 Recouse

Does anyone have a solution to this problem?

Recouse avatar Aug 05 '19 13:08 Recouse

@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.

Ziewvater avatar Aug 05 '19 19:08 Ziewvater

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 avatar Aug 06 '19 05:08 Recouse

Bump

Recouse avatar Nov 14 '19 08:11 Recouse

No any progress?

sjang42 avatar Apr 06 '20 11:04 sjang42

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

Recouse avatar Apr 06 '20 13:04 Recouse

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

joetam avatar Oct 17 '20 23:10 joetam

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

Recouse avatar Oct 18 '20 08:10 Recouse

@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!

luyizhang avatar Oct 20 '20 04:10 luyizhang