Visual_Novel_iOS/crush/Crush/Src/Modules/Home/CardDrag/MeetDragCardContainer.swift

753 lines
29 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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)
/// YESreloadData
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 {
// 4warning(view+1)
let targetCount = isMoveIng ? preLoadViewCount + 1 : preLoadViewCount
for _ in cards.count..<targetCount {
let cardView = dataSource.container(self, viewForRowsAt: loadedIndex)
addAdaptForDragCardView(cardView)
cards.append(cardView)
loadedIndex += 1
}
}
}
///
private func resetLayoutSubviews() {
// | 线
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.6, options: [.allowUserInteraction, .curveEaseInOut]) {
if let delegate = self.delegate, let firstCard = self.cards.first {
delegate.container(self, dargingForCardView: firstCard, direction: self.direction, widthRate: 0, heightRate: 0)
}
for (i, cardView) in self.cards.enumerated() {
cardView.transform = .identity
let frame = self.firstCardFrame
switch i {
case 0:
cardView.frame = frame
case 1:
cardView.transform = CGAffineTransform(scaleX: secondCardScale, y: 1)
case 2:
cardView.transform = CGAffineTransform(scaleX: thirdCardScale, y: 1)
if self.lastCardFrame.isEmpty {
self.lastCardFrame = frame
self.lastCardTransform = cardView.transform
}
default:
break
}
cardView.originTransForm = cardView.transform
}
} completion: { finished in
let isEmpty = self.cards.isEmpty
self.delegate?.container(self, dataSourceIsEmpty: isEmpty)
}
}
private func recordFrame(_ cardView: MeetDragCardView) {
if loadedIndex >= 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 {
// cardviewscale
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()
}
}
/// appFinish
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:
// cardcard
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)
}
}
}