LayoutKit
LayoutKit copied to clipboard
Aspect ratio layout?
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.
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 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!
@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)
}
}
}
@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.
@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 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 Sure, I suppose I could put up a PR tomorrow.
any plans to finalize this and get it merged in?