高效的离屏UIView渲染和镜像

9
我有一个“屏幕外”UIView层次结构,我想在屏幕的不同位置渲染它。此外,应该可以只显示此视图层次结构的部分,并且应该反映对此层次结构所做的所有更改。
困难:
1. 如果要合并视图层次结构中所有更改,则UIView方法drawHierarchy(in:afterScreenUpdates :)始终调用draw(_ rect :) ,因此对于大型层次结构非常低效。您必须在每个屏幕更新时重新绘制它或观察所有视图的所有更改属性。 Draw view hierarchy documentation 2. 如果此层次结构处于“屏幕外”,则UIView方法snapshotView(afterScreenUpdates :)也没有帮助。Snapshot view documentation
“屏幕外”:此视图层次结构的根视图不是应用程序的UI的一部分。它没有superview。
以下是我想法的视觉呈现:

example


@E.Coms 不使用布局约束怎么样? - Qbyte
你可以自己发明另一个简单的视图类。尽可能使用CGLAYER。通常你只需要一个UIView和许多绘图函数。只是不知道绘图有多复杂。 - E.Coms
实际上,您可以多次使用 drawHierarchy(in:afterScreenUpdates:) 来收集静态图像以生成 CGLAYERS。 使用字典从底部到顶部构建层。 假设只有一个图层是动态的。 要组合这样的视图,只需绘制三个图层 :(静态底部,动态图层,静态顶部),这比调用 drawHierarchy(in:afterScreenUpdates:) 快得多。 希望您能理解。 - E.Coms
@E.Coms 我该如何同步视图的动态属性呢?在上面的例子中,“按钮”以某种方式更改了“受控元素”。我需要观察这一点,然后绘制视图层次结构,如果有什么变化。但是,我将不得不观察所有属性,这非常低效,我认为。(我该怎么做呢?) - Qbyte
这里有一个类似的想法。它很难想,但很容易实现,因为它只是遍历树节点。每个节点都有一个缓存层。您可以确定是否需要绘制或更新它。如果您不需要更新或绘制它,可以跳过许多节点。无论如何,如果UIKit没有提供这样的功能,您必须做一些事情来加速。 - E.Coms
显示剩余7条评论
2个回答

0

这是我如何处理它的方法。首先,我会复制您想要复制的视图。我为此编写了一个小扩展程序:

extension UIView {
    func duplicate<T: UIView>() -> T {
        return NSKeyedUnarchiver.unarchiveObject(with: NSKeyedArchiver.archivedData(withRootObject: self)) as! T
    }

    func copyProperties(fromView: UIView, recursive: Bool = true) {
        contentMode = fromView.contentMode
        tag = fromView.tag

        backgroundColor = fromView.backgroundColor
        tintColor = fromView.tintColor

        layer.cornerRadius = fromView.layer.cornerRadius
        layer.maskedCorners = fromView.layer.maskedCorners

        layer.borderColor = fromView.layer.borderColor
        layer.borderWidth = fromView.layer.borderWidth

        layer.shadowOpacity = fromView.layer.shadowOpacity
        layer.shadowRadius = fromView.layer.shadowRadius
        layer.shadowPath = fromView.layer.shadowPath
        layer.shadowColor = fromView.layer.shadowColor
        layer.shadowOffset = fromView.layer.shadowOffset

        clipsToBounds = fromView.clipsToBounds
        layer.masksToBounds = fromView.layer.masksToBounds
        mask = fromView.mask
        layer.mask = fromView.layer.mask

        alpha = fromView.alpha
        isHidden = fromView.isHidden
        if let gradientLayer = layer as? CAGradientLayer, let fromGradientLayer = fromView.layer as? CAGradientLayer {
            gradientLayer.colors = fromGradientLayer.colors
            gradientLayer.startPoint = fromGradientLayer.startPoint
            gradientLayer.endPoint = fromGradientLayer.endPoint
            gradientLayer.locations = fromGradientLayer.locations
            gradientLayer.type = fromGradientLayer.type
        }

        if let imgView = self as? UIImageView, let fromImgView = fromView as? UIImageView {
            imgView.tintColor = .clear
            imgView.image = fromImgView.image?.withRenderingMode(fromImgView.image?.renderingMode ?? .automatic)
            imgView.tintColor = fromImgView.tintColor
        }

        if let btn = self as? UIButton, let fromBtn = fromView as? UIButton {
            btn.setImage(fromBtn.image(for: fromBtn.state), for: fromBtn.state)
        }

        if let textField = self as? UITextField, let fromTextField = fromView as? UITextField {
            if let leftView = fromTextField.leftView {
                textField.leftView = leftView.duplicate()
                textField.leftView?.copyProperties(fromView: leftView)
            }

            if let rightView = fromTextField.rightView {
                textField.rightView = rightView.duplicate()
                textField.rightView?.copyProperties(fromView: rightView)
            }

            textField.attributedText = fromTextField.attributedText
            textField.attributedPlaceholder = fromTextField.attributedPlaceholder
        }

        if let lbl = self as? UILabel, let fromLbl = fromView as? UILabel {
            lbl.attributedText = fromLbl.attributedText
            lbl.textAlignment = fromLbl.textAlignment
            lbl.font = fromLbl.font
            lbl.bounds = fromLbl.bounds
        }

       if recursive {
            for (i, view) in subviews.enumerated() {
                if i >= fromView.subviews.count {
                    break
                }

                view.copyProperties(fromView: fromView.subviews[i])
            }
        }
    }
}

使用这个扩展,只需要做以下操作

let duplicateView = originalView.duplicate()
duplicateView.copyProperties(fromView: originalView)
parentView.addSubview(duplicateView)

然后我会掩盖重复的视图,只获取您想要的特定部分

let mask = UIView(frame: CGRect(x: 0, y: 0, width: yourNewWidth, height: yourNewHeight))
mask.backgroundColor = .black
duplicateView.mask = mask

最后,使用CGAffineTransform将其缩放为所需的任何尺寸。
duplicateView.transform = CGAffineTransform(scaleX: xScale, y: yScale)

copyProperties函数应该能够正常工作,但如果需要从一个视图复制更多的内容到另一个视图中,则可以进行更改。

祝你好运,让我知道进展如何 :)


观察复制的原始视图的更改怎么样?例如,更改背景颜色或视图层次结构? - Qbyte
您可以将观察者添加到原始视图中,并相应地更改副本视图的背景颜色。我会在副本视图上执行与原始视图相同的UI更改。我知道这听起来不是很好,但不幸的是,我认为没有更简单的方法。您还可以向各种子视图添加标记,以便您知道要更改的内容,例如将textfield的标记设置为62,然后编辑具有标记62的副本视图中的视图。 - CentrumGuy

0

我会复制我想要显示的内容并按照自己的需求进行裁剪。

假设我有一个名为ContentViewController的视图控制器,它包含了我想要复制的视图层次结构。我会将所有可以对该层次结构进行更改的操作封装在一个ContentViewModel中。就像这样:

struct ContentViewModel {
    let actionTitle: String?
    let contentMessage: String?
    // ...
}

class ContentViewController: UIViewController {
    func display(_ viewModel: ContentViewModel) { /* ... */ }
}

使用 ClippingView (或简单的 UIScrollView):

class ClippingView: UIView {

    var contentOffset: CGPoint = .zero // a way to specify the part of the view you wish to display
    var contentFrame: CGRect = .zero // the actual size of the clipped view

    var clippedView: UIView?

    override init(frame: CGRect) {
        super.init(frame: frame)
        clipsToBounds = true
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        clippedView?.frame = contentFrame
        clippedView?.frame.origin = contentOffset
    }
}

在一个视图控制器容器中,我会裁剪我的每个内容实例并在每次发生更改时更新它们:

class ContainerViewController: UIViewController {

    let contentViewControllers: [ContentViewController] = // 3 in your case

    override func viewDidLoad() {
        super.viewDidLoad()
        contentViewControllers.forEach { viewController in
             addChil(viewController)
             let clippingView = ClippingView()
             clippingView.clippedView = viewController.view
             clippingView.contentOffset = // ...
             viewController.didMove(to: self)
        }
    }

    func somethingChange() {
        let newViewModel = ContentViewModel(...)
        contentViewControllers.forEach { $0.display(newViewModel) }
    }
} 

这种情况在你的情况下可行吗?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接