// // MeetDragCardContainer.swift // Crush // // Created by AI Assistant on 2024/12/19. // Copyright © 2024年 Crush. All rights reserved. // import UIKit import SnapKit import Lottie // MARK: - 数据源协议 protocol MeetDragCardContainerDataSource: AnyObject { /// 数据源个数 func numberOfRowsInYFLDragCardContainer(_ container: MeetDragCardContainer) -> Int /// 显示数据源 func container(_ container: MeetDragCardContainer, viewForRowsAt index: Int) -> MeetDragCardView } // MARK: - 代理协议 protocol MeetDragCardContainerDelegate: AnyObject { /// 点击卡片回调(⚠️废弃) func container(_ container: MeetDragCardContainer, didSelectRowAt index: Int) /// 拖到最后一张卡片 YES,空,可继续调用reloadData分页数据 func container(_ container: MeetDragCardContainer, dataSourceIsEmpty isEmpty: Bool) /// 当前cardview 是否可以拖拽,默认YES func container(_ container: MeetDragCardContainer, canDragForCardView cardView: MeetDragCardView) -> Bool /// 卡片处于拖拽中回调 func container(_ container: MeetDragCardContainer, dargingForCardView cardView: MeetDragCardView, direction: ContainerDragDirection, widthRate: CGFloat, heightRate: CGFloat) /// 询问这次滑动的方向是否生效,控制 v2.6 ld add func container(_ container: MeetDragCardContainer, canDragFinishForDirection direction: ContainerDragDirection, forCardView cardView: MeetDragCardView) -> Bool /// 卡片拖拽结束回调(卡片消失) func container(_ container: MeetDragCardContainer, dragDidFinshForDirection direction: ContainerDragDirection, forCardView cardView: MeetDragCardView) /// 卡片回看 v2.6 添加 func container(_ container: MeetDragCardContainer, lookingBack direction: ContainerDragDirection, forCardView cardView: MeetDragCardView) func container(_ container: MeetDragCardContainer, enterSmallCardMode smallCardMode: Bool, forCardView cardView: MeetDragCardView) } // MARK: - 主容器类 class MeetDragCardContainer: UIView { // MARK: - 公开属性 private(set) var tmpStoreNopeCardView: MeetDragCardView? private(set) var smallCardMode: Bool = false private(set) var isMoveIng: Bool = false weak var dataSource: MeetDragCardContainerDataSource? weak var delegate: MeetDragCardContainerDelegate? // MARK: - 私有属性 private var cards: [MeetDragCardView] = [] 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: MeetDragConfigure! private var likeOrDislikeShowLogoView: UIView! private var likeShowImageView: UIImageView! private var dislikeShowImageView: UIImageView! // private var likeLottieView: LottieAnimationView! // private var dislikeLottieView: LottieAnimationView! // v2.8 private var currentIntialGestureDirection: ContainerDragDirection = .default private var canMoveView: Bool = true // MARK: - 初始化方法 convenience override init(frame: CGRect) { self.init(frame: frame, configure: MeetDragCardContainer.setDefaultsConfigures()) } init(frame: CGRect, configure: MeetDragConfigure) { super.init(frame: frame) initDataConfigure(configure) } required init?(coder: NSCoder) { super.init(coder: coder) initDataConfigure(MeetDragCardContainer.setDefaultsConfigures()) } // MARK: - 私有方法 private static func setDefaultsConfigures() -> MeetDragConfigure { let configure = MeetDragConfigure() configure.visableCount = 3 configure.containerEdge = 0//16.0 configure.cardEdge = 0.01 configure.cardCornerRadius = 48.0 configure.cardCornerBorderWidth = 0.0 configure.cardBordColor = UIColor.clear configure.cardVTopEdage = 0 configure.cardVBottomEdage = 0//12 return configure } private func initDataConfigure(_ configure: MeetDragConfigure) { resetInitData() initialLikeDislikesShowViews() cards = [] 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.edges.equalToSuperview() } likeShowImageView = { let v = UIImageView() v.image = UIImage(named: "meet_big_like") likeOrDislikeShowLogoView.addSubview(v) v.snp.makeConstraints { make in make.centerX.equalToSuperview() make.bottom.equalToSuperview().offset(-143) } v.isHidden = true return v }() dislikeShowImageView = { let v = UIImageView() v.image = UIImage(named: "meet_big_dislike") likeOrDislikeShowLogoView.addSubview(v) v.snp.makeConstraints { make in make.centerX.equalToSuperview() make.bottom.equalToSuperview().offset(-143) } v.isHidden = true return v }() // likeLottieView = { // let animation = LottieAnimation.named("meet_right_swipe_like") // let v = LottieAnimationView(animation: animation) // // v.contentMode = .scaleAspectFit // v.loopMode = .playOnce // v.backgroundBehavior = .pauseAndRestore // v.backgroundColor = .clear // likeOrDislikeShowLogoView.addSubview(v) // let size = CGSize(width: 200, height: 200) // v.size = size // v.snp.makeConstraints { make in // make.centerX.equalToSuperview() // make.bottom.equalToSuperview().offset(-143) // make.size.equalTo(size) // } // v.isHidden = true // return v // }() // // dislikeLottieView = { // let animation = LottieAnimation.named("meet_left_swipe_dislike") // let v = LottieAnimationView(animation: animation) // // v.contentMode = .scaleAspectFit // v.loopMode = .playOnce // v.backgroundBehavior = .pauseAndRestore // v.backgroundColor = .clear // likeOrDislikeShowLogoView.addSubview(v) // let size = CGSize(width: 200, height: 200) // v.size = size // v.snp.makeConstraints { make in // make.centerX.equalToSuperview() // make.bottom.equalToSuperview().offset(-143) // make.size.equalTo(size) // } // v.isHidden = true // return v // }() } /// 重置数据(为了二次调用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 _ in cards.count..= 3 { cardView.frame = lastCardFrame } else { let frame = cardView.frame if firstCardFrame.isEmpty { firstCardFrame = frame cardCenter = cardView.center } } } /// 移动卡片 private func moveIngStatusChange(_ scale: CGFloat) { // 如果正在移动,添加第四个 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: MeetDragCardView, direction: ContainerDragDirection, scale: CGFloat, 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 || direction == .right { 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 dataSource != nil else { assertionFailure("check dataSource and dataSource Methods!") return } resetInitData() addSubViews() resetLayoutSubviews() } func getCurrentShowCardView() -> MeetDragCardView? { 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 tmpStoreNopeCardView = currentShowCardView 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 } tmpStoreNopeCardView = currentShowCardView 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: MeetDragCardView?, 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: cardCenter = CGPoint(x: self.cardCenter.x, y: self.cardCenter.y) flag = 0 break } tmpStoreNopeCardView = cardView 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 // } // } UIView.animate(withDuration: 0.5) { self.likeOrDislikeShowLogoView.alpha = 0 } return } if show { // likeLottieView.isHidden = true // dislikeLottieView.isHidden = false likeShowImageView.isHidden = true dislikeShowImageView.isHidden = false likeOrDislikeShowLogoView.isHidden = false likeOrDislikeShowLogoView.alpha = widthRate > 0.2 ? 1 : (widthRate / 0.2) } 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 // } // } UIView.animate(withDuration: 0.5) { self.likeOrDislikeShowLogoView.alpha = 0 } return } if show { // likeLottieView.isHidden = false // dislikeLottieView.isHidden = true likeShowImageView.isHidden = false dislikeShowImageView.isHidden = true likeOrDislikeShowLogoView.isHidden = false likeOrDislikeShowLogoView.alpha = widthRate > 0.2 ? 1 : (widthRate / 0.2) } else { UIView.animate(withDuration: 0.45) { self.likeOrDislikeShowLogoView.alpha = 0 } } } // func showNopeLottie() { // likeLottieView.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 // // likeLottieView.isHidden = false // stopLottieAnimation(likeLottieView) // 🔥 推测方法 // likeOrDislikeShowLogoView.alpha = 1 // // playLottieAnimation(likeLottieView) { [weak self] finish in // if finish { // self?.likeOrDislikeShowLogoView.alpha = 0 // } // } // } func showNopeLogo(){ likeShowImageView.isHidden = true dislikeShowImageView.isHidden = false likeOrDislikeShowLogoView.alpha = 1 UIView.animate(withDuration: 0.5) { [self] in likeOrDislikeShowLogoView.alpha = 0 } } func showLikeLogo(){ likeShowImageView.isHidden = false dislikeShowImageView.isHidden = true likeOrDislikeShowLogoView.alpha = 1 UIView.animate(withDuration: 0.5) { [self] in likeOrDislikeShowLogoView.alpha = 0 } } func addAdaptForDragCardView(_ cardView: MeetDragCardView) { 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) // // let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:))) // tapGesture.numberOfTapsRequired = 1 // tapGesture.numberOfTouchesRequired = 1 // cardView.addGestureRecognizer(tapGesture) 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() ?? MeetDragCardView()) } // 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? MeetDragCardView 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 = CGFloat((pan.view?.center.x ?? 0) - cardCenter.x) / CGFloat(cardCenter.x) let verticalSliderRate = CGFloat((pan.view?.center.y ?? 0) - cardCenter.y) / CGFloat(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 = CGFloat((pan.view?.center.x ?? 0) - cardCenter.x) / CGFloat(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: CGFloat(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: MeetDragCardView) { // option2: 不单独处理竖直方向手势 if point.x == 0 { currentIntialGestureDirection = .default } else if point.x < 0 { currentIntialGestureDirection = .left } else { currentIntialGestureDirection = .right } } private func isLottieAnimationPlaying(_ view: LottieAnimationView) -> Bool { return view.isAnimationPlaying } private func stopLottieAnimation(_ view: LottieAnimationView) { view.stop() } private func playLottieAnimation(_ view: LottieAnimationView, completion: @escaping (Bool) -> Void) { view.play { completed in completion(completed) } } }