LayoutKit icon indicating copy to clipboard operation
LayoutKit copied to clipboard

Aspect ratio layout?

Open AttilaTheFun opened this issue 8 years ago • 8 comments

Hi I'm interested in using your library, but am struggling to figure out how to accomplish a task which is trivial to do with auto layout. I'm trying to create a layout with a fixed aspect ratio. Is there a simple way to create something like a SizeLayout which will expand to its superview's (er, super-layout)'s width and then its height will be calculated using an aspect ratio with which it is initialized? Assuming there's a way to do this, I'd like to see a convenience initializer which I can call with something like: SizeLayout<UIImageView>(aspectRatio: ...) The specific use case I have is images in a feed of variable heights with consistent widths.

P.S., it would be nice if we had another typealias for UIImageView < ImageView > NSImageView so we can make our layouts with images compile cross platform as well.

AttilaTheFun avatar Feb 02 '17 09:02 AttilaTheFun

There is Alignment.aspectFit. Does that suit your situation?

If not, there's may be room for an AspectRatioLayout to be added to the library or at least as a sample. Until that happens, you can do it yourself locally in your codebase by making a new layout class yourself and inserting it into your tree of Layouts.

staguer avatar Feb 03 '17 00:02 staguer

@staguer I'm still not sure how I could use the aspect fit measurement to construct such a layout - how do you pass in the aspect ratio to use? Could you post such an example here and add the layout to the example layouts? Thanks!

AttilaTheFun avatar Feb 03 '17 05:02 AttilaTheFun

@staguer I still don't understand how to use the aspect fit alignment to make a layout that scales to the maximum width allowed by its parent, and then expands vertically to maintain an aspect ratio. This is what I'm trying:

public final class AspectImageLayout: SizeLayout<ImageView> {
    public required init(imageURL: URL, imageSize: CGSize) {
        assert(imageSize.width > 0 && imageSize.height > 0)
        let aspectRatio = max(min(imageSize.aspectRatio, 2), 0.5)
        let aspectSize = CGSize(aspectRatio: aspectRatio, width: 1)
        super.init(minWidth: aspectSize.width, maxWidth: aspectSize.width, minHeight: aspectSize.height,
                   maxHeight: aspectSize.height, alignment: .aspectFit)
        { imageView in
            imageView.image = nil
            imageView.contentMode = .scaleAspectFill
            imageView.clipsToBounds = true
            let imageSize = CGSize(aspectRatio: aspectRatio, width: imageView.frame.size.width)
            let options = kKingfisherOptions + [.processor(ResizingImageProcessor(targetSize: imageSize))]
            imageView.kf.setImage(with: imageURL, options: options)
        }
    }
}

AttilaTheFun avatar Feb 03 '17 18:02 AttilaTheFun

@nicksnyder I'm still not sure how exactly to accomplish this using the tools provided. This seems like an extremely common use case and should at least be included as an example, if not as one of the 'primitive' layouts.

AttilaTheFun avatar Feb 04 '17 05:02 AttilaTheFun

@nicksnyder @staguer I was finally able to hack something together that works:

import LayoutKit
import CoreGraphics

open class AspectLayout<V: View>: BaseLayout<V>, ConfigurableLayout {
    open let aspectSize: CGSize
    open let sublayout: Layout?

    // MARK: - Designated initializers

    public init(aspectSize: CGSize,
                alignment: Alignment? = nil,
                flexibility: Flexibility = .flexible,
                viewReuseId: String? = nil,
                sublayout: Layout? = nil,
                config: ((V) -> Void)? = nil)
    {
        self.aspectSize = aspectSize
        self.sublayout = sublayout
        let alignment = alignment ?? Alignment(vertical: .top, horizontal: .fill)
        super.init(alignment: alignment, flexibility: flexibility, viewReuseId: viewReuseId, config: config)
    }

    // MARK: - Layout protocol

    open func measurement(within maxSize: CGSize) -> LayoutMeasurement {
        let aspectRatio = aspectSize.height > 0 ? aspectSize.width / aspectSize.height : 0
        let maxRatio = maxSize.height > 0 ? maxSize.width / maxSize.height : 0

        // Ratio is fatter than max, fit to max width, else expand to height
        let scaledSize = aspectRatio > maxRatio ? CGSize(aspectSize: aspectSize, width: maxSize.width) :
            CGSize(aspectSize: aspectSize, height: maxSize.height)

        // Measure the sublayout if it exists.
        let sublayoutMeasurement = sublayout?.measurement(within: scaledSize)
        let sublayouts = [sublayoutMeasurement].flatMap { $0 }

        return LayoutMeasurement(layout: self, size: scaledSize, maxSize: maxSize, sublayouts: sublayouts)
    }

    open func arrangement(within rect: CGRect, measurement: LayoutMeasurement) -> LayoutArrangement {
        let frame = alignment.position(size: measurement.size, in: rect)
        let sublayoutRect = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
        let sublayouts = measurement.sublayouts.map { (measurement) in
            return measurement.arrangement(within: sublayoutRect)
        }
        return LayoutArrangement(layout: self, frame: frame, sublayouts: sublayouts)
    }
}

I'm sure you can do a better job, but I think something along these lines would be immensely useful for anyone who is going to use your library.

AttilaTheFun avatar Feb 04 '17 06:02 AttilaTheFun

@AttilaTheFun I played around with using SizeLayout with Alignment.aspectFit and realized that it doesn't quite do what you want. Your AspectLayout looks pretty close to mergeable. Want to submit a pull request?

nicksnyder avatar Feb 05 '17 05:02 nicksnyder

@nicksnyder Sure, I suppose I could put up a PR tomorrow.

AttilaTheFun avatar Feb 06 '17 08:02 AttilaTheFun

any plans to finalize this and get it merged in?

netspencer avatar Dec 10 '19 23:12 netspencer