TM-SGNL-iOS/SignalUI/AttachmentApproval/ExpandableContactListView.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

201 lines
7.5 KiB
Swift

//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import UIKit
class ExpandableContactListView: UIView {
private class var listFormatter: ListFormatter {
let formatter = ListFormatter()
if let identifier = NSLocale.preferredLanguages.first {
formatter.locale = Locale(identifier: identifier)
}
return formatter
}
var contactNames: [String] = [] {
didSet {
textLabel.text = Self.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: UIImage(imageLiteralResourceName: "arrow-up-compact"))
pillView.addSubview(arrowView)
arrowView.autoPinEdge(toSuperviewMargin: .leading, withInset: 2)
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 = .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()
}
}