从 UIImageView 中删除多余的 space

Remove extra space from UIImageView

在我的 UIView 中,我有一种方法可以从 base64(来自 API)加载图像并在 UIImageView 中配置它。可以想象,API 返回了不同大小的图像。我正在使用具有本机约束(以编程方式)和 UIImageView contentMode = .scaleAspectFit 的自动布局。某些图像在 UIImageView 中有额外的 space。这里有一些截图(背景是青色的,所以我可以看到我想删除的额外 spaces)。

我的约束:

assetImageView.topAnchor.constraint(equalTo: typeOrderLabel.bottomAnchor, constant: 14.7),
assetImageView.leadingAnchor.constraint(equalTo: typeOrderLabel.leadingAnchor),
 
lazy var assetImageView: UIImageView = {
     let imageView = UIImageView()
     imageView.translatesAutoresizingMaskIntoConstraints = false
     imageView.contentMode = .scaleAspectFit
     imageView.backgroundColor = .cyan
     return imageView
}()

如果您希望图像视图的最大尺寸为 100 x 30,您需要在单元格中创建宽度和高度约束 class:

var cWidth: NSLayoutConstraint!
var cHeight: NSLayoutConstraint!

然后在您单元格的 init 中初始化它们:

// initialize image view Width and Height constraints
cWidth = assetImageView.widthAnchor.constraint(equalToConstant: 0)
cHeight = assetImageView.heightAnchor.constraint(equalToConstant: 0)

NSLayoutConstraint.activate([
        
    // label constraints...

    assetImageView.topAnchor.constraint(equalTo: typeOrderLabel.bottomAnchor, constant: 15.0),
    assetImageView.leadingAnchor.constraint(equalTo: typeOrderLabel.leadingAnchor),

    // activate image view Width and Height constraints
    cWidth,
    cHeight,
        
])

然后,当您在 cellForRowAt 中设置图像时,根据图像的大小计算大小并更新 Width 和 Height 约束常量:

assetImageView.image = img
    
if img.size.height <= maxHeight && img.size.width <= maxWidth {
        
    // image height and width are smaller than max Height and Width
    //  so use actual size
    cWidth.constant = img.size.width
    cHeight.constant = img.size.height
        
} else {
        
    // use standard Aspect Fit calculation
    var f = maxWidth / img.size.width
    var w = img.size.width * f
    var h = img.size.height * f
    if h > maxHeight {
        f = maxHeight / img.size.height
        h = img.size.height * f
        w = img.size.width * f
    }
        
    // update constraints
    cWidth.constant = w
    cHeight.constant = h
        
}

这是一个完整的例子:

struct EduardoStruct {
    var typeOrder: String = ""
    var other: String = ""
    var asset: String = ""
}

class EduardoCell: UITableViewCell {
    
    let typeOrderLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.font = .systemFont(ofSize: 14, weight: .bold)
        v.textColor = .systemGreen
        // if we want to see the label frame
        //v.backgroundColor = .yellow
        return v
    }()
    let assetImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.contentMode = .scaleToFill
        imageView.backgroundColor = .cyan
        return imageView
    }()
    let otherLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.font = .systemFont(ofSize: 13, weight: .thin)
        v.textColor = .darkGray
        // if we want to see the label frame
        //v.backgroundColor = .yellow
        return v
    }()
    
    var cWidth: NSLayoutConstraint!
    var cHeight: NSLayoutConstraint!
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        // add the subviews
        contentView.addSubview(typeOrderLabel)
        contentView.addSubview(assetImageView)
        contentView.addSubview(otherLabel)
        
        // initialize image view Width and Height constraints
        cWidth = assetImageView.widthAnchor.constraint(equalToConstant: 0)
        cHeight = assetImageView.heightAnchor.constraint(equalToConstant: 0)
        
        let g = contentView.layoutMarginsGuide
        NSLayoutConstraint.activate([
            
            typeOrderLabel.topAnchor.constraint(equalTo: g.topAnchor),
            typeOrderLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            typeOrderLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            
            assetImageView.topAnchor.constraint(equalTo: typeOrderLabel.bottomAnchor, constant: 15.0),
            assetImageView.leadingAnchor.constraint(equalTo: typeOrderLabel.leadingAnchor),
            
            otherLabel.topAnchor.constraint(equalTo: assetImageView.bottomAnchor, constant: 10.0),
            otherLabel.leadingAnchor.constraint(equalTo: typeOrderLabel.leadingAnchor),
            otherLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            otherLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            
            // activate image view Width and Height constraints
            cWidth,
            cHeight,
            
        ])
    }
    
    func fillData(_ st: EduardoStruct) {
        typeOrderLabel.text = st.typeOrder
        otherLabel.text = st.other
        
        // image max Width and Height
        let maxWidth: CGFloat = 100
        let maxHeight: CGFloat = 30
        
        guard let img = UIImage(named: st.asset) else {
            // if we can't load the image, set the image view
            //  to maxWidth x maxHeight
            cWidth.constant = maxWidth
            cHeight.constant = maxHeight
            assetImageView.image = nil
            return
        }
        
        assetImageView.image = img
        
        if img.size.height <= maxHeight && img.size.width <= maxWidth {
            
            // image height and width are smaller than max Height and Width
            //  so use actual size
            cWidth.constant = img.size.width
            cHeight.constant = img.size.height
            
        } else {
            
            // use standard Aspect Fit calculation
            var f = maxWidth / img.size.width
            var w = img.size.width * f
            var h = img.size.height * f
            if h > maxHeight {
                f = maxHeight / img.size.height
                h = img.size.height * f
                w = img.size.width * f
            }
            
            // update constraints
            cWidth.constant = w
            cHeight.constant = h
            
        }
    }
}

class EduardoExampleTableViewController: UITableViewController {
    
    var myData: [EduardoStruct] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let imageNames: [String] = [
            "img100x30", "img80x20", "img120x50", "img80x80", "img150x30", "img120x120",
        ]
        
        for (i, str) in imageNames.enumerated() {
            var st: EduardoStruct = EduardoStruct()
            st.typeOrder = "TYPE \(i)"
            st.other = "OTHER \(i)"
            st.asset = str
            myData.append(st)
        }
        
        tableView.register(EduardoCell.self, forCellReuseIdentifier: "cell")
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return myData.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! EduardoCell
        cell.fillData(myData[indexPath.row])
        return cell
    }
    
}

使用该代码,使用这些“资产图像”:

我们得到这个结果:

如您所见,所有图像都是“纵横比”缩放到最大尺寸 100 x 30,没有“额外 space”。“=30=]


编辑

在 OP 澄清后,图像将是 160 x 55 像素,目标是:

  • 仅提取图像的“有用”部分(non-alpha 部分)
  • 100 x 30 的最大尺寸显示该部分,同时保持宽高比。

因此,有了一组新图像,每张图像都 160 x 55 具有透明“背景”(下载这些图像以查看):

我们可以使用这个 UIImage 扩展来“剪掉” non-transparent 部分:

extension UIImage {
    
    func clipAlpha(_ tolerancePercent: Double) -> UIImage {
    
        guard let imageRef = self.cgImage else {
            return self
        }
        
        let columns = imageRef.width
        let rows = imageRef.height
        
        let bytesPerPixel = 4
        let bytesPerRow = bytesPerPixel * columns
        let bitmapByteCount = bytesPerRow * rows
        
        // allocate memory
        let rawData = UnsafeMutablePointer<UInt8>.allocate(capacity: bitmapByteCount)
        // initialize buffer to Zeroes
        rawData.initialize(repeating: 0, count: bitmapByteCount)
        defer {
            rawData.deallocate()
        }
        
        guard let colorSpace = CGColorSpace(name: CGColorSpace.genericRGBLinear) else {
            return self
        }
        
        guard let context = CGContext(
            data: rawData,
            width: columns,
            height: rows,
            bitsPerComponent: 8,
            bytesPerRow: bytesPerRow,
            space: colorSpace,
            bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
            | CGBitmapInfo.byteOrder32Big.rawValue
        ) else {
            return self
        }

        var l: Int = -1
        var r: Int = -1
        var t: Int = -1
        var b: Int = -1
        
        // for debugging...
        //  used to count the number of iterations needed
        //  to find the non-alpha bounding box
        //var c: Int = 0
        
        var colOffset: Int = 0
        var rowOffset: Int = 0
        
        // Draw source image on created context.
        let rc = CGRect(x: 0, y: 0, width: columns, height: rows)
        context.draw(imageRef, in: rc)
        
        let tolerance: Int = Int(255.0 * tolerancePercent)

        // find the left-most non-alpha pixel
        for col in 0..<columns {
            colOffset = col * bytesPerPixel
            for row in 0..<rows {
                // debugging
                //c += 1
                rowOffset = row * bytesPerRow
                // Get alpha of current pixel
                let alpha = CGFloat(rawData[colOffset + rowOffset + 3])
                if alpha > CGFloat(tolerance) {
                    l = col
                    break
                }
            }
            if l > -1 {
                break
            }
        }
        
        // find the right-most non-alpha pixel
        for col in stride(from: columns - 1, to: l, by: -1) {
            colOffset = col * bytesPerPixel
            for row in 0..<rows {
                // debugging
                //c += 1
                rowOffset = row * bytesPerRow
                // Get alpha of current pixel
                let alpha = CGFloat(rawData[colOffset + rowOffset + 3])
                if alpha > CGFloat(tolerance) {
                    r = col
                    break
                }
            }
            if r > -1 {
                break
            }
        }
        
        // find the top-most non-alpha pixel
        for row in 0..<rows {
            rowOffset = row * bytesPerRow
            for col in l..<r {
                // debugging
                //c += 1
                colOffset = col * bytesPerPixel
                // Get alpha of current pixel
                let alpha = CGFloat(rawData[colOffset + rowOffset + 3])
                if alpha > CGFloat(tolerance) {
                    t = row
                    break
                }
            }
            if t > -1 {
                break
            }
        }
        
        // find the bottom-most non-alpha pixel
        for row in stride(from: rows - 1, to: t, by: -1) {
            rowOffset = row * bytesPerRow
            for col in l..<r {
                // debugging
                //c += 1
                colOffset = col * bytesPerPixel
                // Get alpha of current pixel
                let alpha = CGFloat(rawData[colOffset + rowOffset + 3])
                if alpha > CGFloat(tolerance) {
                    b = row
                    break
                }
            }
            if b > -1 {
                break
            }
        }
        
        // debugging
        //print(c, l, t, r, b)

        // define a rectangle for the non-alpha pixels
        let targetRect = CGRect(x: l, y: t, width: r - l + 1, height: b - t + 1)
        let size = targetRect.size
        let renderer = UIGraphicsImageRenderer(size: size)
        
        let renderedImage = renderer.image { _ in
            // render the non-alpha portion
            self.draw(at: CGPoint(x: -targetRect.origin.x, y: -targetRect.origin.y))
        }
        
        return renderedImage

    }

}

然后,我们将首先“剪辑”它,然后应用前面的 aspect-sizing 代码,而不是直接使用图像。

这是完整的示例(仅对原始帖子稍作修改):

struct EduardoStruct {
    var typeOrder: String = ""
    var other: String = ""
    var asset: String = ""
}

class EduardoCell: UITableViewCell {
    
    let typeOrderLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.font = .systemFont(ofSize: 14, weight: .bold)
        v.textColor = .systemGreen
        // if we want to see the label frame
        //v.backgroundColor = .yellow
        return v
    }()
    let assetImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.contentMode = .scaleToFill
        imageView.backgroundColor = .cyan
        return imageView
    }()
    let otherLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.font = .systemFont(ofSize: 13, weight: .thin)
        v.textColor = .darkGray
        // if we want to see the label frame
        //v.backgroundColor = .yellow
        return v
    }()
    
    var cWidth: NSLayoutConstraint!
    var cHeight: NSLayoutConstraint!
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        // add the subviews
        contentView.addSubview(typeOrderLabel)
        contentView.addSubview(assetImageView)
        contentView.addSubview(otherLabel)
        
        // initialize image view Width and Height constraints
        cWidth = assetImageView.widthAnchor.constraint(equalToConstant: 0)
        cHeight = assetImageView.heightAnchor.constraint(equalToConstant: 0)
        
        let g = contentView.layoutMarginsGuide
        NSLayoutConstraint.activate([
            
            typeOrderLabel.topAnchor.constraint(equalTo: g.topAnchor),
            typeOrderLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            typeOrderLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            
            assetImageView.topAnchor.constraint(equalTo: typeOrderLabel.bottomAnchor, constant: 15.0),
            assetImageView.leadingAnchor.constraint(equalTo: typeOrderLabel.leadingAnchor),
            
            otherLabel.topAnchor.constraint(equalTo: assetImageView.bottomAnchor, constant: 10.0),
            otherLabel.leadingAnchor.constraint(equalTo: typeOrderLabel.leadingAnchor),
            otherLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            otherLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            
            // activate image view Width and Height constraints
            cWidth,
            cHeight,
            
        ])
    }
    
    func fillData(_ st: EduardoStruct, showBKG: Bool) {
        typeOrderLabel.text = st.typeOrder
        otherLabel.text = st.other
        
        // image max Width and Height
        let maxWidth: CGFloat = 100
        let maxHeight: CGFloat = 30
        
        assetImageView.backgroundColor = showBKG ? .cyan : .clear
        
        guard let origImg = UIImage(named: st.asset) else {
            // if we can't load the image, set the image view
            //  to maxWidth x maxHeight
            cWidth.constant = maxWidth
            cHeight.constant = maxHeight
            assetImageView.image = nil
            return
        }
        
        let img = origImg.clipAlpha(0.0)
        
        assetImageView.image = img
        
        if img.size.height <= maxHeight && img.size.width <= maxWidth {
            
            // image height and width are smaller than max Height and Width
            //  so use actual size
            cWidth.constant = img.size.width
            cHeight.constant = img.size.height
            
        } else {
            
            // use standard Aspect Fit calculation
            var f = maxWidth / img.size.width
            var w = img.size.width * f
            var h = img.size.height * f
            if h > maxHeight {
                f = maxHeight / img.size.height
                h = img.size.height * f
                w = img.size.width * f
            }
            
            // update constraints
            cWidth.constant = w
            cHeight.constant = h
            
        }
    }
}

class TestSizingCellTableViewController: UITableViewController {
    
    var myData: [EduardoStruct] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let imageNames: [String] = [
            "img1", "img2", "img3", "img4", "img5",
        ]
        
        for (i, str) in imageNames.enumerated() {
            var st: EduardoStruct = EduardoStruct()
            st.typeOrder = "TYPE \(i)"
            st.other = "OTHER \(i)"
            st.asset = str
            myData.append(st)
        }
        
        tableView.register(EduardoCell.self, forCellReuseIdentifier: "cell")
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return myData.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! EduardoCell
        
        // set cell's imageView background to cyan or clear
        let showBKG = false
        
        cell.fillData(myData[indexPath.row], showBKG: showBKG)
        
        return cell
    }
    
}

以及输出——首先将 imageView 背景设置为 .cyan(以便我们可以看到实际帧):

并将 imageView 背景设置为 .clear: