Signal-iOS/SignalUI/ViewControllers/AttachmentApproval/ExpandableContactListView.swift
Igor Solomennikov cb4a9bb9e5 Minor UI fix for the review screen in media editor.
Long recipient name in the top right corner would not be faded at the
end upon initial presentation.
This was happening because subviews did not have their final frames at
the end of `layoutSubviews`.
The fix is to call method that updates fading on next run loop run.
2022-07-14 12:09:35 -07:00

197 lines
7.4 KiB
Swift

//
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
//
import UIKit
class ExpandableContactListView: UIView {
var contactNames: [String] = [] {
didSet {
guard #available(iOS 13, *) else {
textLabel.text = contactNames.joined(separator: ", ")
return
}
textLabel.text = ListFormatter().string(from: contactNames)
}
}
var expanded: Bool = false {
didSet {
scrollView.isScrollEnabled = expanded
scrollViewMaxWidthConstraint?.isActive = !expanded
if !expanded {
scrollView.contentOffset = .zero
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
textLabel.textColor = tintColor
let pillView = PillView()
pillView.layoutMargins = UIEdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 0)
pillView.autoSetDimension(.height, toSize: RoundMediaButton.visibleButtonSize)
addSubview(pillView)
pillView.autoPinEdgesToSuperviewEdges()
let backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
pillView.addSubview(backgroundView)
backgroundView.autoPinEdgesToSuperviewEdges()
let arrowView = UIImageView(image: #imageLiteral(resourceName: "arrow-right-16"))
pillView.addSubview(arrowView)
arrowView.autoPinEdge(toSuperviewMargin: .leading)
arrowView.autoVCenterInSuperview()
scrollViewContainer.clipsToBounds = true
scrollViewContainer.layoutMargins = UIEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: ExpandableContactListView.gradientWidth)
pillView.addSubview(scrollViewContainer)
scrollViewContainer.autoPinEdges(toSuperviewMarginsExcludingEdge: .leading)
scrollViewContainer.leadingAnchor.constraint(equalTo: arrowView.trailingAnchor, constant: 4).isActive = true
scrollView.delegate = self
scrollView.clipsToBounds = false
scrollView.isScrollEnabled = expanded
scrollViewContainer.addSubview(scrollView)
scrollView.autoPinEdgesToSuperviewMargins()
scrollView.addSubview(textLabel)
textLabel.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor).isActive = true
textLabel.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor).isActive = true
textLabel.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor).isActive = true
textLabel.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor).isActive = true
scrollView.heightAnchor.constraint(equalTo: textLabel.heightAnchor).isActive = true
// This constraint sets intrinsic content width on the scroll view.
addConstraint({
let constraint = scrollView.widthAnchor.constraint(equalTo: textLabel.widthAnchor)
constraint.priority = .defaultLow
return constraint
}())
// Limit scroll view width in expanded state to 128 pts.
let scrollViewMaxWidthConstraint = scrollViewContainer.widthAnchor.constraint(lessThanOrEqualToConstant: 128)
if !expanded {
addConstraint(scrollViewMaxWidthConstraint)
}
self.scrollViewMaxWidthConstraint = scrollViewMaxWidthConstraint
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleSingleTap(gestureRecognizer:))))
}
@available(*, unavailable, message: "Use init(frame:)")
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func tintColorDidChange() {
super.tintColorDidChange()
textLabel.textColor = tintColor
}
override func layoutSubviews() {
super.layoutSubviews()
DispatchQueue.main.async {
self.updateTextLabelEdgesFading()
}
}
// MARK: - Layout
private let scrollViewContainer = UIView()
private let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
scrollView.alwaysBounceHorizontal = false
return scrollView
}()
private let textLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 1
label.lineBreakMode = .byClipping
label.font = .ows_dynamicTypeBody2Clamped
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private var scrollViewMaxWidthConstraint: NSLayoutConstraint?
static private let gradientWidth: CGFloat = 14
private var isLeadingEdgeFaded = false
private var isTrailingEdgeFaded = false
private func updateTextLabelEdgesFading() {
// This method would be called in a tight loop when users scrolls.
// Therefore only re-create mask layer if it is necessary.
let shouldFadeLeading = scrollView.contentOffset.x > 0
let shouldFadeTrailing = scrollView.contentOffset.x < scrollView.contentSize.width - scrollView.frame.width
var shouldUpdateLayerMask = shouldFadeLeading != isLeadingEdgeFaded || shouldFadeTrailing != isTrailingEdgeFaded
// Mask layer doesn't resize automatically and therefore width change
// (switching to/from expanded state) mandates mask update.
if !shouldUpdateLayerMask, let maskLayer = scrollViewContainer.layer.mask {
shouldUpdateLayerMask = maskLayer.bounds.width != scrollViewContainer.width
}
guard shouldUpdateLayerMask else {
return
}
isLeadingEdgeFaded = shouldFadeLeading
isTrailingEdgeFaded = shouldFadeTrailing
// Simplest case: no edge fading - no mask layer.
guard isLeadingEdgeFaded || isTrailingEdgeFaded else {
scrollViewContainer.layer.mask = nil
return
}
let gradientWidthInPercent = Self.gradientWidth / scrollViewContainer.width
let gradientStopLocations: [CGFloat] = [ 0, gradientWidthInPercent, 1-gradientWidthInPercent, 1 ]
var gradientColors: [UIColor] = [ .black, .black ]
gradientColors.insert(isLeadingEdgeFaded ? .clear : .black, at: 0)
gradientColors.append(isTrailingEdgeFaded ? .clear : .black)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = scrollViewContainer.bounds
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
gradientLayer.colors = gradientColors.map { $0.cgColor }
gradientLayer.locations = gradientStopLocations.map { NSNumber(value: $0) }
scrollViewContainer.layer.mask = gradientLayer
}
}
extension ExpandableContactListView {
@objc
private func handleSingleTap(gestureRecognizer: UITapGestureRecognizer) {
expanded = !expanded
UIView.animate(withDuration: 0.3,
animations: {
self.superview?.setNeedsLayout()
self.superview?.layoutIfNeeded()
if self.expanded {
self.updateTextLabelEdgesFading()
}
},
completion: { _ in
self.updateTextLabelEdgesFading()
})
}
}
extension ExpandableContactListView: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateTextLabelEdgesFading()
}
}