SwiftUISwiftUI教程与源码

SwiftUI 高级之水平滚动并获取滚动位置

2020-02-03  本文已影响0人  iCloudEnd

效果

Jietu20200204-213912@2x.jpg

代码

Page代码

//
//  PagingScrollView.swift
//  SwiftUIPagingScrollView
//
//  Created by myf on 27/08/2019.
//  Copyright © 2019 Pavel Zak. All rights reserved.
//

import SwiftUI

struct PagingScrollView: View {
    let items: [AnyView]

    init<A: View>(activePageIndex:Binding<Int>, itemCount: Int, pageWidth:CGFloat, tileWidth:CGFloat, tilePadding: CGFloat, @ViewBuilder content: () -> A) {
        let views = content()
        self.items = [AnyView(views)]
        
        self._activePageIndex = activePageIndex
        
        self.pageWidth = pageWidth
        self.tileWidth = tileWidth
        self.tilePadding = tilePadding
        self.tileRemain = (pageWidth-tileWidth-2*tilePadding)/2
        self.itemCount = itemCount
        self.contentWidth = (tileWidth+tilePadding)*CGFloat(self.itemCount)
        
        self.leadingOffset = tileRemain+tilePadding
        self.stackOffset = contentWidth/2 - pageWidth/2 - tilePadding/2
    }
    
    /// index of current page 0..N-1
    @Binding var activePageIndex : Int
    
    /// pageWidth==frameWidth used to properly compute offsets
    let pageWidth: CGFloat
    
    /// width of item / tile
    let tileWidth : CGFloat
    
    /// padding between items
    private let tilePadding : CGFloat
    
    /// how much of surrounding iems is still visible
    private let tileRemain : CGFloat
    
    /// total width of conatiner
    private let contentWidth : CGFloat
    
    /// offset to scroll on the first item
    private let leadingOffset : CGFloat
    
    /// since the hstack is centered by default this offset actualy moves it entirely to the left
    private let stackOffset : CGFloat // to fix center alignment
    
    /// number of items; I did not come with the soluion of extracting the right count in initializer
    private let itemCount : Int
    
    /// some damping factor to reduce liveness
    private let scrollDampingFactor: CGFloat = 0.66
    
    /// current offset of all items
    @State var currentScrollOffset: CGFloat = 0
    
    /// drag offset during drag gesture
    @State private var dragOffset : CGFloat = 0
    
    
    func offsetForPageIndex(_ index: Int)->CGFloat {
        let activePageOffset = CGFloat(index)*(tileWidth+tilePadding)
        
        return self.leadingOffset - activePageOffset
    }
    
    func indexPageForOffset(_ offset : CGFloat) -> Int {
        guard self.itemCount>0 else {
            return 0
        }
        let offset = self.logicalScrollOffset(trueOffset: offset)
        let floatIndex = (offset)/(tileWidth+tilePadding)
        var computedIndex = Int(round(floatIndex))
        computedIndex = max(computedIndex, 0)
        return min(computedIndex, self.itemCount-1)
    }
    
    /// current scroll offset applied on items
    func computeCurrentScrollOffset()->CGFloat {
        return self.offsetForPageIndex(self.activePageIndex) + self.dragOffset
    }
    
    /// logical offset startin at 0 for the first item - this makes computing the page index easier
    func logicalScrollOffset(trueOffset: CGFloat)->CGFloat {
        return (trueOffset-leadingOffset) * -1.0
    }
    
   
    var body: some View {
        GeometryReader { outerGeometry in
            HStack(alignment: .center, spacing: self.tilePadding)  {
                /// building items into HStack
                ForEach(0..<self.items.count) { index in
                    
                        self.items[index]
                            .offset(x: self.currentScrollOffset, y: 0)
                            .frame(width: self.tileWidth)
                    
                }
            }
            .onAppear {
                self.currentScrollOffset = self.offsetForPageIndex(self.activePageIndex)
            }
            .offset(x: self.stackOffset, y: 0)
            .background(Color.black.opacity(0.00001)) // hack - this allows gesture recognizing even when background is transparent
            .frame(width: self.contentWidth)
            .simultaneousGesture( DragGesture(minimumDistance: 1, coordinateSpace: .local) // can be changed to simultaneous gesture to work with buttons
                .onChanged { value in
                    self.dragOffset = value.translation.width
                    self.currentScrollOffset = self.computeCurrentScrollOffset()
                }
                .onEnded { value in
                    // compute nearest index
                    let velocityDiff = (value.predictedEndTranslation.width - self.dragOffset)*self.scrollDampingFactor
                    let newPageIndex = self.indexPageForOffset(self.currentScrollOffset+velocityDiff)
                    self.dragOffset = 0
                    withAnimation(.interpolatingSpring(mass: 0.1, stiffness: 20, damping: 1.5, initialVelocity: 0)){
                        self.activePageIndex = newPageIndex
                        self.currentScrollOffset = self.computeCurrentScrollOffset()
                    }
                }
            )
        }
    }
}

界面

import SwiftUI

struct TileView: View {
    
    let icon: String
    var color: Color
    
    var body: some View {
        VStack {
            ZStack {
                Rectangle()
                    .fill(color)
                    .cornerRadius(20.0)
                Image(systemName: icon)
                    .imageScale(.large)
                    .font(.largeTitle)
            }
        }
    }
}

struct ContentView: View {
    @State private var scrollEffectValue: Double = 13
    @State private var activePageIndex: Int = 0
    
    let tileWidth: CGFloat = 220
    let tilePadding: CGFloat = 20
    let numberOfTiles: Int = 10
    var items = [Color.red, Color.orange, Color.yellow, Color.green,         Color.blue, Color.purple,Color.red, Color.orange, Color.yellow, Color.green,         Color.blue, Color.purple]
    
    var body: some View {
        VStack {
            Spacer()
            GeometryReader { geometry in
                PagingScrollView(activePageIndex: self.$activePageIndex, itemCount:self.numberOfTiles ,pageWidth:geometry.size.width, tileWidth:self.tileWidth, tilePadding: self.tilePadding){
                    ForEach(0 ..< self.numberOfTiles) { index in
                        GeometryReader { geometry2 in
                            TileView(icon: "\(index + 1).circle",color:self.items[index])
                               
                                .rotation3DEffect(Angle(degrees: Double((geometry2.frame(in: .global).minX - self.tileWidth*0.5) / -10 )), axis: (x: 2, y: 11, z: 1))
                                .onTapGesture {
                                    print ("tap on index: \(index) current:\(self.$activePageIndex)")
                            }
                        }
                    }
                }
            }.frame(height:300)
            List{
                Text("current:\(self.activePageIndex)")
                Text("current:\(self.items[self.activePageIndex].description)")
            }
            Spacer()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

源码参考

https://github.com/izakpavel/SwiftUIPagingScrollView

更多SwiftUI教程和代码关注专栏

QQ:3365059189
SwiftUI技术交流QQ群:518696470

上一篇下一篇

猜你喜欢

热点阅读