gpt4 book ai didi

Swift 使用 Scrollview 重新创建 iPhone App Switcher 页面

转载 作者:行者123 更新时间:2023-12-04 15:06:07 29 4
gpt4 key购买 nike

我正在尝试重新创建 iPhone App Switcher 页面 - 向上滑动时出现的页面。

我通过将表示应用程序的 View 数组添加到 ScrollView 来构建它。

不幸的是,我遇到了设置 View 间距的问题。我正在尝试使用抛物线函数设置它,以便 View 折叠到左侧。我认为方程式可能不正确。

这是我的 scrollViewDidScroll 代码:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
items.enumerated().forEach { (index, tabView) in
guard let tabSuperView = tabView.superview else {return}
let screenWidth = UIScreen.main.bounds.width

// Return value between 0 and 1 depending on the location of the tab within the visible screen
// 0 Left hand side or offscreen
// 1 Right hand side or offscreen
let distanceMoved = tabSuperView.convert(CGPoint(x: tabView.frame.minX, y: 0), to: view).x
let screenOffsetPercentage: CGFloat = distanceMoved / screenWidth

// Scale
let minValue: CGFloat = 0.6
let maxValue: CGFloat = 1
let scaleAmount = minValue + (maxValue - minValue) * screenOffsetPercentage
let scaleSize = CGAffineTransform(scaleX: scaleAmount, y: scaleAmount)
tabView.transform = scaleSize

// Set a max and min
let percentAcrossScreen = max(min(distanceMoved / screenWidth, 1.0), 0)

// Spacing
if let prevTabView = items.itemAt(index - 1) {
// Rest of tabs
let constant: CGFloat = 100
let xFrame = prevTabView.frame.origin.x + (pow(percentAcrossScreen, 2) * constant)
tabView.frame.origin.x = max(xFrame, 0)
} else {
// First tab
tabView.frame.origin.x = 20
}
}
}

您将如何解决此问题以复制 iPhone 应用程序切换器页面的滚动体验?

enter image description here

示例项目: https://github.com/Alexander-Frost/ViewContentOffset

最佳答案

总体思路(我将移动 View 称为“卡片”)...

当您向右“推”一张卡片时,根据容器宽度的一部分计算从容器前缘到卡片前缘的距离百分比。然后,将下一张卡片的前缘定位为卡片宽度的百分比。

因此,如果卡片占 View 宽度的 70%,当“拖动”卡片距离 View 前缘的距离为 1/3 时,我们希望顶部卡片几乎被推向右侧查看。

如果拖动的卡片是 1/3rd 的二分之一,我们希望下一张卡片的行距是卡片宽度的 1/2。

正如我在您之前的一个问题中所说,我不确定使用 ScrollView 是否有好处,因为您将在拖动时改变相对距离。

这是一个例子:

enter image description here

您可以尝试此代码 - 只需创建一个新项目并将默认 View Controller 类替换为:

class ViewController: UIViewController {

let switcherView = SwitcherView()

override func viewDidLoad() {
super.viewDidLoad()

view.backgroundColor = .systemYellow

switcherView.translatesAutoresizingMaskIntoConstraints = false
switcherView.backgroundColor = .white

view.addSubview(switcherView)

// respect safe area
let g = view.safeAreaLayoutGuide

NSLayoutConstraint.activate([
// constrain switcher view to all 4 sides of safe area
switcherView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
switcherView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
switcherView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
switcherView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])

}

}

这是“卡片” View 类:

class CardView: UIView {

var theLabels: [UILabel] = []

var cardID: Int = 0 {
didSet {
theLabels.forEach {
$0.text = "\(cardID)"
}
}
}

override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {

for i in 1...5 {
let v = UILabel()
v.font = .systemFont(ofSize: 24.0)
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
switch i {
case 1:
v.topAnchor.constraint(equalTo: topAnchor, constant: 10.0).isActive = true
v.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16.0).isActive = true
case 2:
v.topAnchor.constraint(equalTo: topAnchor, constant: 10.0).isActive = true
v.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16.0).isActive = true
case 3:
v.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10.0).isActive = true
v.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16.0).isActive = true
case 4:
v.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10.0).isActive = true
v.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16.0).isActive = true
default:
v.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
v.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
theLabels.append(v)
}

layer.cornerRadius = 6

// border
layer.borderWidth = 1.0
layer.borderColor = UIColor.gray.cgColor

// shadow
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: -3, height: 3)
layer.shadowOpacity = 0.25
layer.shadowRadius = 2.0
}

}

这里是“SwitcherView”类——所有操作都在这里发生:

class SwitcherView: UIView {

var cards: [CardView] = []

var currentCard: CardView?

var firstLayout: Bool = true

// useful during development...
// if true, highlight the current "control" card in yellow
// if false, leave them all cyan
let showHighlight: Bool = false

override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {

clipsToBounds = true

// add 20 "cards" to the view
for i in 1...20 {
let v = CardView()
v.backgroundColor = .cyan
v.cardID = i
cards.append(v)
addSubview(v)
v.isHidden = true
}

// add a pan gesture recognizer to the view
let pan = UIPanGestureRecognizer(target: self, action: #selector(self.didPan(_:)))
addGestureRecognizer(pan)

}

override func layoutSubviews() {
super.layoutSubviews()

if firstLayout {
// if it's the first time through, layout the cards
firstLayout = false
if let firstCard = cards.first {
if firstCard.frame.width == 0 {
cards.forEach { thisCard in
//thisCard.alpha = 0.750
thisCard.frame = CGRect(origin: .zero, size: CGSize(width: self.bounds.width, height: self.bounds.height))
thisCard.transform = CGAffineTransform(scaleX: 0.71, y: 0.71)
thisCard.frame.origin.x = 0
if thisCard == cards.last {
thisCard.frame.origin.x = 10
}
thisCard.isHidden = false
}
doCentering(for: cards.last!)
}
}
}
}

@objc func didPan(_ gesture: UIPanGestureRecognizer) -> Void {

let translation = gesture.translation(in: self)

var pt = gesture.location(in: self)
pt.y = self.bounds.midY
for c in cards.reversed() {
if c.frame.contains(pt) {
if let cc = currentCard {
if let idx1 = cards.firstIndex(of: cc),
let idx2 = cards.firstIndex(of: c),
idx2 > idx1 {
if showHighlight {
currentCard?.backgroundColor = .cyan
}
currentCard = c
if showHighlight {
currentCard?.backgroundColor = .yellow
}
}
} else {
currentCard = c
if showHighlight {
currentCard?.backgroundColor = .yellow
}
}
break
}
}

switch gesture.state {
case .changed:
if let controlCard = currentCard {
// update card leading edge
controlCard.frame.origin.x += translation.x
// don't allow drag left past 1.0
controlCard.frame.origin.x = max(controlCard.frame.origin.x, 1.0)
// update the positions for the rest of the cards
updateCards(controlCard)
gesture.setTranslation(.zero, in: self)
}

case .ended:
if showHighlight {
currentCard?.backgroundColor = .cyan
}

guard let controlCard = currentCard else {
return
}

if let idx = cards.firstIndex(of: controlCard) {
// use pan velocity to "throw" the cards
let velocity = gesture.velocity(in: self)
// convert to a reasonable Int value
let offset: Int = Int(floor(velocity.x / 500.0))
// step up or down in array of cards based on velocity
let newIDX = max(min(idx - offset, cards.count - 1), 0)
doCentering(for: cards[newIDX])
}

currentCard = nil

default:
break
}

}

func updateCards(_ controlCard: CardView) -> Void {

guard let idx = cards.firstIndex(of: controlCard) else {
print("controlCard not found in array of cards - can't update")
return
}

var relativeCard: CardView = controlCard
var n = idx

// for each card to the right of the control card
while n < cards.count - 1 {
let nextCard = cards[n + 1]
// get percent distance of leading edge of relative card
// to 33% of the view width
let pct = relativeCard.frame.origin.x / (self.bounds.width * 1.0 / 3.0)
// move next card that percentage of the width of a card
nextCard.frame.origin.x = relativeCard.frame.origin.x + (relativeCard.frame.size.width * pct) // min(pct, 1.0))
relativeCard = nextCard
n += 1
}

// reset relative card and index
relativeCard = controlCard
n = idx

// for each card to the left of the control card
while n > 0 {
let prevCard = cards[n - 1]
// get percent distance of leading edge of relative card
// to half the view width
let pct = relativeCard.frame.origin.x / self.bounds.width
// move prev card that percentage of 33% of the view width
prevCard.frame.origin.x = (self.bounds.width * 1.0 / 3.0) * pct
relativeCard = prevCard
n -= 1
}

self.cards.forEach { c in

let x = c.frame.origin.x

// scale transform each card between 71% and 75%
// based on card's leading edge distance to one-half the view width
let pct = x / (self.bounds.width * 0.5)
let sc = 0.71 + (0.04 * min(pct, 1.0))
c.transform = CGAffineTransform(scaleX: sc, y: sc)

// set translucent for far left cards
if cards.count > 1 {
c.alpha = min(1.0, x / 10.0)
}

}

}

func doCentering(for cCard: CardView) -> Void {

guard let idx = cards.firstIndex(of: cCard) else {
return
}

var controlCard = cCard

// if the leading edge is greater than 1/2 the view width,
// and it's not the Bottom card,
// set cur card to the previous card
if idx > 0 && controlCard.frame.origin.x > self.bounds.width * 0.5 {
controlCard = cards[idx - 1]
}

// center of control card will be offset to the right of center
var newX = self.bounds.width * 0.6
if controlCard == cards.last {
// if it's the Top card, center it
newX = self.bounds.width * 0.5
}
if controlCard == cards.first {
// if it's the Bottom card, center it + just a little to the right
newX = self.bounds.width * 0.51
}
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.1, options: [.allowUserInteraction, .curveEaseOut], animations: {
controlCard.center.x = newX
self.updateCards(controlCard)
}, completion: nil)

}

}

当我们停止平移时,代码会找到前缘最靠近 View 中心的卡片...如果其前缘小于宽度的 50%,它会将其滑回左侧,以便它成为“中心”牌。如果它的前缘大于宽度的 50%,它会向右滑动,前一张卡片成为“中心”卡片。


编辑 - 向 SwitcherView 添加了一些“Pan Velocity”处理。

关于Swift 使用 Scrollview 重新创建 iPhone App Switcher 页面,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66083219/

29 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com