// // YFLDragCardContainer.swift // Crush // // Created by AI Assistant on 2024/12/19. // Copyright © 2024年 Crush. All rights reserved. // import UIKit import SnapKit // MARK: - 数据源协议 protocol YFLDragCardContainerDataSource: AnyObject { /// 数据源个数 func numberOfRowsInYFLDragCardContainer(_ container: YFLDragCardContainer) -> Int /// 显示数据源 func container(_ container: YFLDragCardContainer, viewForRowsAt index: Int) -> YFLDragCardView } // MARK: - 代理协议 protocol YFLDragCardContainerDelegate: AnyObject { /// 点击卡片回调 func container(_ container: YFLDragCardContainer, didSelectRowAt index: Int) /// 拖到最后一张卡片 YES,空,可继续调用reloadData分页数据 func container(_ container: YFLDragCardContainer, dataSourceIsEmpty isEmpty: Bool) /// 当前cardview 是否可以拖拽,默认YES func container(_ container: YFLDragCardContainer, canDragForCardView cardView: YFLDragCardView) -> Bool /// 卡片处于拖拽中回调 func container(_ container: YFLDragCardContainer, dargingForCardView cardView: YFLDragCardView, direction: ContainerDragDirection, widthRate: Float, heightRate: Float) /// 询问这次滑动的方向是否生效,控制 v2.6 ld add func container(_ container: YFLDragCardContainer, canDragFinishForDirection direction: ContainerDragDirection, forCardView cardView: YFLDragCardView) -> Bool /// 卡片拖拽结束回调(卡片消失) func container(_ container: YFLDragCardContainer, dragDidFinshForDirection direction: ContainerDragDirection, forCardView cardView: YFLDragCardView) /// 卡片回看 v2.6 添加 func container(_ container: YFLDragCardContainer, lookingBack direction: ContainerDragDirection, forCardView cardView: YFLDragCardView) func container(_ container: YFLDragCardContainer, enterSmallCardMode smallCardMode: Bool, forCardView cardView: YFLDragCardView) } // MARK: - 主容器类 class YFLDragCardContainer: UIView { // MARK: - 公开属性 private(set) var tmpStoreNopeCardView: YFLDragCardView? private(set) var smallCardMode: Bool = false private(set) var isMoveIng: Bool = false weak var dataSource: YFLDragCardContainerDataSource? weak var delegate: YFLDragCardContainerDelegate? // MARK: - 私有属性 private var cards: [YFLDragCardView] = [] private var direction: ContainerDragDirection = .default private var loadedIndex: Int = 0 private var firstCardFrame: CGRect = .zero private var lastCardFrame: CGRect = .zero private var cardCenter: CGPoint = .zero private var lastCardTransform: CGAffineTransform = .identity private var configure: YFLDragConfigure! // 🔥 根据上下文推测:动画相关视图 private var likeOrDislikeShowLogoView: UIView! private var likeOrDislikeShowImageView: UIImageView! private var likeLottieView: UIView! // 🔥 推测为Lottie动画视图 private var dislikeLottieView: UIView! // �� 推测为Lottie动画视图 private var superlikeLottieView: UIView! // 🔥 推测为Lottie动画视图 private var superlikeWordsShowBg: UIImageView! private var superlikeWordsLabel: UILabel! private var superLikeCountLeft: UILabel! // v2.8 private var currentIntialGestureDirection: ContainerDragDirection = .default private var canMoveView: Bool = true // MARK: - 初始化方法 convenience init(frame: CGRect) { self.init(frame: frame, configure: YFLDragCardContainer.setDefaultsConfigures()) } init(frame: CGRect, configure: YFLDragConfigure) { super.init(frame: frame) initDataConfigure(configure) } required init?(coder: NSCoder) { super.init(coder: coder) initDataConfigure(YFLDragCardContainer.setDefaultsConfigures()) } // MARK: - 私有方法 private static func setDefaultsConfigures() -> YFLDragConfigure { let configure = YFLDragConfigure() configure.visableCount = 3 configure.containerEdge = 16.0 configure.cardEdge = 0.01 configure.cardCornerRadius = 8.0 configure.cardCornerBorderWidth = 0.0 configure.cardBordColor = UIColor.clear configure.cardVTopEdage = 0 configure.cardVBottomEdage = 12 return configure } private func initDataConfigure(_ configure: YFLDragConfigure) { resetInitData() initialLikeDislikesShowViews() cards = [] backgroundColor = UIColor.white self.configure = configure } private func initialLikeDislikesShowViews() { likeOrDislikeShowLogoView = UIView() likeOrDislikeShowLogoView.backgroundColor = UIColor.clear likeOrDislikeShowLogoView.isUserInteractionEnabled = false likeOrDislikeShowLogoView.alpha = 0 likeOrDislikeShowLogoView.layer.zPosition = 10 insertSubview(likeOrDislikeShowLogoView, at: 0) likeOrDislikeShowLogoView.snp.makeConstraints { make in make.center.equalToSuperview() } // 🔥 根据上下文推测:创建Lottie动画视图 likeLottieView = createLottieView(animationName: "LottieResources/kuolie_swipe_like") dislikeLottieView = createLottieView(animationName: "LottieResources/kuolie_swipe_dislike") superlikeLottieView = createLottieView(animationName: "LottieResources/kuolie_superlike") // 🔥 根据上下文推测:创建超级喜欢文字背景 superlikeWordsShowBg = UIImageView() superlikeWordsShowBg.image = UIImage(named: "kuolie_superlike_words_bg") superlikeLottieView.addSubview(superlikeWordsShowBg) superlikeWordsShowBg.snp.makeConstraints { make in make.centerX.equalToSuperview() make.bottom.equalToSuperview().offset(8) make.size.equalTo(CGSize(width: 186, height: 32)) } // 🔥 根据上下文推测:创建超级喜欢文字标签 superlikeWordsLabel = UILabel() superlikeWordsLabel.textColor = UIColor.white superlikeWordsLabel.font = UIFont.systemFont(ofSize: 16) // 🔥 推测字体 superlikeWordsLabel.text = "SUPER LIKE" // 🔥 推测文字 superlikeLottieView.addSubview(superlikeWordsLabel) superlikeWordsLabel.snp.makeConstraints { make in make.center.equalTo(superlikeWordsShowBg) } } private func createLottieView(animationName: String) -> UIView { // 🔥 根据上下文推测:创建Lottie动画视图 let view = UIView() view.contentMode = .scaleAspectFill likeOrDislikeShowLogoView.addSubview(view) view.isHidden = true view.snp.makeConstraints { make in make.center.equalTo(likeOrDislikeShowLogoView) make.size.equalTo(CGSize(width: 120, height: 120)) } return view } /// 重置数据(为了二次调用reload) private func resetInitData() { loadedIndex = 0 resetCards() direction = .default isMoveIng = false } /// 清掉所有card view private func resetCards() { for view in cards { view.removeFromSuperview() } cards.removeAll() } /// 添加子视图 private func addSubViews() { guard let dataSource = dataSource else { return } let sum = dataSource.numberOfRowsInYFLDragCardContainer(self) let preLoadViewCount = min(sum, configure.visableCount) // 预防越界 if loadedIndex < sum { // 当手势滑动,加载第四个,最多创建4个。不存在内存warning。(手势停止,滑动的view没消失,需要干掉多创建的+1) let targetCount = isMoveIng ? preLoadViewCount + 1 : preLoadViewCount for i in cards.count..= 3 { cardView.frame = lastCardFrame } else { let frame = cardView.frame if firstCardFrame.isEmpty { firstCardFrame = frame cardCenter = cardView.center } } } /// 移动卡片 private func moveIngStatusChange(_ scale: Float) { // 如果正在移动,添加第四个 if !isMoveIng { isMoveIng = true addSubViews() } else { // 第四个加载完,立马改变没作用在手势上其他cardview的scale let absScale = min(abs(scale), boundaryRation) let transFormtxPoor = (secondCardScale - thirdCardScale) / (boundaryRation / absScale) let frameYPoor: CGFloat = 0 for (index, cardView) in cards.enumerated() { guard index > 0 else { continue } switch index { case 1: let scaleTransform = CGAffineTransform(scaleX: transFormtxPoor + secondCardScale, y: 1) let translateTransform = CGAffineTransform(translationX: 0, y: -frameYPoor) cardView.transform = scaleTransform.concatenating(translateTransform) case 2: let scaleTransform = CGAffineTransform(scaleX: transFormtxPoor + thirdCardScale, y: 1) let translateTransform = CGAffineTransform(translationX: 0, y: -frameYPoor) cardView.transform = scaleTransform.concatenating(translateTransform) case 3: cardView.transform = lastCardTransform default: break } } } } // 手势结束 private func panGesturemMoveFinishOrCancle(_ cardView: YFLDragCardView, direction: ContainerDragDirection, scale: Float, isDisappear: Bool) { if !isDisappear { // 没有消失,恢复原样 if currentIntialGestureDirection == .left || currentIntialGestureDirection == .right || currentIntialGestureDirection == .default { UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.6, options: [.allowUserInteraction, .curveEaseInOut]) { self.subRestoreCardState() } completion: { finished in } } else { // 其他情况下没有动画 subRestoreCardState() } } else { // v2.6 if let delegate = delegate { if !delegate.container(self, canDragFinishForDirection: direction, forCardView: cardView) { subRestoreCardState() return } } delegate?.container(self, dragDidFinshForDirection: self.direction, forCardView: cardView) if direction == .left { tmpStoreNopeCardView = cardView } let flag = direction == .left ? -1 : 2 let screenWidth = UIScreen.main.bounds.width UIView.animate(withDuration: 0.5, delay: 0.0, options: [.curveLinear, .allowUserInteraction]) { self.isMoveIng = true cardView.center = CGPoint(x: CGFloat(flag) * screenWidth, y: CGFloat(flag) * screenWidth / CGFloat(scale) + self.cardCenter.y) } completion: { finished in self.isMoveIng = false cardView.removeFromSuperview() } if let index = cards.firstIndex(of: cardView) { cards.remove(at: index) } isMoveIng = false resetLayoutSubviews() } } private func subRestoreCardState() { // 干掉多创建的第四个.重置标量 if isMoveIng && cards.count > configure.visableCount { if let lastView = cards.last { lastView.removeFromSuperview() cards.removeLast() loadedIndex = lastView.tag } } isMoveIng = false resetLayoutSubviews() } // MARK: - 公开方法 func reloadData() { guard let dataSource = dataSource else { assertionFailure("check dataSource and dataSource Methods!") return } resetInitData() addSubViews() resetLayoutSubviews() } func getCurrentShowCardView() -> YFLDragCardView? { return cards.first } func getCurrentShowCardViewIndex() -> Int { return cards.first?.tag ?? 0 } /// 手动控制滑动,且不回调代理方法 func removeCardViewForDirection(_ direction: ContainerDragDirection) { guard !isMoveIng else { return } let screenWidth = UIScreen.main.bounds.width var cardCenter = CGPoint.zero var flag = 0 guard let currentShowCardView = cards.first else { return } switch direction { case .left: cardCenter = CGPoint(x: -screenWidth / 2.0, y: self.cardCenter.y) flag = -1 tmpStoreNopeCardView = currentShowCardView case .right: cardCenter = CGPoint(x: screenWidth * 1.5, y: self.cardCenter.y) flag = 1 default: break } UIView.animate(withDuration: 0.35) { self.isMoveIng = true let translate = CGAffineTransform(translationX: CGFloat(flag) * 20, y: 0) currentShowCardView.transform = translate.rotated(by: CGFloat(flag) * .pi / 4 / 4) currentShowCardView.center = cardCenter } completion: { finished in self.isMoveIng = false currentShowCardView.removeFromSuperview() if let index = self.cards.firstIndex(of: currentShowCardView) { self.cards.remove(at: index) } self.addSubViews() self.resetLayoutSubviews() } } /// app控制滑动,且回调滑动Finish代理 func removeCardViewWithCallDelegateForDirection(_ direction: ContainerDragDirection) { guard !isMoveIng else { return } let screenWidth = UIScreen.main.bounds.width var cardCenter = CGPoint.zero var flag = 0 guard let currentShowCardView = cards.first else { return } switch direction { case .left: cardCenter = CGPoint(x: -screenWidth / 2.0, y: self.cardCenter.y) flag = -1 tmpStoreNopeCardView = currentShowCardView case .right: cardCenter = CGPoint(x: screenWidth * 1.5, y: self.cardCenter.y) flag = 1 default: break } UIView.animate(withDuration: 0.35) { let translate = CGAffineTransform(translationX: CGFloat(flag) * 20, y: 0) currentShowCardView.transform = translate.rotated(by: CGFloat(flag) * .pi / 4 / 4) currentShowCardView.center = cardCenter } completion: { finished in currentShowCardView.removeFromSuperview() if let index = self.cards.firstIndex(of: currentShowCardView) { self.cards.remove(at: index) } self.addSubViews() self.resetLayoutSubviews() } delegate?.container(self, dragDidFinshForDirection: direction, forCardView: currentShowCardView) } /// 手动回看 Meet 卡片,将之前存的临时CardView 回看回来 func addCardView(_ cardView: YFLDragCardView?, fromDirection direction: ContainerDragDirection) { guard !isMoveIng else { return } let targetCardView = cardView ?? tmpStoreNopeCardView guard let cardView = targetCardView else { return } let screenWidth = UIScreen.main.bounds.width var cardCenter = CGPoint.zero var flag = 0 switch direction { case .left: cardCenter = CGPoint(x: -screenWidth / 2.0, y: self.cardCenter.y) flag = -1 tmpStoreNopeCardView = cardView case .right: cardCenter = CGPoint(x: screenWidth * 1.5, y: self.cardCenter.y) flag = 1 default: break } cards.insert(cardView, at: 0) addSubview(cardView) cardView.center = cardCenter let translate = CGAffineTransform(translationX: CGFloat(flag) * 20, y: 0) cardView.transform = translate.rotated(by: CGFloat(flag) * .pi / 4 / 4) cardCenter = CGPoint(x: self.cardCenter.x, y: self.cardCenter.y) UIView.animate(withDuration: 0.35) { cardView.transform = .identity cardView.center = cardCenter self.isMoveIng = true } completion: { finished in self.isMoveIng = false self.tmpStoreNopeCardView = nil } delegate?.container(self, lookingBack: direction, forCardView: cardView) } func showNopeLogo(_ show: Bool, widthRate: CGFloat) { if widthRate == 0 { if !isLottieAnimationPlaying(dislikeLottieView) { // 🔥 推测方法 UIView.animate(withDuration: 0.25) { self.likeOrDislikeShowLogoView.alpha = 0 } } return } if show { likeLottieView.isHidden = true superlikeLottieView.isHidden = true dislikeLottieView.isHidden = false likeOrDislikeShowLogoView.isHidden = false likeOrDislikeShowLogoView.alpha = widthRate > 0.2 ? 1 : (widthRate / 0.2) superLikeCountLeft.isHidden = true } else { UIView.animate(withDuration: 0.45) { self.likeOrDislikeShowLogoView.alpha = 0 } } } func showLikeLogo(_ show: Bool, widthRate: CGFloat) { if widthRate == 0 { if !isLottieAnimationPlaying(likeLottieView) { // 🔥 推测方法 UIView.animate(withDuration: 0.25) { self.likeOrDislikeShowLogoView.alpha = 0 } } return } if show { likeLottieView.isHidden = false dislikeLottieView.isHidden = true superlikeLottieView.isHidden = true likeOrDislikeShowLogoView.alpha = widthRate > 0.2 ? 1 : (widthRate / 0.2) superLikeCountLeft.isHidden = true } else { UIView.animate(withDuration: 0.45) { self.likeOrDislikeShowLogoView.alpha = 0 } } } func showNopeLottie() { likeLottieView.isHidden = true superlikeLottieView.isHidden = true dislikeLottieView.isHidden = false stopLottieAnimation(dislikeLottieView) // 🔥 推测方法 likeOrDislikeShowLogoView.alpha = 1 playLottieAnimation(dislikeLottieView) { [weak self] finish in if finish { self?.likeOrDislikeShowLogoView.alpha = 0 } } } func showLikeLottie() { dislikeLottieView.isHidden = true superlikeLottieView.isHidden = true likeLottieView.isHidden = false stopLottieAnimation(likeLottieView) // 🔥 推测方法 likeOrDislikeShowLogoView.alpha = 1 playLottieAnimation(likeLottieView) { [weak self] finish in if finish { self?.likeOrDislikeShowLogoView.alpha = 0 } } } func showSuperLikeLottie(complete: @escaping (Bool) -> Void) { dislikeLottieView.isHidden = true likeLottieView.isHidden = true superlikeLottieView.isHidden = false stopLottieAnimation(superlikeLottieView) // 🔥 推测方法 likeOrDislikeShowLogoView.alpha = 1 playLottieAnimation(superlikeLottieView) { [weak self] finish in self?.likeOrDislikeShowLogoView.alpha = 0 complete(finish) } } func addAdaptForDragCardView(_ cardView: YFLDragCardView) { let y = configure.containerEdge + configure.cardVTopEdage let width = frame.size.width - 2 * configure.containerEdge let height = frame.size.height - 2 * configure.containerEdge - configure.cardVTopEdage - configure.cardVBottomEdage cardView.frame = CGRect(x: configure.containerEdge, y: y, width: width, height: height) cardView.setConfigure(configure) cardView.YFLDragCardViewLayoutSubviews() recordFrame(cardView) cardView.tag = loadedIndex // 添加拖拽手势 let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) cardView.addGestureRecognizer(panGesture) addSubview(cardView) sendSubviewToBack(cardView) } func clearTmpStoreNopeCardView() { tmpStoreNopeCardView?.removeFromSuperview() tmpStoreNopeCardView = nil } func switchAllCardsToSmallCardMode(_ smallMode: Bool) { guard smallCardMode != smallMode else { return } smallCardMode = smallMode delegate?.container(self, enterSmallCardMode: smallCardMode, forCardView: getCurrentShowCardView() ?? YFLDragCardView()) } // MARK: - 手势处理 @objc private func handleTapGesture(_ tap: UITapGestureRecognizer) { delegate?.container(self, didSelectRowAt: tap.view?.tag ?? 0) } @objc private func handlePanGesture(_ pan: UIPanGestureRecognizer) { guard let cardView = pan.view as? YFLDragCardView else { return } let canEdit = delegate?.container(self, canDragForCardView: cardView) ?? true if canEdit { switch pan.state { case .began: let point = pan.translation(in: self) doVDirectionLogicByPoint(point, forCardView: cardView) case .changed: guard currentIntialGestureDirection != .up && currentIntialGestureDirection != .down else { return } if let delegate = delegate { // 计算横向滑动比例 >0 向右 <0 向左 let horizionSliderRate = Float((pan.view?.center.x ?? 0) - cardCenter.x) / Float(cardCenter.x) let verticalSliderRate = Float((pan.view?.center.y ?? 0) - cardCenter.y) / Float(cardCenter.y) // 正在滑动,需要创建第四个。 moveIngStatusChange(horizionSliderRate) if currentIntialGestureDirection == .default { // 再次决定方向 let point = pan.translation(in: self) doVDirectionLogicByPoint(point, forCardView: cardView) } else { // 已经确定了方向,且走到这儿,肯定是左右滑 // 以自身的左上角为原点;每次移动后,原点都置0;计算的是相对于上一个位置的偏移; let point = pan.translation(in: self) cardView.center = CGPoint(x: (pan.view?.center.x ?? 0) + point.x * 0.5, y: pan.view?.center.y ?? 0) // 当angle为正值时,逆时针旋转坐标系统,反之顺时针旋转坐标系统 let rotationAngle = ((pan.view?.center.x ?? 0) - cardCenter.x) / cardCenter.x * (.pi / 4 / 12) cardView.transform = cardView.originTransForm.rotated(by: rotationAngle) pan.setTranslation(.zero, in: self) // 设置坐标原点位上次的坐标 } if horizionSliderRate > 0 { direction = .right } else if horizionSliderRate < 0 { direction = .left } else { direction = .default } if currentIntialGestureDirection == .default { currentIntialGestureDirection = direction } delegate.container(self, dargingForCardView: cardView, direction: direction, widthRate: horizionSliderRate, heightRate: verticalSliderRate) } case .ended, .cancelled: // 还原card位置,或者消失card let horizionSliderRate = Float((pan.view?.center.x ?? 0) - cardCenter.x) / Float(cardCenter.x) let moveY = (pan.view?.center.y ?? 0) - cardCenter.y let moveX = (pan.view?.center.x ?? 0) - cardCenter.x panGesturemMoveFinishOrCancle(cardView, direction: direction, scale: Float(moveX / moveY), isDisappear: abs(horizionSliderRate) > boundaryRation) for per in cards { // 上下滑完后,不隐藏后面的cardView per.isHidden = false } currentIntialGestureDirection = .default default: break } } } // MARK: - 辅助方法 private func doVDirectionLogicByPoint(_ point: CGPoint, forCardView cardView: YFLDragCardView) { // option2: 不单独处理竖直方向手势 if point.x == 0 { currentIntialGestureDirection = .default } else if point.x < 0 { currentIntialGestureDirection = .left } else { currentIntialGestureDirection = .right } } // 🔥 根据上下文推测的Lottie动画相关方法 private func isLottieAnimationPlaying(_ view: UIView) -> Bool { // 🔥 推测:检查Lottie动画是否正在播放 return false } private func stopLottieAnimation(_ view: UIView) { // 🔥 推测:停止Lottie动画 } private func playLottieAnimation(_ view: UIView, completion: @escaping (Bool) -> Void) { // 🔥 推测:播放Lottie动画 DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { completion(true) } } }