Use ListItemSelectionIndicatorView in All Media view.
Added functionality to show selection indicator with a white outline - to be displayed on top of media. Rename ListItemSelectionIndicatorView to SelectionIndicatorView.
This commit is contained in:
parent
868ed6bb4b
commit
b5838a1afc
@ -504,7 +504,6 @@
|
||||
45069FC629D3A7C800D0DD14 /* WideMediaTileViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45069FC529D3A7C800D0DD14 /* WideMediaTileViewLayout.swift */; };
|
||||
45069FC829D3A7E700D0DD14 /* SquareMediaTileViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45069FC729D3A7E700D0DD14 /* SquareMediaTileViewLayout.swift */; };
|
||||
45069FCA29D4FFBB00D0DD14 /* MediaTileDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45069FC929D4FFBB00D0DD14 /* MediaTileDateFormatter.swift */; };
|
||||
45069FCE29D64CB300D0DD14 /* MediaSelectionIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45069FCD29D64CB300D0DD14 /* MediaSelectionIndicatorView.swift */; };
|
||||
450B0FC929FB301700B9A458 /* AudioMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450B0FC829FB301700B9A458 /* AudioMessageView.swift */; };
|
||||
45161BA928A2E54B0055AB45 /* ThreadReplyInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45161BA828A2E54B0055AB45 /* ThreadReplyInfo.swift */; };
|
||||
4520D8D51D417D8E00123472 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4520D8D41D417D8E00123472 /* Photos.framework */; };
|
||||
@ -1518,7 +1517,7 @@
|
||||
768FC7B42F316CF800707F72 /* CVDimmableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 768FC7B32F316CEF00707F72 /* CVDimmableView.swift */; };
|
||||
768FC7B62F32A0B000707F72 /* UIImage+Blur.swift in Sources */ = {isa = PBXBuildFile; fileRef = 768FC7B52F32A0AB00707F72 /* UIImage+Blur.swift */; };
|
||||
7694C9172FB51AA4006E727F /* DisappearingMessagesChatIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7694C9162FB51AA4006E727F /* DisappearingMessagesChatIndicatorView.swift */; };
|
||||
76964AFB2FB4035D007130BE /* ListItemSelectionIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76964AFA2FB4034C007130BE /* ListItemSelectionIndicatorView.swift */; };
|
||||
76964AFB2FB4035D007130BE /* SelectionIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76964AFA2FB4034C007130BE /* SelectionIndicatorView.swift */; };
|
||||
76995F15283868BD009DD4F4 /* ImageEditorViewController+StrokeWidthSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76995F14283868BD009DD4F4 /* ImageEditorViewController+StrokeWidthSlider.swift */; };
|
||||
76A066EA2EA99D1F009B3ED5 /* ConversationInputToolbar+VoiceMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76A066E92EA99D0E009B3ED5 /* ConversationInputToolbar+VoiceMemo.swift */; };
|
||||
76A2EB1028B578B800A29C24 /* MediaTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76A2EB0F28B578B800A29C24 /* MediaTextView.swift */; };
|
||||
@ -4717,7 +4716,6 @@
|
||||
45069FC529D3A7C800D0DD14 /* WideMediaTileViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideMediaTileViewLayout.swift; sourceTree = "<group>"; };
|
||||
45069FC729D3A7E700D0DD14 /* SquareMediaTileViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SquareMediaTileViewLayout.swift; sourceTree = "<group>"; };
|
||||
45069FC929D4FFBB00D0DD14 /* MediaTileDateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaTileDateFormatter.swift; sourceTree = "<group>"; };
|
||||
45069FCD29D64CB300D0DD14 /* MediaSelectionIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSelectionIndicatorView.swift; sourceTree = "<group>"; };
|
||||
450B0FC829FB301700B9A458 /* AudioMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMessageView.swift; sourceTree = "<group>"; };
|
||||
45161BA828A2E54B0055AB45 /* ThreadReplyInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadReplyInfo.swift; sourceTree = "<group>"; };
|
||||
451764291DE939FD00EDB8B9 /* ContactCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactCell.swift; sourceTree = "<group>"; };
|
||||
@ -5720,7 +5718,7 @@
|
||||
768FC7B32F316CEF00707F72 /* CVDimmableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CVDimmableView.swift; sourceTree = "<group>"; };
|
||||
768FC7B52F32A0AB00707F72 /* UIImage+Blur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Blur.swift"; sourceTree = "<group>"; };
|
||||
7694C9162FB51AA4006E727F /* DisappearingMessagesChatIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisappearingMessagesChatIndicatorView.swift; sourceTree = "<group>"; };
|
||||
76964AFA2FB4034C007130BE /* ListItemSelectionIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListItemSelectionIndicatorView.swift; sourceTree = "<group>"; };
|
||||
76964AFA2FB4034C007130BE /* SelectionIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionIndicatorView.swift; sourceTree = "<group>"; };
|
||||
76995F14283868BD009DD4F4 /* ImageEditorViewController+StrokeWidthSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImageEditorViewController+StrokeWidthSlider.swift"; sourceTree = "<group>"; };
|
||||
76A066E92EA99D0E009B3ED5 /* ConversationInputToolbar+VoiceMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationInputToolbar+VoiceMemo.swift"; sourceTree = "<group>"; };
|
||||
76A2EB0F28B578B800A29C24 /* MediaTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaTextView.swift; sourceTree = "<group>"; };
|
||||
@ -9139,7 +9137,6 @@
|
||||
34A95546271B510400B05242 /* GradientView.swift */,
|
||||
66F98DE52DBBED68009F1A86 /* LineWrappingStackView.swift */,
|
||||
34A95529271B510400B05242 /* LinkingTextView.swift */,
|
||||
76964AFA2FB4034C007130BE /* ListItemSelectionIndicatorView.swift */,
|
||||
34A9552F271B510400B05242 /* LoopingVideoView.swift */,
|
||||
34A9551D271B510400B05242 /* ManualLayoutView.swift */,
|
||||
34A9550C271B510400B05242 /* ManualStackView.swift */,
|
||||
@ -9160,6 +9157,7 @@
|
||||
45A6DAD51EBBF85500893231 /* ReminderView.swift */,
|
||||
763D7DDA27E155ED002EA7E6 /* RoundMediaButton.swift */,
|
||||
766BCA7C29FB049400046016 /* RTLEnabledCollectionViewFlowLayout.swift */,
|
||||
76964AFA2FB4034C007130BE /* SelectionIndicatorView.swift */,
|
||||
885275BF27E26775003F2F9B /* TextAttachmentView.swift */,
|
||||
764FE0402A2EF3A7004D2804 /* TextFieldFormatting.swift */,
|
||||
34A9553E271B510400B05242 /* TextViewWithPlaceholder.swift */,
|
||||
@ -11169,7 +11167,6 @@
|
||||
32AC5CE6255B51E900829BD8 /* JoinGroupCallPill.swift */,
|
||||
88A941982409A391000E9700 /* LottieToggleButton.swift */,
|
||||
45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */,
|
||||
45069FCD29D64CB300D0DD14 /* MediaSelectionIndicatorView.swift */,
|
||||
45069FC929D4FFBB00D0DD14 /* MediaTileDateFormatter.swift */,
|
||||
8829883A25B9FD6700DEE1E3 /* MockConversationView.swift */,
|
||||
3236FCC32592B67B006D33B9 /* NameCollisionReviewCell.swift */,
|
||||
@ -17857,7 +17854,6 @@
|
||||
5003BB3F299DA0F10037159B /* LinkPreviewFetchState.swift in Sources */,
|
||||
3402AA77271D9E180084CBAE /* LinkPreviewState.swift in Sources */,
|
||||
3402AA6D271D9E180084CBAE /* LinkPreviewView.swift in Sources */,
|
||||
76964AFB2FB4035D007130BE /* ListItemSelectionIndicatorView.swift in Sources */,
|
||||
3402AA8C271D9E180084CBAE /* LoopingVideoView.swift in Sources */,
|
||||
3402AA9C271D9E180084CBAE /* ManualLayoutView.swift in Sources */,
|
||||
3402AA8D271D9E180084CBAE /* ManualStackView.swift in Sources */,
|
||||
@ -17932,6 +17928,7 @@
|
||||
B9488E752CDED27200C1294B /* ScrollOffset.swift in Sources */,
|
||||
50597BBF2B97D629004681E1 /* SearchableNameFinder.swift in Sources */,
|
||||
66FC638E29EDABAC00F00DAC /* SearchDisplayConfigurations.swift in Sources */,
|
||||
76964AFB2FB4035D007130BE /* SelectionIndicatorView.swift in Sources */,
|
||||
66FBC4E328DA82AA00BD9E8B /* SelectMyStoryRecipientsViewController.swift in Sources */,
|
||||
504861AA2EEB4D0D00B13C49 /* SendableAttachment.swift in Sources */,
|
||||
D9791BB92EAA84830016AA5A /* SheetDisplayableError.swift in Sources */,
|
||||
@ -18537,7 +18534,6 @@
|
||||
76BB06FA29AD84DB00978856 /* MediaItemViewController.swift in Sources */,
|
||||
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */,
|
||||
4CD675C722E7D393008010D2 /* MediaPresentationContext.swift in Sources */,
|
||||
45069FCE29D64CB300D0DD14 /* MediaSelectionIndicatorView.swift in Sources */,
|
||||
45069FCA29D4FFBB00D0DD14 /* MediaTileDateFormatter.swift in Sources */,
|
||||
45D9784229F0B50000BBB3C0 /* MediaTileListModeCell.swift in Sources */,
|
||||
45CADA8B298DD2B4009EBDF5 /* MediaTileScrollFlag.swift in Sources */,
|
||||
|
||||
@ -29,7 +29,7 @@ class MediaTileListModeCell: UICollectionViewCell, MediaGalleryCollectionViewCel
|
||||
return view
|
||||
}()
|
||||
|
||||
let selectionButton = ListItemSelectionIndicatorView()
|
||||
let selectionButton = SelectionIndicatorView()
|
||||
|
||||
private let selectedMaskView = UIView()
|
||||
|
||||
|
||||
@ -1,98 +0,0 @@
|
||||
//
|
||||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import SignalUI
|
||||
import UIKit
|
||||
|
||||
/// A checkmark in a circle to indicate an item (typically in a table view or collection view) is
|
||||
/// selected.
|
||||
class MediaSelectionIndicatorView: UIView {
|
||||
private let outlineBadgeView: UIView = {
|
||||
let imageView = UIImageView(image: UIImage(imageLiteralResourceName: "circle"))
|
||||
imageView.contentMode = .center
|
||||
imageView.tintColor = .white
|
||||
imageView.isHidden = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private let selectedBadgeView: UIView = {
|
||||
let imageView = UIImageView(image: Theme.iconImage(.checkCircleFill))
|
||||
imageView.contentMode = .center
|
||||
imageView.tintColor = .ows_accentBlue
|
||||
|
||||
// This will give checkmark it's color.
|
||||
let backgroundView = CircleView(diameter: 18)
|
||||
backgroundView.backgroundColor = .white
|
||||
|
||||
let containerView = UIView(frame: imageView.bounds)
|
||||
containerView.isHidden = true
|
||||
|
||||
containerView.addSubview(backgroundView)
|
||||
backgroundView.autoCenterInSuperview()
|
||||
|
||||
containerView.addSubview(imageView)
|
||||
imageView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
return containerView
|
||||
}()
|
||||
|
||||
var isSelected: Bool = false {
|
||||
didSet {
|
||||
updateAppearance()
|
||||
}
|
||||
}
|
||||
|
||||
var allowsMultipleSelection: Bool = false {
|
||||
didSet {
|
||||
updateAppearance()
|
||||
}
|
||||
}
|
||||
|
||||
var outlineColor: UIColor = .white {
|
||||
didSet {
|
||||
outlineBadgeView.tintColor = outlineColor
|
||||
}
|
||||
}
|
||||
|
||||
var hidesOutlineWhenSelected: Bool = false {
|
||||
didSet {
|
||||
updateAppearance()
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
||||
addSubview(selectedBadgeView)
|
||||
selectedBadgeView.autoCenterInSuperview()
|
||||
|
||||
addSubview(outlineBadgeView)
|
||||
outlineBadgeView.autoCenterInSuperview()
|
||||
|
||||
autoSetDimensions(to: .square(24))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func updateAppearance() {
|
||||
if isSelected {
|
||||
outlineBadgeView.isHidden = hidesOutlineWhenSelected
|
||||
selectedBadgeView.isHidden = false
|
||||
} else if allowsMultipleSelection {
|
||||
outlineBadgeView.isHidden = false
|
||||
selectedBadgeView.isHidden = true
|
||||
} else {
|
||||
outlineBadgeView.isHidden = true
|
||||
selectedBadgeView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
func reset() {
|
||||
selectedBadgeView.isHidden = true
|
||||
outlineBadgeView.isHidden = true
|
||||
}
|
||||
}
|
||||
@ -69,7 +69,7 @@ class PhotoGridViewCell: UICollectionViewCell {
|
||||
|
||||
private var durationLabel: UILabel?
|
||||
private var durationLabelBackground: UIView?
|
||||
private let selectionButton = MediaSelectionIndicatorView()
|
||||
private let selectionIndicator = SelectionIndicatorView(style: .media)
|
||||
|
||||
private let highlightedMaskView: UIView
|
||||
private let selectedMaskView: UIView
|
||||
@ -117,14 +117,14 @@ class PhotoGridViewCell: UICollectionViewCell {
|
||||
contentView.addSubview(imageView)
|
||||
contentView.addSubview(highlightedMaskView)
|
||||
contentView.addSubview(selectedMaskView)
|
||||
contentView.addSubview(selectionButton)
|
||||
contentView.addSubview(selectionIndicator)
|
||||
|
||||
imageView.autoPinEdgesToSuperviewEdges()
|
||||
highlightedMaskView.autoPinEdgesToSuperviewEdges()
|
||||
selectedMaskView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
selectionButton.autoPinEdge(toSuperviewEdge: .trailing, withInset: 5)
|
||||
selectionButton.autoPinEdge(toSuperviewEdge: .top, withInset: 5)
|
||||
selectionIndicator.autoPinEdge(toSuperviewEdge: .trailing, withInset: 5)
|
||||
selectionIndicator.autoPinEdge(toSuperviewEdge: .top, withInset: 5)
|
||||
}
|
||||
|
||||
@available(*, unavailable, message: "Unimplemented")
|
||||
@ -134,8 +134,8 @@ class PhotoGridViewCell: UICollectionViewCell {
|
||||
|
||||
private func updateSelectionState() {
|
||||
selectedMaskView.isHidden = !isSelected
|
||||
selectionButton.isSelected = isSelected
|
||||
selectionButton.allowsMultipleSelection = allowsMultipleSelection
|
||||
selectionIndicator.isSelected = isSelected
|
||||
selectionIndicator.isHidden = allowsMultipleSelection == false
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
@ -279,7 +279,6 @@ class PhotoGridViewCell: UICollectionViewCell {
|
||||
durationLabelBackground?.isHidden = true
|
||||
highlightedMaskView.isHidden = true
|
||||
selectedMaskView.isHidden = true
|
||||
selectionButton.reset()
|
||||
}
|
||||
|
||||
func mediaPresentationContext(collectionView: UICollectionView, in coordinateSpace: UICoordinateSpace) -> MediaPresentationContext? {
|
||||
|
||||
@ -231,7 +231,7 @@ public class ContactShareViewController: OWSTableViewController2 {
|
||||
|
||||
let field: ContactShareField
|
||||
|
||||
private lazy var checkmark = ListItemSelectionIndicatorView()
|
||||
private lazy var checkmark = SelectionIndicatorView()
|
||||
|
||||
init(field: ContactShareField) {
|
||||
self.field = field
|
||||
@ -275,7 +275,7 @@ public class ContactShareViewController: OWSTableViewController2 {
|
||||
}
|
||||
|
||||
class func contactNameCell(for contactName: String) -> UITableViewCell {
|
||||
let checkmark = ListItemSelectionIndicatorView()
|
||||
let checkmark = SelectionIndicatorView()
|
||||
checkmark.isSelected = true
|
||||
checkmark.isEnabled = false
|
||||
|
||||
|
||||
@ -423,7 +423,7 @@ extension BaseMemberViewController: RecipientPickerDelegate {
|
||||
|
||||
let accessoryView: UIView
|
||||
if isPreExistingMember {
|
||||
let indicatorView = ListItemSelectionIndicatorView()
|
||||
let indicatorView = SelectionIndicatorView()
|
||||
indicatorView.isSelected = true
|
||||
indicatorView.isEnabled = false
|
||||
|
||||
@ -432,13 +432,13 @@ extension BaseMemberViewController: RecipientPickerDelegate {
|
||||
if let customIndicatorView = memberViewDelegate.memberViewCustomIndicatorForPickedMember(recipient) {
|
||||
accessoryView = customIndicatorView
|
||||
} else {
|
||||
let indicatorView = ListItemSelectionIndicatorView()
|
||||
let indicatorView = SelectionIndicatorView()
|
||||
indicatorView.isSelected = true
|
||||
|
||||
accessoryView = indicatorView
|
||||
}
|
||||
} else {
|
||||
accessoryView = ListItemSelectionIndicatorView()
|
||||
accessoryView = SelectionIndicatorView()
|
||||
}
|
||||
let accessoryViewWrapper = ManualLayoutView.wrapSubviewUsingIOSAutoLayout(accessoryView)
|
||||
return ContactCellAccessoryView(accessoryView: accessoryViewWrapper, size: .square(24))
|
||||
|
||||
@ -1353,7 +1353,7 @@ class ConversationPickerCell: ContactTableViewCell {
|
||||
|
||||
// MARK: - Subviews
|
||||
|
||||
private lazy var selectionView = ListItemSelectionIndicatorView()
|
||||
private lazy var selectionView = SelectionIndicatorView()
|
||||
|
||||
func buildAccessoryView(disappearingMessagesConfig: DisappearingMessagesConfigurationRecord?) -> ContactCellAccessoryView {
|
||||
|
||||
|
||||
@ -280,7 +280,7 @@ private class MyStorySettingsDataSource: NSObject {
|
||||
hStack.autoSetDimension(.height, toSize: 35, relation: .greaterThanOrEqual)
|
||||
hStack.autoPinHeightToSuperview(withMargin: 6)
|
||||
|
||||
let selectionIndicator = ListItemSelectionIndicatorView()
|
||||
let selectionIndicator = SelectionIndicatorView()
|
||||
selectionIndicator.isSelected = isSelected
|
||||
hStack.addArrangedSubview(selectionIndicator)
|
||||
|
||||
|
||||
@ -150,6 +150,14 @@ public class RingView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
override public var bounds: CGRect {
|
||||
didSet {
|
||||
if bounds.size != oldValue.size {
|
||||
updatePath()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public var tintColor: UIColor! {
|
||||
didSet {
|
||||
updateColor()
|
||||
|
||||
@ -1,113 +0,0 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import SignalServiceKit
|
||||
import UIKit
|
||||
|
||||
public class ListItemSelectionIndicatorView: UIView {
|
||||
|
||||
// MARK: UIView
|
||||
|
||||
override init(frame: CGRect = .init(origin: .zero, size: .square(ListItemSelectionIndicatorView.preferredSize))) {
|
||||
super.init(frame: frame)
|
||||
|
||||
directionalLayoutMargins = .init(margin: 1)
|
||||
|
||||
// Because it is often paired with UILabels, we want to make
|
||||
// this view as compact and as compression resistant as possible.
|
||||
setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||
setContentCompressionResistancePriority(.required - 10, for: .horizontal)
|
||||
setContentCompressionResistancePriority(.required - 10, for: .vertical)
|
||||
|
||||
sizeToFit()
|
||||
|
||||
addSubview(unselectedView)
|
||||
addSubview(selectedView)
|
||||
updateAppearance(animated: false)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: Layout
|
||||
|
||||
private static let preferredSize: CGFloat = 22
|
||||
|
||||
override public var intrinsicContentSize: CGSize {
|
||||
.square(Self.preferredSize) + directionalLayoutMargins.asSize
|
||||
}
|
||||
|
||||
override public func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
let circleRadius = Self.preferredSize / 2
|
||||
let origin = CGPoint(x: bounds.center.x - circleRadius, y: bounds.center.y - circleRadius)
|
||||
let size = CGSize.square(Self.preferredSize)
|
||||
let frame = CGRect(origin: origin, size: size)
|
||||
unselectedView.frame = frame
|
||||
selectedView.frame = frame
|
||||
checkmarkIcon.center = selectedView.bounds.center
|
||||
}
|
||||
|
||||
// MARK: State
|
||||
|
||||
private var _isSelected: Bool = false
|
||||
|
||||
public var isSelected: Bool {
|
||||
get { _isSelected }
|
||||
set { setIsSelected(newValue, animated: false) }
|
||||
}
|
||||
|
||||
public func setIsSelected(_ isSelected: Bool, animated: Bool) {
|
||||
guard isSelected != _isSelected else { return }
|
||||
_isSelected = isSelected
|
||||
updateAppearance(animated: animated)
|
||||
}
|
||||
|
||||
private var _isEnabled: Bool = true
|
||||
|
||||
public var isEnabled: Bool {
|
||||
get { _isEnabled }
|
||||
set { setIsEnabled(newValue, animated: false) }
|
||||
}
|
||||
|
||||
public func setIsEnabled(_ isEnabled: Bool, animated: Bool) {
|
||||
guard isEnabled != _isEnabled else { return }
|
||||
_isEnabled = isEnabled
|
||||
updateAppearance(animated: animated)
|
||||
}
|
||||
|
||||
// MARK: Appearance
|
||||
|
||||
private let unselectedView: UIView = {
|
||||
let ringView = RingView()
|
||||
ringView.lineWidth = 2
|
||||
ringView.tintColor = .Signal.tertiaryLabel
|
||||
return ringView
|
||||
}()
|
||||
|
||||
private lazy var selectedView: UIView = {
|
||||
let circleView = CircleView()
|
||||
circleView.backgroundColor = .Signal.accent
|
||||
circleView.addSubview(checkmarkIcon)
|
||||
return circleView
|
||||
}()
|
||||
|
||||
private let checkmarkIcon: UIImageView = {
|
||||
let imageView = UIImageView(image: UIImage(named: "check-compact"))
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.tintColor = .white
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private func updateAppearance(animated: Bool) {
|
||||
selectedView.setIsHidden(isSelected == false, animated: animated)
|
||||
unselectedView.setIsHidden(isSelected, animated: animated)
|
||||
|
||||
selectedView.backgroundColor = isEnabled ? .Signal.accent : .Signal.tertiaryLabel
|
||||
}
|
||||
}
|
||||
177
SignalUI/Views/SelectionIndicatorView.swift
Normal file
177
SignalUI/Views/SelectionIndicatorView.swift
Normal file
@ -0,0 +1,177 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import SignalServiceKit
|
||||
import UIKit
|
||||
|
||||
public class SelectionIndicatorView: UIView {
|
||||
|
||||
public enum Style {
|
||||
/// Use in lists over plain colored background.
|
||||
case list
|
||||
/// Use over media.
|
||||
case media
|
||||
}
|
||||
|
||||
// MARK: UIView
|
||||
|
||||
public init(style: Style = .list) {
|
||||
self.style = style
|
||||
|
||||
super.init(frame: .init(origin: .zero, size: .square(SelectionIndicatorView.preferredSize)))
|
||||
|
||||
// Because it is often paired with UILabels, we want to make
|
||||
// this view as compact and as compression resistant as possible.
|
||||
setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||
setContentCompressionResistancePriority(.required - 10, for: .horizontal)
|
||||
setContentCompressionResistancePriority(.required - 10, for: .vertical)
|
||||
|
||||
switch style {
|
||||
case .list:
|
||||
addSubview(innerRing)
|
||||
case .media:
|
||||
addSubview(outerRing)
|
||||
}
|
||||
addSubview(selectedView)
|
||||
updateAppearance(animated: false)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: Layout
|
||||
|
||||
private static let preferredSize: CGFloat = 24
|
||||
|
||||
private static let ringStrokeWidth: CGFloat = 2
|
||||
|
||||
private static let innerRingInset: CGFloat = 1
|
||||
|
||||
override public var intrinsicContentSize: CGSize {
|
||||
.square(Self.preferredSize)
|
||||
}
|
||||
|
||||
override public func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
switch style {
|
||||
case .list:
|
||||
innerRing.center = bounds.center
|
||||
// Inner ring is inset by 1dp relative to the view's bounds.
|
||||
// Filled circle (checkmark's background) has the same diameter as inner ring.
|
||||
let circleDiameter = Self.preferredSize - 2 * Self.innerRingInset
|
||||
innerRing.bounds.size = .square(circleDiameter)
|
||||
selectedView.bounds.size = .square(circleDiameter)
|
||||
case .media:
|
||||
outerRing.center = bounds.center
|
||||
outerRing.bounds.size = .square(Self.preferredSize)
|
||||
// Filled circle (checkmark's background) fills inside of the outer ring.
|
||||
selectedView.bounds.size = .square(Self.preferredSize - 2 * Self.ringStrokeWidth)
|
||||
}
|
||||
|
||||
selectedView.center = bounds.center
|
||||
|
||||
// Checkmark is self-sized and only needs to be centered properly.
|
||||
checkmarkIcon.center = selectedView.bounds.center
|
||||
}
|
||||
|
||||
// MARK: State
|
||||
|
||||
private var _isSelected: Bool = false
|
||||
|
||||
public var isSelected: Bool {
|
||||
get { _isSelected }
|
||||
set { setIsSelected(newValue, animated: false) }
|
||||
}
|
||||
|
||||
public func setIsSelected(_ isSelected: Bool, animated: Bool) {
|
||||
guard isSelected != _isSelected else { return }
|
||||
_isSelected = isSelected
|
||||
updateAppearance(animated: animated)
|
||||
}
|
||||
|
||||
private var _isEnabled: Bool = true
|
||||
|
||||
public var isEnabled: Bool {
|
||||
get { _isEnabled }
|
||||
set { setIsEnabled(newValue, animated: false) }
|
||||
}
|
||||
|
||||
public func setIsEnabled(_ isEnabled: Bool, animated: Bool) {
|
||||
guard isEnabled != _isEnabled else { return }
|
||||
_isEnabled = isEnabled
|
||||
updateAppearance(animated: animated)
|
||||
}
|
||||
|
||||
// Make this a `let` to simplify layout and avoid overhead of creating unused views.
|
||||
// The assumption is to only reference `innerRing` when style is `list`
|
||||
// and only reference `outerRing` when style is `media`.
|
||||
public let style: Style
|
||||
|
||||
// MARK: Appearance
|
||||
|
||||
/// Color that fills the selection ring and is the background for checkmark image.
|
||||
public var fillColor: UIColor = .Signal.accent {
|
||||
didSet {
|
||||
selectedView.backgroundColor = fillColor
|
||||
}
|
||||
}
|
||||
|
||||
private var effectiveFillColor: UIColor {
|
||||
isEnabled ? fillColor : .Signal.tertiaryLabel
|
||||
}
|
||||
|
||||
/// Color for the ckeckmark image and outer ring for media-style indicators.
|
||||
public var strokeColor: UIColor = .white {
|
||||
didSet {
|
||||
checkmarkIcon.tintColor = strokeColor
|
||||
if case .media = style {
|
||||
outerRing.tintColor = strokeColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var innerRing: UIView = {
|
||||
owsAssertDebug(style == .list, "Invalid access")
|
||||
let ringView = RingView()
|
||||
ringView.lineWidth = SelectionIndicatorView.ringStrokeWidth
|
||||
ringView.tintColor = .Signal.tertiaryLabel
|
||||
return ringView
|
||||
}()
|
||||
|
||||
private lazy var outerRing: UIView = {
|
||||
owsAssertDebug(style == .media, "Invalid access")
|
||||
let ringView = RingView()
|
||||
ringView.lineWidth = SelectionIndicatorView.ringStrokeWidth
|
||||
ringView.tintColor = strokeColor
|
||||
return ringView
|
||||
}()
|
||||
|
||||
private lazy var selectedView: UIView = {
|
||||
let circleView = CircleView()
|
||||
circleView.backgroundColor = effectiveFillColor
|
||||
circleView.addSubview(checkmarkIcon)
|
||||
return circleView
|
||||
}()
|
||||
|
||||
private lazy var checkmarkIcon: UIImageView = {
|
||||
let imageView = UIImageView(image: UIImage(named: "check-compact"))
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.tintColor = strokeColor
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private func updateAppearance(animated: Bool) {
|
||||
if case .list = style {
|
||||
innerRing.setIsHidden(isSelected, animated: animated)
|
||||
}
|
||||
// Outer ring is always visible.
|
||||
selectedView.setIsHidden(isSelected == false, animated: animated)
|
||||
|
||||
selectedView.backgroundColor = effectiveFillColor
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user