英文:
UIImageView with fixed height: How can I get the image view width to grow or shrink to match the aspect ratio of the image?
问题
I am not supplying code because this is purely a conceptual "how to achieve this" question. But I have listed my attempts and the constraints below.
我没有提供代码,因为这纯粹是一个概念性的“如何实现这一点”的问题。但我已经列出了我的尝试和约束如下。
I've tried a few different strategies to achieve this. Basically, I have variable width images and would like the image to line up perfectly on the left and maintain the 8px spacing with the label on the right. See the images below!
我尝试了一些不同的策略来实现这一点。基本上,我有可变宽度的图像,希望图像能够完美地与左侧对齐,并保持与右侧标签的8像素间距。请参见下面的图片!
The views themselves are inside a vertical UIStackView. This row a horizontal UIStackView but I've also tried it with a simple UIView container. The only constraint set is on the UIImageView, fixed height of 20px. But I'm looking for any viable strategy to achieve variable width, consistent margins behavior.
这些视图本身位于垂直UIStackView内。这一行是一个水平UIStackView,但我还尝试了将其放入一个简单的UIView容器中。唯一设置的约束是在UIImageView上,固定高度为20像素。但我正在寻找任何可行的策略来实现可变宽度和一致的边距行为。
I've tried setting the compressionResistence/contentHuggingPriority low on the UIImageView and every combination in between. Is this just not possible?
我尝试在UIImageView上设置compressionResistance和contentHuggingPriority的值较低,以及各种组合。这是否根本不可能?
I've highlighted the background blue to demonstrate the issue.
我已经突出显示了蓝色背景以说明问题。
Undesired Behavior: not left aligned, extra space between text
不希望的行为:不左对齐,文本之间有额外的空间
Desired Behavior: no wasted space, everything lines up
期望的行为:没有浪费的空间,一切都对齐
As shown above, the Blue image view is 20 x 40 points, with Content Mode set to Aspect Fit.
如上所示,蓝色图像视图为20 x 40点,内容模式设置为Aspect Fit。
How can I get the image view width to grow or shrink to match the aspect ratio of the image?
如何使图像视图的宽度能够根据图像的宽高比而增长或缩小?
英文:
I am not supplying code because this is purely a conceptual "how to achieve this" question. But I have listed my attempts and the constraints below.
I've tried a few different strategies to achieve this. Basically, I have variable width images and would like the image to line up perfectly on the left and maintain the 8px spacing with the label on the right. See the images below!
The views themselves are inside a vertical UIStackView. This row a horizontal UIStackView but I've also tried it with a simple UIView container. The only constraint set is on the UIImageView, fixed height of 20px. But I'm looking for any viable strategy to achieve variable width, consistent margins behavior.
I've tried setting the compressionResistence/contentHuggingPriority low on the UIImageView and every combination in between. Is this just not possible?
I've highlighted the background blue to demonstrate the issue
Undesired Behavior: not left aligned, extra space between text
Desired Behavior: no wasted space, everything lines up
As shown above, the Blue image view is 20 x 40 points, with Content Mode set to Aspect Fit.
How can I get the image view width to grow or shrink to match the aspect ratio of the image?
答案1
得分: 4
为了使UIImageView
的大小与图像的纵横比相匹配,您可以将宽度约束设置为高度约束的倍数。
如果我们想要一个图像视图的高度为20个点,并且具有"aspect-fit"的宽度:
imageView.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: img.size.width / img.size.height).isActive = true
所以,假设我们有一个大小为100 x 100
的图像... 我们这样说:
img.size.width / img.size.height == 1.0
widthAnchor
等于20.0 * 1.0
- 结果imageView的框架大小是
20, 20
如果我们的图像大小是250 x 100
:
img.size.width / img.size.height == 2.5
widthAnchor
等于20.0 * 2.5
- 结果imageView的框架大小是
50, 20
假设您有一个UIView
子类,在初始化期间设置了约束,然后设置了图像,您可以使用一个可修改的约束作为视图的变量/属性:
class SomeCustomView: UIView {
// 当设置小图像时,我们将更新这个约束
private var smallImageWidthConstraint: NSLayoutConstraint!
// 初始约束设置
smallImageView.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
// 小图像的宽度约束 - 在设置图像时将修改
// 小图像的默认大小是20x40
smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalToConstant: 40.0)
smallImageWidthConstraint.isActive = true
}
// 当设置.image属性时,我们更新该约束
// 取消激活
smallImageWidthConstraint.isActive = false
// 根据高度设置宽度以匹配图像的纵横比
smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalTo: smallImageView.heightAnchor, multiplier: img.size.width / img.size.height)
// 重新激活
smallImageWidthConstraint.isActive = true
这是一个快速示例,使用了这些3个图像。
英文:
To get a UIImageView
size to match the image aspect-ratio, you can set the width constraint as a multiple of the height constraint.
If we want an image view to have a Height of 20-points, and an "aspect-fit" Width:
imageView.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: img.size.width / img.size.height).isActive = true
So, let's say we have an image that is 100 x 100
... we're saying:
img.size.width / img.size.height == 1.0
widthAnchor
equals20.0 * 1.0
- resulting imageView frame size is
20, 20
If our image size is 250 x 100
img.size.width / img.size.height == 2.5
widthAnchor
equals20.0 * 2.5
- resulting imageView frame size is
50, 20
Assuming you have a UIView
subclass where the constraints are setup during init, and you are then setting the image, you want to use a "modifiable" constraint as a var / property of your view:
class SomeCustomView: UIView {
// we'll update this constraint when the small image is set
private var smallImageWidthConstraint: NSLayoutConstraint!
During initial constraint setup:
smallImageView.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
// small image width constraint - will be modified when we set the image
// small image default is 20x40
smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalToConstant: 40.0)
smallImageWidthConstraint.isActive = true
When the .image
is set, we update that constraint:
// de-activate
smallImageWidthConstraint.isActive = false
// set width proportional to height to match image aspect ratio
smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalTo: smallImageView.heightAnchor, multiplier: img.size.width / img.size.height)
// re-activate
smallImageWidthConstraint.isActive = true
Here's a quick example, using these 3 images:
In assets, I named them p272x120
, p91x200
and p232x209
since those are the actual image dimensions.
Using this custom view:
class SomeCustomView: UIView {
public var bigImage: UIImage? {
didSet { bigImageView.image = bigImage }
}
public var topString: String = "" {
didSet { topLabel.text = topString }
}
public var sideString: String = "" {
didSet { sideLabel.text = sideString }
}
// when we set the small image, we also update the width constraint
public var smallImage: UIImage? {
didSet {
smallImageView.image = smallImage
// unwrap optional
if let img = smallImage {
// de-activate
smallImageWidthConstraint.isActive = false
// set width proportional to height to match image aspect ratio
smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalTo: smallImageView.heightAnchor, multiplier: img.size.width / img.size.height)
// re-activate
smallImageWidthConstraint.isActive = true
}
}
}
private let bigImageView = UIImageView()
private let smallImageView = UIImageView()
private let topLabel: UILabel = {
let v = UILabel()
v.font = .systemFont(ofSize: 15.0, weight: .bold)
v.numberOfLines = 0
return v
}()
private let subLabel: UILabel = {
let v = UILabel()
v.font = .italicSystemFont(ofSize: 14.0)
v.numberOfLines = 0
return v
}()
private let sideLabel: UILabel = {
let v = UILabel()
v.font = .systemFont(ofSize: 12.0, weight: .bold)
v.numberOfLines = 0
return v
}()
// we'll update this constraint when the small image is set
private var smallImageWidthConstraint: NSLayoutConstraint!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
// horizontal stack view to hold
// small image and side label
let hStack = UIStackView(arrangedSubviews: [smallImageView, sideLabel])
hStack.spacing = 8
hStack.alignment = .top
// vertical stack view to hold labels and ssmall image
let vStack = UIStackView(arrangedSubviews: [topLabel, subLabel, hStack])
vStack.axis = .vertical
vStack.spacing = 8
[bigImageView, vStack].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
}
// small image width constraint - will be modified when we set the image
// small image default is 20x40
smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalToConstant: 40.0)
NSLayoutConstraint.activate([
// big image 80x80
bigImageView.widthAnchor.constraint(equalToConstant: 80.0),
bigImageView.heightAnchor.constraint(equalToConstant: 80.0),
// leading
bigImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12.0),
// center vertically
bigImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
// at least 12-points top/bottom
bigImageView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 12.0),
bigImageView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -12.0),
// vStack leading 8-points from big image
vStack.leadingAnchor.constraint(equalTo: bigImageView.trailingAnchor, constant: 8.0),
// center vStack vertically
vStack.centerYAnchor.constraint(equalTo: centerYAnchor),
// at least 12-points top/bottom
vStack.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 12.0),
vStack.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -12.0),
// trailing
vStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
// small image height 20-points
smallImageView.heightAnchor.constraint(equalToConstant: 20.0),
// activate the "changeable" width constraint
smallImageWidthConstraint,
])
// let's set some properties
backgroundColor = UIColor(white: 0.9, alpha: 1.0)
// edit this after development...
// big image will be set by controller
bigImageView.backgroundColor = .systemRed
if let img = UIImage(systemName: "swift") {
bigImageView.image = img
}
// these will be .clear
smallImageView.backgroundColor = .systemBlue
topLabel.backgroundColor = .yellow
subLabel.backgroundColor = .cyan
sideLabel.backgroundColor = .green
}
// this is here during development to show
// the resulting small imageView size
override func layoutSubviews() {
super.layoutSubviews()
// let's use Int values, so we don't output .33333333...
let w = Int(smallImageView.frame.width)
let h = Int(smallImageView.frame.height)
subLabel.text = "Actual size (rounded): (w: \(w), h: \(h))"
}
}
and this example controller (putting the custom views in a vertical stack view):
class AspectImageVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 20.0
let myData: [[String]] = [
["p272x120", "Wide Image", "Nintendo Switch"],
["p91x200", "Tall, Narrow Image", "Left Controller"],
["p232x209", "Square-ish Image", "Nintendo Cube"],
["", "No Image", "So we can see default 20x40 small image view size"],
]
myData.forEach { d in
let imgName = d[0]
let top = d[1]
let side = d[2]
let someView = SomeCustomView()
if imgName.isEmpty {
someView.topString = top
} else {
someView.topString = imgName + " " + top
// safely load the image
if let img = UIImage(named: imgName) {
someView.smallImage = img
}
}
someView.sideString = side
stackView.addArrangedSubview(someView)
}
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
])
}
}
We get this output:
Edit - in response to comment...
Views can have more than one of "the same" constraint.
If you want your "small image view" to have a MAX width of 40-points...
Add a lessThanOrEqualToConstant
width constraint:
smallImageView.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
smallImageView.widthAnchor.constraint(lessThanOrEqualToConstant: 40.0).isActive = true
// small image width constraint - will be modified when we set the image
// small image default is 20x40
smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalToConstant: 40.0)
smallImageWidthConstraint.isActive = true
By default, constraints have .priority = .required
. So, when we set the image and modify the width constraint:
// de-activate
smallImageWidthConstraint.isActive = false
// set width proportional to height to match image aspect ratio
smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalTo: smallImageView.heightAnchor, multiplier: img.size.width / img.size.height)
// use less-than-required so we can limit its width
smallImageWidthConstraint.priority = .required - 1
// re-activate
smallImageWidthConstraint.isActive = true
So, the SomeCustomView
with those modifications:
class SomeCustomView: UIView {
public var bigImage: UIImage? {
didSet { bigImageView.image = bigImage }
}
public var topString: String = "" {
didSet { topLabel.text = topString }
}
public var sideString: String = "" {
didSet { sideLabel.text = sideString }
}
// when we set the small image, we also update the width constraint
public var smallImage: UIImage? {
didSet {
smallImageView.image = smallImage
// unwrap optional
if let img = smallImage {
// de-activate
smallImageWidthConstraint.isActive = false
// set width proportional to height to match image aspect ratio
smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalTo: smallImageView.heightAnchor, multiplier: img.size.width / img.size.height)
// use less-than-required so we can limit its width
smallImageWidthConstraint.priority = .required - 1
// re-activate
smallImageWidthConstraint.isActive = true
}
}
}
private let bigImageView = UIImageView()
private let smallImageView: UIImageView = {
let v = UIImageView()
v.contentMode = .scaleAspectFit
return v
}()
private let topLabel: UILabel = {
let v = UILabel()
v.font = .systemFont(ofSize: 15.0, weight: .bold)
v.numberOfLines = 0
return v
}()
private let subLabel: UILabel = {
let v = UILabel()
v.font = .italicSystemFont(ofSize: 14.0)
v.numberOfLines = 0
return v
}()
private let sideLabel: UILabel = {
let v = UILabel()
v.font = .systemFont(ofSize: 12.0, weight: .bold)
v.numberOfLines = 0
return v
}()
// we'll update this constraint when the small image is set
private var smallImageWidthConstraint: NSLayoutConstraint!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
// horizontal stack view to hold
// small image and side label
let hStack = UIStackView(arrangedSubviews: [smallImageView, sideLabel])
hStack.spacing = 8
hStack.alignment = .center
// vertical stack view to hold labels and ssmall image
let vStack = UIStackView(arrangedSubviews: [topLabel, subLabel, hStack])
vStack.axis = .vertical
vStack.spacing = 8
[bigImageView, vStack].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
}
// small image width constraint - will be modified when we set the image
// small image default is 20x40
smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalToConstant: 40.0)
NSLayoutConstraint.activate([
// big image 80x80
bigImageView.widthAnchor.constraint(equalToConstant: 80.0),
bigImageView.heightAnchor.constraint(equalToConstant: 80.0),
// leading
bigImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12.0),
// center vertically
bigImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
// at least 12-points top/bottom
bigImageView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 12.0),
bigImageView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -12.0),
// vStack leading 8-points from big image
vStack.leadingAnchor.constraint(equalTo: bigImageView.trailingAnchor, constant: 8.0),
// center vStack vertically
vStack.centerYAnchor.constraint(equalTo: centerYAnchor),
// at least 12-points top/bottom
vStack.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 12.0),
vStack.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -12.0),
// trailing
vStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
// small image height 20-points
smallImageView.heightAnchor.constraint(equalToConstant: 20.0),
// MAX width of 40-points
smallImageView.widthAnchor.constraint(lessThanOrEqualToConstant: 40.0),
// activate the "changeable" width constraint
smallImageWidthConstraint,
])
// let's set some properties
backgroundColor = UIColor(white: 0.9, alpha: 1.0)
// edit this after development...
// big image will be set by controller
bigImageView.backgroundColor = .systemRed
bigImageView.tintColor = .systemYellow
if let img = UIImage(systemName: "swift") {
bigImageView.image = img
}
// these will be .clear
smallImageView.backgroundColor = .systemBlue
topLabel.backgroundColor = .yellow
subLabel.backgroundColor = .cyan
sideLabel.backgroundColor = .green
}
// this is here during development to show
// the resulting small imageView size
override func layoutSubviews() {
super.layoutSubviews()
// let's use Int values, so we don't output .33333333...
let w = Int(smallImageView.frame.width)
let h = Int(smallImageView.frame.height)
subLabel.text = "Actual size (rounded): (w: \(w), h: \(h))"
}
}
and the output is now:
Note that the top image is only 40-points wide, instead of 45 as before.
答案2
得分: -1
Using DonMag的答案,我想出了自己的解决方案。我还没有测试边缘情况或其他任何东西,但它适用于我的用途。我在界面生成器中设置了固定的高度和内容模式为纵向适合
@IBDesignable
@objc class UIVariableWidthImageView : UIImageView {
override var image: UIImage! {
get {
return super.image
}
set {
super.image = newValue
widthConstraint?.isActive = false
if newValue != nil {
widthConstraint = self.widthAnchor.constraint(equalTo: self.heightAnchor, multiplier: newValue.size.width / newValue.size.height)
widthConstraint?.isActive = true
}
}
}
private var widthConstraint: NSLayoutConstraint?
}
英文:
Using DonMag's answer I came up with my own drop in solution. I haven't test edge cases or anything, but it works for my use. I set a fixed height and the content mode to aspect fit in interface builder
@IBDesignable
@objc class UIVariableWidthImageView : UIImageView {
override var image: UIImage! {
get {
return super.image
}
set {
super.image = newValue
widthConstraint?.isActive = false
if newValue != nil {
widthConstraint = self.widthAnchor.constraint(equalTo: self.heightAnchor, multiplier: newValue.size.width / newValue.size.height)
widthConstraint?.isActive = true
}
}
}
private var widthConstraint: NSLayoutConstraint?
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论