From 56049104717724b7eba0100da4e854ccb1021a1e Mon Sep 17 00:00:00 2001 From: wenlei Date: Thu, 16 Nov 2023 15:49:48 +0800 Subject: [PATCH] Finish building the player interface --- .../IndieMusic.xcodeproj/project.pbxproj | 12 + .../IndieMusic/Common/SegmentControl.swift | 639 ++++++++++++++++++ IndieMusic/IndieMusic/Models/Player.swift | 28 + .../Modules/Home/HomeViewController.swift | 2 + .../Modules/Player/PlayerView.swift | 223 +++++- .../Modules/Player/PlayerViewController.swift | 55 +- .../Modules/Player/PlayerViewModel.swift | 34 + IndieMusic/Podfile | 5 +- 8 files changed, 982 insertions(+), 16 deletions(-) create mode 100644 IndieMusic/IndieMusic/Common/SegmentControl.swift create mode 100644 IndieMusic/IndieMusic/Models/Player.swift create mode 100644 IndieMusic/IndieMusic/Modules/Player/PlayerViewModel.swift diff --git a/IndieMusic/IndieMusic.xcodeproj/project.pbxproj b/IndieMusic/IndieMusic.xcodeproj/project.pbxproj index 7da626b..2f90c96 100644 --- a/IndieMusic/IndieMusic.xcodeproj/project.pbxproj +++ b/IndieMusic/IndieMusic.xcodeproj/project.pbxproj @@ -14,6 +14,9 @@ 774399A62AFE036A006F8EEA /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774399A52AFE036A006F8EEA /* PlayerView.swift */; }; 774399A82AFE28BA006F8EEA /* BlurEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774399A72AFE28BA006F8EEA /* BlurEffectView.swift */; }; 774399AA2AFE3170006F8EEA /* PaddingLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774399A92AFE3170006F8EEA /* PaddingLabel.swift */; }; + 774A17F32B0459C900F56DF1 /* PlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774A17F22B0459C900F56DF1 /* PlayerViewModel.swift */; }; + 774A17F52B045F1C00F56DF1 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774A17F42B045F1C00F56DF1 /* Player.swift */; }; + 774A17F72B04932100F56DF1 /* SegmentControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774A17F62B04932100F56DF1 /* SegmentControl.swift */; }; 778B8A212AF8E36D0034AFD4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 778B8A202AF8E36D0034AFD4 /* AppDelegate.swift */; }; 778B8A232AF8E36D0034AFD4 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 778B8A222AF8E36D0034AFD4 /* SceneDelegate.swift */; }; 778B8A282AF8E36D0034AFD4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 778B8A262AF8E36D0034AFD4 /* Main.storyboard */; }; @@ -101,6 +104,9 @@ 774399A52AFE036A006F8EEA /* PlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = ""; }; 774399A72AFE28BA006F8EEA /* BlurEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurEffectView.swift; sourceTree = ""; }; 774399A92AFE3170006F8EEA /* PaddingLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaddingLabel.swift; sourceTree = ""; }; + 774A17F22B0459C900F56DF1 /* PlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewModel.swift; sourceTree = ""; }; + 774A17F42B045F1C00F56DF1 /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; }; + 774A17F62B04932100F56DF1 /* SegmentControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentControl.swift; sourceTree = ""; }; 778B8A1D2AF8E36D0034AFD4 /* IndieMusic.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IndieMusic.app; sourceTree = BUILT_PRODUCTS_DIR; }; 778B8A202AF8E36D0034AFD4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 778B8A222AF8E36D0034AFD4 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -201,6 +207,7 @@ 7743999D2AFA18C3006F8EEA /* PlayerViewController.swift */, 7743999F2AFA1968006F8EEA /* PlayerTabBar.swift */, 774399A52AFE036A006F8EEA /* PlayerView.swift */, + 774A17F22B0459C900F56DF1 /* PlayerViewModel.swift */, ); path = Player; sourceTree = ""; @@ -295,6 +302,7 @@ 778B8AB22AF8ED270034AFD4 /* WebViewController.swift */, 774399A72AFE28BA006F8EEA /* BlurEffectView.swift */, 774399A92AFE3170006F8EEA /* PaddingLabel.swift */, + 774A17F62B04932100F56DF1 /* SegmentControl.swift */, ); path = Common; sourceTree = ""; @@ -343,6 +351,7 @@ 778B8A8C2AF8ECF20034AFD4 /* AudioTrack.swift */, 778B8A892AF8ECF20034AFD4 /* EmptyModel.swift */, 778B8A8A2AF8ECF20034AFD4 /* Token.swift */, + 774A17F42B045F1C00F56DF1 /* Player.swift */, ); path = Models; sourceTree = ""; @@ -738,6 +747,7 @@ 778B8ABE2AF8ED280034AFD4 /* ViewModel.swift in Sources */, 774399A82AFE28BA006F8EEA /* BlurEffectView.swift in Sources */, 778B8A852AF8ECE50034AFD4 /* SearchViewController.swift in Sources */, + 774A17F52B045F1C00F56DF1 /* Player.swift in Sources */, 778B8AAB2AF8ED0E0034AFD4 /* UIView+Borders.swift in Sources */, 778B8AC12AF8ED280034AFD4 /* TableView.swift in Sources */, 778B8A9C2AF8ECFC0034AFD4 /* LogManager.swift in Sources */, @@ -755,8 +765,10 @@ 778B8A232AF8E36D0034AFD4 /* SceneDelegate.swift in Sources */, 778B8A6F2AF8ECD30034AFD4 /* APIConfig.swift in Sources */, 778B8A8E2AF8ECF20034AFD4 /* Artist.swift in Sources */, + 774A17F32B0459C900F56DF1 /* PlayerViewModel.swift in Sources */, 778B8A6D2AF8ECD30034AFD4 /* Observable+Operators.swift in Sources */, 778B8ABB2AF8ED280034AFD4 /* WebViewController.swift in Sources */, + 774A17F72B04932100F56DF1 /* SegmentControl.swift in Sources */, 778B8A712AF8ECD30034AFD4 /* Api.swift in Sources */, 778B8A812AF8ECE50034AFD4 /* HomeTabBarViewModel.swift in Sources */, 778B8AC02AF8ED280034AFD4 /* ViewController.swift in Sources */, diff --git a/IndieMusic/IndieMusic/Common/SegmentControl.swift b/IndieMusic/IndieMusic/Common/SegmentControl.swift new file mode 100644 index 0000000..914ad40 --- /dev/null +++ b/IndieMusic/IndieMusic/Common/SegmentControl.swift @@ -0,0 +1,639 @@ +// +// SegmentControl.swift +// IndieMusic +// +// Created by WenLei on 2023/11/15. +// + +import UIKit + +struct SegmentControlSetting { + + static let itemTextColor = UIColor.init(hex: 0xFFFFFF, alpha: 0.4) + static let itemSelectedTextColor = UIColor.init(hex: 0xFFFFFF, alpha: 1) + + static let itemBackgroundColor = color(red: 255.0, green: 250.0, blue: 250.0, alpha: 1.0) + static let itemSelectedBackgroundColor = color(red: 255.0, green: 250.0, blue: 250.0, alpha: 1.0) + + static let itemBorder : CGFloat = 30.0 + //MARK - Text font + static let textFont = UIFont.systemFont(ofSize: 16.0) + static let selectedTextFont = UIFont.systemFont(ofSize: 16.0) + + //MARK - slider + static let selectedViewBackgroundColor = UIColor.orange + static let selectedViewpadding : CGFloat = 20.0 + + //MARK - bridge + static let bridgeColor = UIColor.red + static let bridgeWidth : CGFloat = 7.0 + + //MARK - divider + static let dividerWidth : CGFloat = 2.0 + static let dividerpPadding : CGFloat = 10.0 + + + //MARK - inline func + @inline(__always) static func color(red:Float, green:Float, blue:Float, alpha:Float) -> UIColor { + return UIColor.init(red: CGFloat(red/255.0), green: CGFloat(green/255.0), blue: CGFloat(blue/255.0), alpha: CGFloat(alpha)) + } +} + +fileprivate class SelectedBackgroundView : UIView { + + lazy var cornerMask: CAShapeLayer = { + let mask = CAShapeLayer() + return mask + }() + + + override init(frame: CGRect) { + super.init(frame: frame) + + } + + override func layoutSubviews() { + super.layoutSubviews() + + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate var color : UIColor? { + didSet{ + self.backgroundColor = color + } + } +} + +fileprivate enum SegmentItemViewState : Int { + case Normal + case Selected +} + +fileprivate class SegmentItemView : UIView { + + fileprivate func itemWidth() -> CGFloat { + + if let text = titleLabel.text { + let string = text as NSString + let size = string.size(withAttributes: [NSAttributedString.Key.font:selectedFont!]) + return size.width + SegmentControlSetting.itemBorder + } + + return 0.0 + } + + fileprivate let titleLabel = UILabel() + fileprivate lazy var bridgeView : CALayer = { + let view = CALayer() + let width = SegmentControlSetting.bridgeWidth + view.bounds = CGRect(x: 0.0, y: 0.0, width: width, height: width) + view.backgroundColor = SegmentControlSetting.bridgeColor.cgColor + view.cornerRadius = view.bounds.size.width * 0.5 + return view + }() + + fileprivate lazy var dividerImageView : UIImageView = { + let dividerImageView = UIImageView.init(image: UIImage.init(named: "")) + dividerImageView.backgroundColor = .gray + + return dividerImageView + }() + + + fileprivate var dividerWidth : CGFloat = 1 { + didSet{ + if state == .Normal { + + } + } + } + + fileprivate var dividerpPadding : CGFloat = 13 { + didSet{ + if state == .Normal { + + } + } + } + + + fileprivate func showBridge(show:Bool){ + self.bridgeView.isHidden = !show + } + + fileprivate var state : SegmentItemViewState = .Normal { + didSet{ + updateItemView(state: state) + } + } + + fileprivate var font : UIFont?{ + didSet{ + if state == .Normal { + self.titleLabel.font = font + } + } + } + fileprivate var selectedFont : UIFont?{ + didSet{ + if state == .Selected { + self.titleLabel.font = selectedFont + } + } + } + + fileprivate var text : String?{ + didSet{ + self.titleLabel.text = text + } + } + + fileprivate var textColor : UIColor?{ + didSet{ + if state == .Normal { + self.titleLabel.textColor = textColor + } + } + } + fileprivate var selectedTextColor : UIColor?{ + didSet{ + if state == .Selected { + self.titleLabel.textColor = selectedTextColor + } + } + } + + fileprivate var itemBackgroundColor : UIColor?{ + didSet{ + if state == .Normal { + self.backgroundColor = .red + } + } + } + fileprivate var selectedBackgroundColor : UIColor?{ + didSet{ + if state == .Selected { + self.backgroundColor = selectedBackgroundColor + } + } + } + + fileprivate var textAlignment = NSTextAlignment.center { + didSet{ + self.titleLabel.textAlignment = textAlignment + } + } + + private func updateItemView(state: SegmentItemViewState){ + switch state { + case .Normal: + self.titleLabel.font = self.font + self.titleLabel.textColor = self.textColor + self.backgroundColor = self.itemBackgroundColor + case .Selected: + self.titleLabel.font = selectedFont + self.titleLabel.textColor = self.selectedTextColor + self.backgroundColor = self.selectedBackgroundColor + } + self.setNeedsLayout() + self.layoutIfNeeded() + } + + override init(frame: CGRect) { + super.init(frame: frame) + + + titleLabel.textAlignment = .center + + addSubview(titleLabel) + + bridgeView.isHidden = true + layer.addSublayer(bridgeView) + + layer.masksToBounds = true + + addSubview(dividerImageView) + } + + fileprivate override func layoutSubviews() { + super.layoutSubviews() + + backgroundColor = UIColor.clear + + + titleLabel.sizeToFit() + + titleLabel.center.x = bounds.size.width * 0.5 + titleLabel.center.y = bounds.size.height * 0.5 + + let width = bridgeView.bounds.size.width + let x:CGFloat = titleLabel.frame.maxX + bridgeView.frame = CGRect(x: x, y: bounds.midY - width, width: width, height: width) + + dividerImageView.frame = CGRect(x: bounds.maxX - dividerWidth, y: bounds.minY + dividerpPadding, width: 1, height: bounds.size.height - dividerpPadding * 2) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +@objc public protocol SegmentControlDelegate { + @objc optional func didSelected(segement: SegmentControl, index: Int) +} + + + + +open class SegmentControl: UIControl { + + fileprivate struct Constants { + static let height : CGFloat = 40.0 + } + + open weak var delegate : SegmentControlDelegate? + + open var autoAdjustWidth = false { + didSet{ + + } + } + + + open func segementWidth() -> CGFloat { + return bounds.size.width / (CGFloat)(itemViews.count) + } + + open func segmentWidth(index: Int) -> CGFloat { + guard index >= 0 && index < itemViews.count else { + return 0.0 + } + if autoAdjustWidth { + return itemViews[index].itemWidth() + } else{ + return segementWidth() + } + } + + open func selectedViewWidth(index: Int) -> CGFloat { + guard index >= 0 && index < itemViews.count else { + return segmentWidth(index: index) + } + if autoAdjustWidth { + return itemViews[index].itemWidth() - SegmentControlSetting.selectedViewpadding + } else{ + return segementWidth() - SegmentControlSetting.selectedViewpadding + } + } + + open func selectedViewHeight() -> CGFloat { + return 10 + } + + open var selectedIndex = 0 { + willSet{ + let originItem = self.itemViews[selectedIndex] + originItem.state = .Normal + + let selectItem = self.itemViews[newValue] + selectItem.state = .Selected + } + } + + + open var itemTextColor = SegmentControlSetting.itemTextColor{ + didSet{ + self.itemViews.forEach { (itemView) in + itemView.textColor = itemTextColor + } + } + } + + open var itemSelectedTextColor = SegmentControlSetting.itemSelectedTextColor{ + didSet{ + self.itemViews.forEach { (itemView) in + itemView.selectedTextColor = itemSelectedTextColor + } + } + } + open var itemBackgroundColor = SegmentControlSetting.itemBackgroundColor{ + didSet{ + self.itemViews.forEach { (itemView) in + itemView.itemBackgroundColor = itemBackgroundColor + } + } + } + + open var itemSelectedBackgroundColor = SegmentControlSetting.itemSelectedBackgroundColor{ + didSet{ + self.itemViews.forEach { (itemView) in + itemView.selectedBackgroundColor = itemSelectedBackgroundColor + } + } + } + + open var sliderViewColor = SegmentControlSetting.selectedViewBackgroundColor{ + didSet{ + self.selectedBackgroundView.color = sliderViewColor + } + } + + + open var font = SegmentControlSetting.textFont{ + didSet{ + self.itemViews.forEach { (itemView) in + itemView.font = font + } + } + } + + open var selectedFont = SegmentControlSetting.selectedTextFont{ + didSet{ + self.itemViews.forEach { (itemView) in + itemView.selectedFont = selectedFont + } + } + } + + open var items : [String]? { + didSet{ + guard items != nil && items!.count > 0 else { + fatalError("Items cannot be empty") + } + + self.removeAllItemView() + + + for title in items! { + let view = self.createItemView(title: title) + self.itemViews.append(view) + self.contentView.addSubview(view) + } + self.selectedIndex = 0 + + self.contentView.sendSubviewToBack(self.selectedBackgroundView) + } + } + + open func showBridge(show:Bool, index:Int){ + + guard index < itemViews.count && index >= 0 else { + return + } + + itemViews[index].showBridge(show: show) + } + + + + open var autoScrollWhenIndexChange = false + + open var isShowSeparator = false + + open var scrollToPointWhenIndexChanged : CGPoint? + + open var bounces = false { + didSet{ + self.scrollView.bounces = bounces + } + } + + fileprivate func removeAllItemView() { + itemViews.forEach { (label) in + label.removeFromSuperview() + } + itemViews.removeAll() + } + private var itemWidths = [CGFloat]() + private func createItemView(title:String) -> SegmentItemView { + return createItemView(title: title, + font: self.font, + selectedFont: self.selectedFont, + textColor: self.itemTextColor, + selectedTextColor: self.itemSelectedTextColor, + backgroundColor: UIColor.clear, + selectedBackgroundColor: self.itemSelectedBackgroundColor + ) + } + + private func createItemView(title:String, font:UIFont, selectedFont:UIFont, textColor:UIColor, selectedTextColor:UIColor, backgroundColor:UIColor, selectedBackgroundColor:UIColor) -> SegmentItemView { + let item = SegmentItemView() + + item.text = title + item.textColor = textColor + item.textAlignment = .center + item.font = font + item.selectedFont = selectedFont + + item.itemBackgroundColor = backgroundColor + item.selectedTextColor = selectedTextColor + item.selectedBackgroundColor = selectedBackgroundColor + + item.state = .Normal + return item + } + + fileprivate lazy var scrollView : UIScrollView = { + let scrollView = UIScrollView() + scrollView.alwaysBounceHorizontal = true + scrollView.alwaysBounceVertical = false + scrollView.showsHorizontalScrollIndicator = false + scrollView.showsVerticalScrollIndicator = false + scrollView.bounces = false + scrollView.backgroundColor = UIColor.clear + return scrollView + }() + fileprivate lazy var contentView = UIView() + + fileprivate lazy var selectedBackgroundView: SelectedBackgroundView = { + let selectedBackgroundView = SelectedBackgroundView.init() + + return selectedBackgroundView + }() + + + + + fileprivate var itemViews = [SegmentItemView]() + + fileprivate var numberOfSegments : Int { + return itemViews.count + } + + override init(frame: CGRect) { + super.init(frame: frame) + + setupViews() + + scrollToPointWhenIndexChanged = scrollView.center + } + + fileprivate func setupViews() { + + addSubview(scrollView) + scrollView.addSubview(contentView) + contentView.addSubview(selectedBackgroundView) + + + scrollView.frame = bounds + contentView.frame = scrollView.bounds + + scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + + addTapGesture() + } + + private func addTapGesture() { + let tap = UITapGestureRecognizer(target: self, action: #selector(didTapSegement(tapGesture:))) + + contentView.addGestureRecognizer(tap) + } + + @objc private func didTapSegement(tapGesture:UITapGestureRecognizer) { + let index = selectedTargetIndex(gesture: tapGesture) + move(to: index) + } + + open func move(to index:Int){ + move(to: index, animated: true) + } + + open func move(to index:Int, animated:Bool) { + + let position = centerX(with: index) - 10 + if animated { + UIView.animate(withDuration: 0.2, animations: { + self.selectedBackgroundView.center.x = position + }) { (finished) in + self.delegate?.didSelected?(segement: self, index: index) + self.selectedIndex = index + + if self.autoScrollWhenIndexChange { + self.scrollItemToPoint(index: index, point: self.scrollToPointWhenIndexChanged!) + } + } + + } else { + // 中心点 + self.selectedBackgroundView.center.x = position + self.selectedBackgroundView.center.y = self.center.y + + delegate?.didSelected?(segement: self, index: index) + selectedIndex = index + + if autoScrollWhenIndexChange { + scrollItemToPoint(index: index, point: scrollToPointWhenIndexChanged!) + } + } + + + } + + fileprivate func currentItemX(index:Int) -> CGFloat { + if autoAdjustWidth { + var x:CGFloat = 0.0 + for i in 0.. CGFloat { + if autoAdjustWidth { + return currentItemX(index: index) + segmentWidth(index: index)*0.5 + } + return (CGFloat(index) + 0.5)*segementWidth() + } + + private func selectedTargetIndex(gesture: UIGestureRecognizer) -> Int { + let location = gesture.location(in: contentView) + var index = 0 + + if autoAdjustWidth { + for (i,itemView) in itemViews.enumerated() { + if itemView.frame.contains(location) { + index = i + break + } + } + } else { + index = Int(location.x / segmentWidth(index: selectedIndex)) + } + + if index < 0 { + index = 0 + } + if index > numberOfSegments - 1 { + index = numberOfSegments - 1 + } + return index + } + + private func scrollItemToCenter(index : Int) { + scrollItemToPoint(index: index, point: CGPoint(x: scrollView.bounds.size.width * 0.5, y: 0)) + } + + private func scrollItemToPoint(index : Int,point:CGPoint) { + + let currentX = currentItemX(index: index) + + let scrollViewWidth = scrollView.bounds.size.width + + var scrollX = currentX - point.x + segmentWidth(index: index) * 0.5 + + let maxScrollX = scrollView.contentSize.width - scrollViewWidth + + if scrollX > maxScrollX { + scrollX = maxScrollX + } + if scrollX < 0.0 { + scrollX = 0.0 + } + + scrollView.setContentOffset(CGPoint(x: scrollX, y: 0.0), animated: true) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + open override func layoutSubviews() { + super.layoutSubviews() + + guard itemViews.count > 0 else { + return + } + + var x:CGFloat = 0.0 + let y:CGFloat = 0.0 + var width:CGFloat = segmentWidth(index: selectedIndex) + let height:CGFloat = bounds.size.height + + var contentWidth:CGFloat = 5.0 + + + selectedBackgroundView.frame = CGRect(x: currentItemX(index: selectedIndex) + SegmentControlSetting.selectedViewpadding, y: height - 22, width: 36, height: selectedViewHeight()) + + + self.selectedBackgroundView.center.x = centerX(with: 0) - 10 + + + for (index,item) in itemViews.enumerated() { + x = contentWidth + width = segmentWidth(index: index) + item.frame = CGRect(x: x, y: y, width: width, height: height) + + contentWidth += width + } + contentView.frame = CGRect(x: 0.0, y: 0.0, width: contentWidth, height: height) + scrollView.contentSize = contentView.bounds.size + + + } +} diff --git a/IndieMusic/IndieMusic/Models/Player.swift b/IndieMusic/IndieMusic/Models/Player.swift new file mode 100644 index 0000000..8f73a9d --- /dev/null +++ b/IndieMusic/IndieMusic/Models/Player.swift @@ -0,0 +1,28 @@ +// +// Player.swift +// IndieMusic +// +// Created by WenLei on 2023/11/15. +// + +import Foundation +import RxDataSources + +struct PlayerLyrics: Codable { + let lyrics: String? +} + + + +struct PlayerLyricsSection { + var items: [PlayerLyrics] +} + +extension PlayerLyricsSection: SectionModelType { + typealias Item = PlayerLyrics + + init(original: PlayerLyricsSection, items: [Item]) { + self = original + self.items = items + } +} diff --git a/IndieMusic/IndieMusic/Modules/Home/HomeViewController.swift b/IndieMusic/IndieMusic/Modules/Home/HomeViewController.swift index f5d4324..39df4d2 100644 --- a/IndieMusic/IndieMusic/Modules/Home/HomeViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Home/HomeViewController.swift @@ -37,6 +37,8 @@ class HomeViewController: ViewController { } override func touchesBegan(_ touches: Set, with event: UIEvent?) { + + let viewModel = PlayerViewModel.init(provider: viewModel!.provider) let play = PlayerViewController.init(viewModel: viewModel, navigator: navigator) play.modalPresentationStyle = .custom self.present(play, animated: true) diff --git a/IndieMusic/IndieMusic/Modules/Player/PlayerView.swift b/IndieMusic/IndieMusic/Modules/Player/PlayerView.swift index 7a480b4..0e05699 100644 --- a/IndieMusic/IndieMusic/Modules/Player/PlayerView.swift +++ b/IndieMusic/IndieMusic/Modules/Player/PlayerView.swift @@ -15,10 +15,14 @@ class PlayerViewTopBar: UIView { return dropButton }() - lazy var segementView: UIView = { - let segementView = UIView.init() + lazy var segmentControl: SegmentControl = { + let segmentControl = SegmentControl.init() + segmentControl.items = ["歌曲", "歌词"] + segmentControl.font = UIFont.systemFont(ofSize: 15) + segmentControl.selectedFont = UIFont.systemFont(ofSize: 15) - return segementView + + return segmentControl }() lazy var moreButton: UIButton = { @@ -41,7 +45,7 @@ class PlayerViewTopBar: UIView { func makeUI() { addSubview(dropButton) - addSubview(segementView) + addSubview(segmentControl) addSubview(moreButton) } @@ -60,10 +64,12 @@ class PlayerViewTopBar: UIView { make.centerY.equalTo(self) } - segementView.snp.makeConstraints { make in - make.left.equalTo(dropButton.snp.right).offset(10) - make.right.equalTo(moreButton.snp.left).offset(-10) + segmentControl.snp.makeConstraints { make in +// make.left.equalTo(dropButton.snp.right).offset(10) +// make.right.equalTo(moreButton.snp.left).offset(-10) + make.width.equalTo(145) make.top.equalTo(self) + make.centerX.equalTo(self) make.bottom.equalTo(self) } @@ -164,7 +170,9 @@ class PlayerControlView: UIView { } - +protocol PlayerScrollViewDelegate: NSObjectProtocol { + func scrollViewDidChange(index: Int) +} class PlayerScrollView: UIScrollView { var containerView: UIView = { @@ -180,13 +188,16 @@ class PlayerScrollView: UIScrollView { return playerInfoView }() - var lyricsView: UIView = { - let lyricsView = UIView.init() + var playerLyricsView: PlayerLyricsView = { + let playerLyricsView = PlayerLyricsView.init() - return lyricsView + return playerLyricsView }() + weak var scrollDelegate: PlayerScrollViewDelegate? + + override init(frame: CGRect) { super.init(frame: frame) @@ -198,10 +209,14 @@ class PlayerScrollView: UIScrollView { } func makeUI() { + showsHorizontalScrollIndicator = false + isPagingEnabled = true + delegate = self + addSubview(containerView) containerView.addSubview(playerInfoView) - containerView.addSubview(lyricsView) + containerView.addSubview(playerLyricsView) } override func layoutSubviews() { @@ -220,20 +235,202 @@ class PlayerScrollView: UIScrollView { make.width.equalTo(BaseDimensions.screenWidth) } - lyricsView.snp.makeConstraints { make in + playerLyricsView.snp.makeConstraints { make in make.left.equalTo(playerInfoView.snp.right) make.top.equalTo(containerView) make.bottom.equalTo(containerView) make.right.equalTo(containerView) + make.width.equalTo(BaseDimensions.screenWidth) + } } + func moveToPage(page: Int, animated: Bool) { + let width = frame.size.width + let offset = CGPoint(x: Int(width) * page, y: 0) + setContentOffset(offset, animated: animated) + } + +} + +extension PlayerScrollView: UIScrollViewDelegate { +// func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { +// let pageWidth = BaseDimensions.screenWidth // 自定义的页面宽度 +// let targetXContentOffset = targetContentOffset.pointee.x +// let contentWidth = scrollView.contentSize.width +// var newPage = targetXContentOffset / pageWidth +// +// if velocity.x == 0 { // 没有滑动速度时 +// newPage = floor((targetXContentOffset - pageWidth / 2) / pageWidth) + 1.0 +// } else { +// newPage = velocity.x > 0 ? ceil(newPage) : floor(newPage) +// } +// +// // 防止新页面索引超出范围 +// newPage = max(0, min(newPage, ceil(contentWidth / pageWidth) - 1)) +// targetContentOffset.pointee.x = CGFloat(newPage * pageWidth) +// } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + let width = scrollView.frame.size.width + let pageIndex = Int(round(scrollView.contentOffset.x / width)) + + scrollDelegate?.scrollViewDidChange(index: pageIndex) + + } } + +class PlayerLyricsView: UIView { + lazy var tableView: UITableView = { + let tableView = UITableView.init() + tableView.backgroundColor = .clear + + return tableView + }() + + lazy var audioTrackView: UIView = { + let artistInfoView = UIView.init() + + return artistInfoView + }() + + + lazy var audioTrackLabel: UILabel = { + let audioTrackLabel = UILabel.init() + audioTrackLabel.font = UIFont.systemFont(ofSize: 20) + audioTrackLabel.textColor = UIColor.init(hex: 0xFFFFFF) + + return audioTrackLabel + }() + + lazy var artistLabel: UILabel = { + let artistLabel = UILabel.init() + artistLabel.font = UIFont.systemFont(ofSize: 12) + artistLabel.textColor = UIColor.init(hex: 0xFFFFFF, alpha: 0.6) + + return artistLabel + }() + + + override init(frame: CGRect) { + super.init(frame: frame) + + makeUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func makeUI() { +// audioTrackView.backgroundColor = .red +// tableView.backgroundColor = .blue + + addSubview(audioTrackView) + addSubview(tableView) + + audioTrackView.addSubview(audioTrackLabel) + audioTrackView.addSubview(artistLabel) + + audioTrackLabel.text = "test" + artistLabel.text = "tester" + } + + override func layoutSubviews() { + super.layoutSubviews() + + + audioTrackLabel.snp.makeConstraints { make in + make.left.equalTo(audioTrackView).offset(18) + make.right.equalTo(audioTrackView).offset(-18) + make.top.equalTo(audioTrackView).offset(30) + } + + artistLabel.snp.makeConstraints { make in + make.left.equalTo(audioTrackView).offset(18) + make.right.equalTo(audioTrackView).offset(-18) + make.top.equalTo(audioTrackLabel.snp.bottom) + } + + + audioTrackView.snp.makeConstraints { make in + make.left.equalTo(self) + make.right.equalTo(self) + make.top.equalTo(self) + make.height.equalTo(100) + } + + tableView.snp.makeConstraints { make in + make.left.equalTo(self) + make.right.equalTo(self) + make.top.equalTo(audioTrackView.snp.bottom) + make.bottom.equalTo(self) + } + } +} + +class PlayerLyricsCell: UITableViewCell { + var lyricsLabel: UILabel = { + let lyricsLabel = UILabel.init() + lyricsLabel.font = UIFont.systemFont(ofSize: 18) + lyricsLabel.textColor = .init(hex: 0xFFFFFF, alpha: 1) + + + return lyricsLabel + }() + + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + self.selectionStyle = .none + + makeUI() + autoLayout() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func makeUI() { + backgroundColor = .clear +// contentView.backgroundColor = .clear + + contentView.addSubview(lyricsLabel) + + } + + func autoLayout() { + lyricsLabel.snp.makeConstraints { make in + make.left.equalTo(contentView).offset(18) + make.right.equalTo(contentView).offset(-18) + make.top.equalTo(contentView).offset(18) + make.bottom.equalTo(contentView).offset(-18) + } + + } + +} + + + class PlayerInfoView: UIView { var coverView: UIImageView = { let coverView = UIImageView.init() diff --git a/IndieMusic/IndieMusic/Modules/Player/PlayerViewController.swift b/IndieMusic/IndieMusic/Modules/Player/PlayerViewController.swift index 429eca3..6c7fb2c 100644 --- a/IndieMusic/IndieMusic/Modules/Player/PlayerViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Player/PlayerViewController.swift @@ -6,6 +6,9 @@ // import UIKit +import RxSwift +import RxCocoa +import RxDataSources class PlayerViewController: ViewController { @@ -20,12 +23,12 @@ class PlayerViewController: ViewController { playerViewTopBar.setContentHuggingPriority(.required, for: .vertical) playerViewTopBar.setContentCompressionResistancePriority(.required, for: .vertical) + return playerViewTopBar }() var playerScrollView: PlayerScrollView = { let playerScrollView = PlayerScrollView.init() - return playerScrollView }() @@ -44,6 +47,8 @@ class PlayerViewController: ViewController { override func viewDidLoad() { super.viewDidLoad() + + } override func viewWillAppear(_ animated: Bool) { @@ -55,6 +60,10 @@ class PlayerViewController: ViewController { override func makeUI() { super.makeUI() + playerScrollView.scrollDelegate = self + playerViewTopBar.segmentControl.delegate = self + + view.backgroundColor = .white view.addSubview(blurEffectView) @@ -67,6 +76,22 @@ class PlayerViewController: ViewController { override func bindViewModel() { super.bindViewModel() + playerScrollView.playerLyricsView.tableView.register(PlayerLyricsCell.self, forCellReuseIdentifier: "PlayerLyricsCell") + + + guard let viewModel = viewModel as? PlayerViewModel else { return } + + let input = PlayerViewModel.Input.init() + let output = viewModel.transform(input: input) + + let dataSource = PlayerViewController.dataSource { cell, oneStepInfoModel in + + } + output.items.bind(to: playerScrollView.playerLyricsView.tableView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag) + + + + } override func viewDidLayoutSubviews() { @@ -103,3 +128,31 @@ class PlayerViewController: ViewController { self.dismiss(animated: true) } } + +extension PlayerViewController { + static func dataSource(_ buttonTapHandler: @escaping (PlayerLyricsCell, PlayerLyrics) -> Void) -> RxTableViewSectionedReloadDataSource { + return RxTableViewSectionedReloadDataSource { dataSource, tableView, indexPath, item in + let cell: PlayerLyricsCell = tableView.dequeueReusableCell(withIdentifier: "PlayerLyricsCell", for: indexPath) as! PlayerLyricsCell + cell.lyricsLabel.text = item.lyrics + return cell + + } + + } + + + +} + +extension PlayerViewController: SegmentControlDelegate { + func didSelected(segement: SegmentControl, index: Int) { + + playerScrollView.moveToPage(page: index, animated: true) + } +} + +extension PlayerViewController: PlayerScrollViewDelegate { + func scrollViewDidChange(index: Int) { + playerViewTopBar.segmentControl.move(to: index, animated: true) + } +} diff --git a/IndieMusic/IndieMusic/Modules/Player/PlayerViewModel.swift b/IndieMusic/IndieMusic/Modules/Player/PlayerViewModel.swift new file mode 100644 index 0000000..c52c851 --- /dev/null +++ b/IndieMusic/IndieMusic/Modules/Player/PlayerViewModel.swift @@ -0,0 +1,34 @@ +// +// PlayerViewModel.swift +// IndieMusic +// +// Created by WenLei on 2023/11/15. +// + +import Foundation +import RxSwift +import RxCocoa + +class PlayerViewModel: ViewModel, ViewModelType { + + struct Input { + } + + struct Output { + let items: BehaviorRelay<[PlayerLyricsSection]> + + } + + let items = BehaviorRelay<[PlayerLyricsSection]>.init(value: []) + + func transform(input: Input) -> Output { + + let lyrics = PlayerLyrics.init(lyrics: "1233211232") + let section = PlayerLyricsSection.init(items: [lyrics, lyrics, lyrics, lyrics, lyrics, lyrics, lyrics]) + + items.accept([section]) + + return Output.init(items: items) + } + +} diff --git a/IndieMusic/Podfile b/IndieMusic/Podfile index cf860a1..fe4ce0c 100644 --- a/IndieMusic/Podfile +++ b/IndieMusic/Podfile @@ -14,14 +14,15 @@ target 'IndieMusic' do pod 'CocoaLumberjack/Swift' pod 'KeychainAccess' - + pod 'NSObject+Rx' pod 'RxViewController' pod 'RxSwift', '6.2.0' pod 'RxCocoa', '6.2.0' pod 'Moya/RxSwift', '~> 15.0' pod 'RxTheme', '~> 6.0' - + pod 'RxDataSources' + target 'IndieMusicTests' do inherit! :search_paths # Pods for testing