parent
42e29862ac
commit
6857529d06
|
@ -13,6 +13,7 @@ public protocol GalleryContentViewController: UIViewController {
|
|||
var contentSize: CGSize { get }
|
||||
var activityItemsForSharing: [Any] { get }
|
||||
var caption: String? { get }
|
||||
var contentOverlayAccessoryViewController: UIViewController? { get }
|
||||
var bottomControlsAccessoryViewController: UIViewController? { get }
|
||||
var canAnimateFromSourceView: Bool { get }
|
||||
|
||||
|
@ -20,6 +21,10 @@ public protocol GalleryContentViewController: UIViewController {
|
|||
}
|
||||
|
||||
public extension GalleryContentViewController {
|
||||
var contentOverlayAccessoryViewController: UIViewController? {
|
||||
nil
|
||||
}
|
||||
|
||||
var bottomControlsAccessoryViewController: UIViewController? {
|
||||
nil
|
||||
}
|
||||
|
|
|
@ -14,4 +14,5 @@ public protocol GalleryContentViewControllerContainer {
|
|||
func setGalleryContentLoading(_ loading: Bool)
|
||||
func galleryContentChanged()
|
||||
func disableGalleryScrollAndZoom()
|
||||
func setGalleryControlsVisible(_ visible: Bool, animated: Bool)
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ class GalleryItemViewController: UIViewController {
|
|||
|
||||
let itemIndex: Int
|
||||
let content: GalleryContentViewController
|
||||
private var overlayVC: UIViewController?
|
||||
|
||||
private var activityIndicator: UIActivityIndicatorView?
|
||||
private(set) var scrollView: UIScrollView!
|
||||
|
@ -32,6 +33,9 @@ class GalleryItemViewController: UIViewController {
|
|||
private var closeButtonTopConstraint: NSLayoutConstraint!
|
||||
private var bottomControlsView: UIStackView!
|
||||
private(set) var captionTextView: UITextView!
|
||||
|
||||
private var singleTap: UITapGestureRecognizer!
|
||||
private var doubleTap: UITapGestureRecognizer!
|
||||
|
||||
private var contentViewLeadingConstraint: NSLayoutConstraint?
|
||||
private var contentViewTopConstraint: NSLayoutConstraint?
|
||||
|
@ -70,6 +74,19 @@ class GalleryItemViewController: UIViewController {
|
|||
addContent()
|
||||
centerContent()
|
||||
|
||||
overlayVC = content.contentOverlayAccessoryViewController
|
||||
if let overlayVC {
|
||||
overlayVC.view.isHidden = activityIndicator != nil
|
||||
overlayVC.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(overlayVC.view)
|
||||
NSLayoutConstraint.activate([
|
||||
overlayVC.view.leadingAnchor.constraint(equalTo: content.view.leadingAnchor),
|
||||
overlayVC.view.trailingAnchor.constraint(equalTo: content.view.trailingAnchor),
|
||||
overlayVC.view.topAnchor.constraint(equalTo: content.view.topAnchor),
|
||||
overlayVC.view.bottomAnchor.constraint(equalTo: content.view.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
topControlsView = UIView()
|
||||
topControlsView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(topControlsView)
|
||||
|
@ -118,6 +135,7 @@ class GalleryItemViewController: UIViewController {
|
|||
captionTextView.isSelectable = true
|
||||
captionTextView.font = .preferredFont(forTextStyle: .body)
|
||||
captionTextView.adjustsFontForContentSizeCategory = true
|
||||
captionTextView.alwaysBounceVertical = true
|
||||
updateCaptionTextView()
|
||||
bottomControlsView.addArrangedSubview(captionTextView)
|
||||
|
||||
|
@ -151,8 +169,10 @@ class GalleryItemViewController: UIViewController {
|
|||
captionTextView.heightAnchor.constraint(equalToConstant: 150),
|
||||
])
|
||||
|
||||
let singleTap = UITapGestureRecognizer(target: self, action: #selector(viewPressed))
|
||||
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(viewDoublePressed))
|
||||
singleTap = UITapGestureRecognizer(target: self, action: #selector(viewPressed))
|
||||
singleTap.delegate = self
|
||||
doubleTap = UITapGestureRecognizer(target: self, action: #selector(viewDoublePressed))
|
||||
doubleTap.delegate = self
|
||||
doubleTap.numberOfTapsRequired = 2
|
||||
// this requirement is needed to make sure the double tap is ever recognized
|
||||
singleTap.require(toFail: doubleTap)
|
||||
|
@ -192,6 +212,8 @@ class GalleryItemViewController: UIViewController {
|
|||
}
|
||||
|
||||
func addContent() {
|
||||
content.loadViewIfNeeded()
|
||||
|
||||
content.setControlsVisible(controlsVisible, animated: false)
|
||||
|
||||
content.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -219,6 +241,16 @@ class GalleryItemViewController: UIViewController {
|
|||
content.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
if let overlayVC {
|
||||
NSLayoutConstraint.activate([
|
||||
overlayVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
overlayVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
overlayVC.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
overlayVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
content.view.layoutIfNeeded()
|
||||
}
|
||||
|
||||
|
@ -405,6 +437,7 @@ extension GalleryItemViewController: GalleryContentViewControllerContainer {
|
|||
|
||||
func setGalleryContentLoading(_ loading: Bool) {
|
||||
if loading {
|
||||
overlayVC?.view.isHidden = true
|
||||
if activityIndicator == nil {
|
||||
let activityIndicator = UIActivityIndicatorView(style: .large)
|
||||
self.activityIndicator = activityIndicator
|
||||
|
@ -430,6 +463,7 @@ extension GalleryItemViewController: GalleryContentViewControllerContainer {
|
|||
} else {
|
||||
activityIndicator.removeFromSuperview()
|
||||
self.activityIndicator = nil
|
||||
self.overlayVC?.view.isHidden = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -450,6 +484,10 @@ extension GalleryItemViewController: GalleryContentViewControllerContainer {
|
|||
addContent()
|
||||
}
|
||||
}
|
||||
|
||||
func setGalleryControlsVisible(_ visible: Bool, animated: Bool) {
|
||||
setControlsVisible(visible, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
extension GalleryItemViewController: UIScrollViewDelegate {
|
||||
|
@ -472,3 +510,17 @@ extension GalleryItemViewController: UIScrollViewDelegate {
|
|||
scrollView.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
extension GalleryItemViewController: UIGestureRecognizerDelegate {
|
||||
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer == singleTap {
|
||||
let loc = gestureRecognizer.location(in: view)
|
||||
return !topControlsView.frame.contains(loc) && !bottomControlsView.frame.contains(loc)
|
||||
} else if gestureRecognizer == doubleTap {
|
||||
let loc = gestureRecognizer.location(in: content.view)
|
||||
return content.view.bounds.contains(loc)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -185,6 +185,8 @@
|
|||
D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */; };
|
||||
D691771929A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */; };
|
||||
D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */; };
|
||||
D69261232BB3AEFB0023152C /* VideoOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */; };
|
||||
D69261272BB3BA610023152C /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69261262BB3BA610023152C /* Box.swift */; };
|
||||
D6934F2C2BA7AD32002B1C8D /* GalleryVC in Frameworks */ = {isa = PBXBuildFile; productRef = D6934F2B2BA7AD32002B1C8D /* GalleryVC */; };
|
||||
D6934F2E2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */; };
|
||||
D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */; };
|
||||
|
@ -196,6 +198,7 @@
|
|||
D6934F3C2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */; };
|
||||
D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */; };
|
||||
D6934F402BAA19EC002B1C8D /* VideoActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */; };
|
||||
D6934F422BAC7D6E002B1C8D /* VideoControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */; };
|
||||
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; };
|
||||
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */; };
|
||||
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */; };
|
||||
|
@ -587,6 +590,8 @@
|
|||
D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNoContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderMovedOverlayView.swift; sourceTree = "<group>"; };
|
||||
D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityHandlingContext.swift; sourceTree = "<group>"; };
|
||||
D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoOverlayViewController.swift; sourceTree = "<group>"; };
|
||||
D69261262BB3BA610023152C /* Box.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = "<group>"; };
|
||||
D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentsGalleryDataSource.swift; sourceTree = "<group>"; };
|
||||
D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGalleryContentViewController.swift; sourceTree = "<group>"; };
|
||||
D6934F312BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingGalleryContentViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -597,6 +602,7 @@
|
|||
D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoGalleryContentViewController.swift; sourceTree = "<group>"; };
|
||||
D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageActivityItemSource.swift; sourceTree = "<group>"; };
|
||||
D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoActivityItemSource.swift; sourceTree = "<group>"; };
|
||||
D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoControlsViewController.swift; sourceTree = "<group>"; };
|
||||
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; };
|
||||
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedProfileCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FeaturedProfileCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||
|
@ -829,6 +835,8 @@
|
|||
D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */,
|
||||
D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */,
|
||||
D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */,
|
||||
D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */,
|
||||
D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */,
|
||||
D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */,
|
||||
);
|
||||
path = Gallery;
|
||||
|
@ -1487,6 +1495,7 @@
|
|||
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
|
||||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
||||
D6D79F582A13293200AB2315 /* BackgroundManager.swift */,
|
||||
D69261262BB3BA610023152C /* Box.swift */,
|
||||
D61F75B6293C119700C0B37F /* Filterer.swift */,
|
||||
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
|
||||
D61F75BA293C183100C0B37F /* HTMLConverter.swift */,
|
||||
|
@ -1946,6 +1955,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D6934F422BAC7D6E002B1C8D /* VideoControlsViewController.swift in Sources */,
|
||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
||||
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */,
|
||||
D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */,
|
||||
|
@ -1989,6 +1999,7 @@
|
|||
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */,
|
||||
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
|
||||
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
|
||||
D69261232BB3AEFB0023152C /* VideoOverlayViewController.swift in Sources */,
|
||||
D6934F3C2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift in Sources */,
|
||||
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */,
|
||||
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
|
||||
|
@ -2162,6 +2173,7 @@
|
|||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
||||
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
||||
D646DCD42A0729440059ECEB /* ActionNotificationGroupCollectionViewCell.swift in Sources */,
|
||||
D69261272BB3BA610023152C /* Box.swift in Sources */,
|
||||
D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */,
|
||||
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */,
|
||||
D65B4B58297203A700DABDFB /* ReportSelectRulesView.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// Box.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 3/26/24.
|
||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@propertyWrapper
|
||||
class Box<Value> {
|
||||
var wrappedValue: Value
|
||||
|
||||
init(wrappedValue: Value) {
|
||||
self.wrappedValue = wrappedValue
|
||||
}
|
||||
}
|
|
@ -0,0 +1,459 @@
|
|||
//
|
||||
// VideoControlsViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 3/21/24.
|
||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
|
||||
class VideoControlsViewController: UIViewController {
|
||||
private static let formatter: DateComponentsFormatter = {
|
||||
let f = DateComponentsFormatter()
|
||||
f.allowedUnits = [.minute, .second]
|
||||
f.zeroFormattingBehavior = .pad
|
||||
return f
|
||||
}()
|
||||
|
||||
private let player: AVPlayer
|
||||
@Box private var playbackSpeed: Float
|
||||
|
||||
private lazy var muteButton = MuteButton().configure {
|
||||
$0.addTarget(self, action: #selector(muteButtonPressed), for: .touchUpInside)
|
||||
$0.setMuted(false, animated: false)
|
||||
}
|
||||
|
||||
private let timestampLabel = UILabel().configure {
|
||||
$0.text = "0:00"
|
||||
$0.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular))
|
||||
}
|
||||
|
||||
private lazy var scrubbingControl = VideoScrubbingControl().configure {
|
||||
$0.heightAnchor.constraint(equalToConstant: 44).isActive = true
|
||||
$0.addTarget(self, action: #selector(scrubbingStarted), for: .editingDidBegin)
|
||||
$0.addTarget(self, action: #selector(scrubbingChanged), for: .editingChanged)
|
||||
$0.addTarget(self, action: #selector(scrubbingEnded), for: .editingDidEnd)
|
||||
}
|
||||
|
||||
private let timeRemainingLabel = UILabel().configure {
|
||||
$0.text = "-0:00"
|
||||
$0.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular))
|
||||
}
|
||||
|
||||
private lazy var optionsButton = MenuButton { [unowned self] in
|
||||
let imageName: String
|
||||
if #available(iOS 17.0, *) {
|
||||
switch self.playbackSpeed {
|
||||
case 0.5:
|
||||
imageName = "gauge.with.dots.needle.0percent"
|
||||
case 1:
|
||||
imageName = "gauge.with.dots.needle.33percent"
|
||||
case 1.25:
|
||||
imageName = "gauge.with.dots.needle.50percent"
|
||||
case 2:
|
||||
imageName = "gauge.with.dots.needle.100percent"
|
||||
default:
|
||||
imageName = "gauge.with.dots.needle.67percent"
|
||||
}
|
||||
} else {
|
||||
imageName = "speedometer"
|
||||
}
|
||||
let speedMenu = UIMenu(title: "Playback Speed", image: UIImage(systemName: imageName), children: PlaybackSpeed.allCases.map { speed in
|
||||
UIAction(title: speed.displayName, state: self.playbackSpeed == speed.rate ? .on : .off) { [unowned self] _ in
|
||||
self.playbackSpeed = speed.rate
|
||||
if self.player.rate > 0 {
|
||||
self.player.rate = speed.rate
|
||||
}
|
||||
}
|
||||
})
|
||||
return UIMenu(children: [speedMenu])
|
||||
}
|
||||
|
||||
private lazy var hStack = UIStackView(arrangedSubviews: [
|
||||
muteButton,
|
||||
timestampLabel,
|
||||
scrubbingControl,
|
||||
timeRemainingLabel,
|
||||
optionsButton,
|
||||
]).configure {
|
||||
$0.axis = .horizontal
|
||||
$0.spacing = 8
|
||||
$0.alignment = .center
|
||||
}
|
||||
|
||||
private var timestampObserverToken: Any?
|
||||
private var scrubberObserverToken: Any?
|
||||
|
||||
private var wasPlayingWhenScrubbingStarted = false
|
||||
private var scrubbingTargetTime: CMTime?
|
||||
private var isSeeking = false
|
||||
|
||||
init(player: AVPlayer, playbackSpeed: Box<Float>) {
|
||||
self.player = player
|
||||
self._playbackSpeed = playbackSpeed
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
hStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(hStack)
|
||||
NSLayoutConstraint.activate([
|
||||
hStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 4),
|
||||
hStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -4),
|
||||
hStack.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
hStack.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
timestampObserverToken = player.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 2), queue: .main) { [unowned self] _ in
|
||||
self.updateTimestamps()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateTimestamps() {
|
||||
let current = player.currentTime()
|
||||
timestampLabel.text = VideoControlsViewController.formatter.string(from: current.seconds)!
|
||||
let duration = player.currentItem!.duration
|
||||
if duration != .indefinite {
|
||||
let remaining = duration - current
|
||||
timeRemainingLabel.text = "-" + VideoControlsViewController.formatter.string(from: remaining.seconds)!
|
||||
|
||||
if scrubberObserverToken == nil {
|
||||
let interval = CMTime(value: 1, timescale: CMTimeScale(self.scrubbingControl.bounds.width))
|
||||
if interval.isValid {
|
||||
self.scrubberObserverToken = self.player.addPeriodicTimeObserver(forInterval: interval, queue: .main, using: { _ in
|
||||
self.scrubbingControl.fractionComplete = self.player.currentTime().seconds / duration.seconds
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func scrubbingStarted() {
|
||||
wasPlayingWhenScrubbingStarted = player.rate > 0
|
||||
player.rate = 0
|
||||
}
|
||||
|
||||
@objc private func scrubbingChanged() {
|
||||
let duration = player.currentItem!.duration
|
||||
let time = CMTime(value: CMTimeValue(scrubbingControl.fractionComplete * duration.seconds * 1_000_000_000), timescale: 1_000_000_000)
|
||||
scrubbingTargetTime = time
|
||||
if !isSeeking {
|
||||
seekToScrubbingTime()
|
||||
}
|
||||
}
|
||||
|
||||
private func seekToScrubbingTime() {
|
||||
guard let scrubbingTargetTime else {
|
||||
return
|
||||
}
|
||||
isSeeking = true
|
||||
player.seek(to: scrubbingTargetTime) { finished in
|
||||
if finished {
|
||||
if self.scrubbingTargetTime != scrubbingTargetTime {
|
||||
self.seekToScrubbingTime()
|
||||
} else {
|
||||
self.isSeeking = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func scrubbingEnded() {
|
||||
scrubbingChanged()
|
||||
if wasPlayingWhenScrubbingStarted {
|
||||
player.rate = playbackSpeed
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func muteButtonPressed() {
|
||||
player.isMuted.toggle()
|
||||
muteButton.setMuted(player.isMuted, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private class VideoScrubbingControl: UIControl {
|
||||
var fractionComplete: Double = 0 {
|
||||
didSet {
|
||||
updateFillLayerMask()
|
||||
}
|
||||
}
|
||||
|
||||
private let trackLayer = CAShapeLayer()
|
||||
private let fillLayer = CAShapeLayer()
|
||||
private let fillMaskLayer = CALayer()
|
||||
|
||||
private var scrubbingStartFraction: Double?
|
||||
private var touchStartLocation: CGPoint?
|
||||
private var animator: UIViewPropertyAnimator?
|
||||
|
||||
#if !os(visionOS)
|
||||
private var feedbackGenerator: UIImpactFeedbackGenerator?
|
||||
#endif
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
||||
trackLayer.fillColor = UIColor.systemGray.cgColor
|
||||
trackLayer.shadowColor = UIColor.black.cgColor
|
||||
layer.addSublayer(trackLayer)
|
||||
|
||||
fillLayer.fillColor = UIColor.white.cgColor
|
||||
fillLayer.mask = fillMaskLayer
|
||||
layer.addSublayer(fillLayer)
|
||||
|
||||
fillMaskLayer.backgroundColor = UIColor.black.cgColor
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSublayers(of layer: CALayer) {
|
||||
super.layoutSublayers(of: layer)
|
||||
|
||||
let trackFrame = CGRect(x: 0, y: (layer.bounds.height - 8) / 2, width: layer.bounds.width, height: 8)
|
||||
|
||||
trackLayer.frame = trackFrame
|
||||
trackLayer.path = CGPath(roundedRect: CGRect(x: 0, y: 0, width: trackFrame.width, height: trackFrame.height), cornerWidth: 4, cornerHeight: 4, transform: nil)
|
||||
trackLayer.shadowPath = trackLayer.path
|
||||
|
||||
fillLayer.frame = trackFrame
|
||||
fillLayer.path = trackLayer.path
|
||||
updateFillLayerMask()
|
||||
}
|
||||
|
||||
private func updateFillLayerMask() {
|
||||
// I don't know where this animation is coming from
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
fillMaskLayer.frame = CGRect(x: 0, y: 0, width: fractionComplete * bounds.width, height: 8)
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||
touchStartLocation = touch.location(in: self)
|
||||
scrubbingStartFraction = fractionComplete
|
||||
|
||||
animator = UIViewPropertyAnimator(duration: 0.1, curve: .linear)
|
||||
animator!.addAnimations {
|
||||
self.transform = CGAffineTransform(scaleX: 1, y: 1.5)
|
||||
}
|
||||
animator!.startAnimation()
|
||||
|
||||
sendActions(for: .editingDidBegin)
|
||||
|
||||
#if !os(visionOS)
|
||||
feedbackGenerator = UIImpactFeedbackGenerator(style: .light)
|
||||
feedbackGenerator!.prepare()
|
||||
#endif
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||
guard let touchStartLocation,
|
||||
let scrubbingStartFraction else {
|
||||
return false
|
||||
}
|
||||
let location = touch.location(in: self)
|
||||
let translation = CGPoint(x: location.x - touchStartLocation.x, y: location.y - touchStartLocation.y)
|
||||
let scrubbingAmount = translation.x / bounds.width
|
||||
let unclampedFractionComplete = scrubbingStartFraction + scrubbingAmount
|
||||
let newFractionComplete = max(0, min(1, unclampedFractionComplete))
|
||||
#if !os(visionOS)
|
||||
if newFractionComplete != fractionComplete && (newFractionComplete == 0 || newFractionComplete == 1) {
|
||||
feedbackGenerator!.impactOccurred(intensity: 0.5)
|
||||
}
|
||||
#endif
|
||||
fractionComplete = newFractionComplete
|
||||
sendActions(for: .editingChanged)
|
||||
|
||||
if unclampedFractionComplete < 0 || unclampedFractionComplete > 1 {
|
||||
let stretchFactor: CGFloat
|
||||
if unclampedFractionComplete < 0 {
|
||||
stretchFactor = 1/(unclampedFractionComplete * bounds.width / 10 - 1) + 1
|
||||
} else {
|
||||
stretchFactor = -1/((unclampedFractionComplete-1) * bounds.width / 10 + 1) + 1
|
||||
}
|
||||
let stretchAmount = 8 * stretchFactor
|
||||
transform = CGAffineTransform(scaleX: 1 + stretchAmount / bounds.width, y: 1 + 0.5 * (1 - stretchFactor))
|
||||
.translatedBy(x: sign(unclampedFractionComplete) * stretchAmount / 2, y: 0)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
||||
touchStartLocation = nil
|
||||
resetScale()
|
||||
sendActions(for: .editingDidEnd)
|
||||
#if !os(visionOS)
|
||||
feedbackGenerator = nil
|
||||
#endif
|
||||
}
|
||||
|
||||
override func cancelTracking(with event: UIEvent?) {
|
||||
touchStartLocation = nil
|
||||
resetScale()
|
||||
sendActions(for: .editingDidEnd)
|
||||
#if !os(visionOS)
|
||||
feedbackGenerator = nil
|
||||
#endif
|
||||
}
|
||||
|
||||
private func resetScale() {
|
||||
if let animator,
|
||||
animator.isRunning {
|
||||
animator.isReversed = true
|
||||
animator.startAnimation()
|
||||
} else {
|
||||
animator?.pauseAnimation()
|
||||
animator = UIViewPropertyAnimator(duration: 0.1, curve: .linear)
|
||||
animator!.addAnimations {
|
||||
self.transform = .identity
|
||||
}
|
||||
animator!.startAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private class MuteButton: UIControl {
|
||||
private let imageView = UIImageView()
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
CGSize(width: 32, height: 32)
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.tintColor = .white
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(imageView)
|
||||
NSLayoutConstraint.activate([
|
||||
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setMuted(_ muted: Bool, animated: Bool) {
|
||||
let image = UIImage(systemName: muted ? "speaker.slash.fill" : "speaker.wave.3.fill")!
|
||||
if animated,
|
||||
#available(iOS 17.0, *) {
|
||||
imageView.setSymbolImage(image, contentTransition: .replace.wholeSymbol, options: .speed(5))
|
||||
} else {
|
||||
imageView.image = image
|
||||
}
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
!(gestureRecognizer is UITapGestureRecognizer)
|
||||
}
|
||||
}
|
||||
|
||||
private class MenuButton: UIControl {
|
||||
private let menuProvider: () -> UIMenu
|
||||
|
||||
private let imageView = UIImageView()
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
CGSize(width: 32, height: 32)
|
||||
}
|
||||
|
||||
init(menuProvider: @escaping () -> UIMenu) {
|
||||
self.menuProvider = menuProvider
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
imageView.image = UIImage(systemName: "ellipsis.circle")
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.tintColor = .white
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(imageView)
|
||||
NSLayoutConstraint.activate([
|
||||
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
])
|
||||
|
||||
isContextMenuInteractionEnabled = true
|
||||
showsMenuAsPrimaryAction = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||
UIContextMenuConfiguration(actionProvider: { _ in
|
||||
self.menuProvider()
|
||||
})
|
||||
}
|
||||
|
||||
override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willDisplayMenuFor configuration: UIContextMenuConfiguration, animator: (any UIContextMenuInteractionAnimating)?) {
|
||||
animator?.addAnimations {
|
||||
self.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
|
||||
}
|
||||
}
|
||||
|
||||
override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willEndFor configuration: UIContextMenuConfiguration, animator: (any UIContextMenuInteractionAnimating)?) {
|
||||
if let animator {
|
||||
animator.addAnimations {
|
||||
self.transform = .identity
|
||||
}
|
||||
} else {
|
||||
self.transform = .identity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum PlaybackSpeed: CaseIterable {
|
||||
case half, regular, oneAndAQuarter, oneAndAHalf, two
|
||||
|
||||
var rate: Float {
|
||||
switch self {
|
||||
case .half:
|
||||
0.5
|
||||
case .regular:
|
||||
1
|
||||
case .oneAndAQuarter:
|
||||
1.25
|
||||
case .oneAndAHalf:
|
||||
1.5
|
||||
case .two:
|
||||
2
|
||||
}
|
||||
}
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .half:
|
||||
"0.5×"
|
||||
case .regular:
|
||||
"1×"
|
||||
case .oneAndAQuarter:
|
||||
"1.25×"
|
||||
case .oneAndAHalf:
|
||||
"1.5×"
|
||||
case .two:
|
||||
"2×"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,9 +16,14 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
|||
private let item: AVPlayerItem
|
||||
let player: AVPlayer
|
||||
|
||||
@available(iOS, obsoleted: 16.0, message: "Use AVPlayer.defaultRate")
|
||||
@Box private var playbackSpeed: Float = 1
|
||||
|
||||
private var presentationSizeObservation: NSKeyValueObservation?
|
||||
private var statusObservation: NSKeyValueObservation?
|
||||
private var rateObservation: NSKeyValueObservation?
|
||||
private var isFirstAppearance = true
|
||||
private var hideControlsWorkItem: DispatchWorkItem?
|
||||
|
||||
init(url: URL, caption: String?) {
|
||||
self.url = url
|
||||
|
@ -64,6 +69,20 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
|||
}
|
||||
}
|
||||
})
|
||||
rateObservation = player.observe(\.rate, options: .old, changeHandler: { [unowned self] player, info in
|
||||
hideControlsWorkItem?.cancel()
|
||||
if player.rate > 0 && info.oldValue == 0 {
|
||||
hideControlsWorkItem = DispatchWorkItem { [weak self] in
|
||||
guard let self,
|
||||
let container = self.container,
|
||||
container.galleryControlsVisible else {
|
||||
return
|
||||
}
|
||||
container.setGalleryControlsVisible(false, animated: true)
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: hideControlsWorkItem!)
|
||||
}
|
||||
})
|
||||
|
||||
preferredContentSize = item.presentationSize
|
||||
}
|
||||
|
@ -95,6 +114,18 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
|||
[VideoActivityItemSource(asset: item.asset, url: url)]
|
||||
}
|
||||
|
||||
private lazy var overlayVC = VideoOverlayViewController(player: player, playbackSpeed: _playbackSpeed)
|
||||
var contentOverlayAccessoryViewController: UIViewController? {
|
||||
overlayVC
|
||||
}
|
||||
|
||||
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed)
|
||||
|
||||
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
||||
overlayVC.setVisible(visible)
|
||||
hideControlsWorkItem?.cancel()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class PlayerView: UIView {
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
//
|
||||
// VideoOverlayViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 3/26/24.
|
||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
|
||||
class VideoOverlayViewController: UIViewController {
|
||||
|
||||
private static let playImage = UIImage(systemName: "play.fill")!
|
||||
private static let pauseImage = UIImage(systemName: "pause.fill")!
|
||||
|
||||
private let player: AVPlayer
|
||||
@Box private var playbackSpeed: Float
|
||||
|
||||
private var dimmingView: UIView!
|
||||
private var controlsStack: UIStackView!
|
||||
private var skipBackButton: VideoOverlayButton!
|
||||
private var skipForwardButton: VideoOverlayButton!
|
||||
|
||||
private var rateObservation: NSKeyValueObservation?
|
||||
|
||||
init(player: AVPlayer, playbackSpeed: Box<Float>) {
|
||||
self.player = player
|
||||
self._playbackSpeed = playbackSpeed
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
dimmingView = UIView()
|
||||
dimmingView.backgroundColor = .black
|
||||
dimmingView.alpha = 0.2
|
||||
dimmingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(dimmingView)
|
||||
NSLayoutConstraint.activate([
|
||||
dimmingView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
dimmingView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
dimmingView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
dimmingView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
skipBackButton = VideoOverlayButton(image: UIImage(systemName: "gobackward.10")!)
|
||||
skipBackButton.addTarget(self, action: #selector(skipBackPressed), for: .touchUpInside)
|
||||
|
||||
let playPauseButton = VideoOverlayButton(image: VideoOverlayViewController.pauseImage)
|
||||
playPauseButton.addTarget(self, action: #selector(playPausePressed), for: .touchUpInside)
|
||||
|
||||
skipForwardButton = VideoOverlayButton(image: UIImage(systemName: "goforward.10")!)
|
||||
skipForwardButton.addTarget(self, action: #selector(skipForwardPressed), for: .touchUpInside)
|
||||
|
||||
controlsStack = UIStackView(arrangedSubviews: [
|
||||
skipBackButton,
|
||||
playPauseButton,
|
||||
skipForwardButton,
|
||||
])
|
||||
controlsStack.axis = .horizontal
|
||||
controlsStack.alignment = .center
|
||||
controlsStack.spacing = 24
|
||||
controlsStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(controlsStack)
|
||||
NSLayoutConstraint.activate([
|
||||
controlsStack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
controlsStack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||
|
||||
skipBackButton.widthAnchor.constraint(equalToConstant: 50),
|
||||
skipBackButton.heightAnchor.constraint(equalToConstant: 50),
|
||||
playPauseButton.widthAnchor.constraint(equalToConstant: 66),
|
||||
playPauseButton.heightAnchor.constraint(equalToConstant: 66),
|
||||
skipForwardButton.widthAnchor.constraint(equalToConstant: 50),
|
||||
skipForwardButton.heightAnchor.constraint(equalToConstant: 50),
|
||||
])
|
||||
|
||||
rateObservation = player.observe(\.rate, changeHandler: { player, _ in
|
||||
MainActor.runUnsafely {
|
||||
playPauseButton.image = player.rate > 0 ? VideoOverlayViewController.pauseImage : VideoOverlayViewController.playImage
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func setVisible(_ visible: Bool) {
|
||||
loadViewIfNeeded()
|
||||
|
||||
view.alpha = visible ? 1 : 0
|
||||
}
|
||||
|
||||
@objc private func playPausePressed() {
|
||||
if player.rate > 0 {
|
||||
player.rate = 0
|
||||
} else {
|
||||
player.rate = playbackSpeed
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func skipBackPressed() {
|
||||
player.seek(to: player.currentTime() - CMTime(value: 10, timescale: 1))
|
||||
}
|
||||
|
||||
@objc private func skipForwardPressed() {
|
||||
player.seek(to: player.currentTime() + CMTime(value: 10, timescale: 1))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class VideoOverlayButton: UIControl {
|
||||
var image: UIImage? {
|
||||
get {
|
||||
imageView.image
|
||||
}
|
||||
set {
|
||||
imageView.image = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private let backgroundView = UIView()
|
||||
private let imageView = UIImageView()
|
||||
|
||||
private var animator: UIViewPropertyAnimator?
|
||||
|
||||
override var isEnabled: Bool {
|
||||
didSet {
|
||||
imageView.tintColor = isEnabled ? .white : .lightGray
|
||||
}
|
||||
}
|
||||
|
||||
init(image: UIImage) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
backgroundView.alpha = 0
|
||||
backgroundView.backgroundColor = .lightGray.withAlphaComponent(0.5)
|
||||
backgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(backgroundView)
|
||||
NSLayoutConstraint.activate([
|
||||
backgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
backgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
backgroundView.topAnchor.constraint(equalTo: topAnchor),
|
||||
backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
|
||||
imageView.image = image
|
||||
imageView.tintColor = .white
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.preferredSymbolConfiguration = .init(scale: .large)
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(imageView)
|
||||
NSLayoutConstraint.activate([
|
||||
imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
||||
imageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
|
||||
imageView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
||||
imageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
backgroundView.layer.cornerRadius = bounds.height / 2
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
bounds.contains(point) ? self : nil
|
||||
}
|
||||
|
||||
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.backgroundView.alpha = 1
|
||||
self.backgroundView.transform = CGAffineTransform(scaleX: 1/0.8, y: 1/0.8)
|
||||
self.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
|
||||
}
|
||||
return super.beginTracking(touch, with: event)
|
||||
}
|
||||
|
||||
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.backgroundView.alpha = 0
|
||||
self.backgroundView.transform = .identity
|
||||
self.transform = .identity
|
||||
}
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer is UITapGestureRecognizer {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue