├── _Pods.xcodeproj ├── Gemfile ├── Example ├── Gemini │ ├── Images.xcassets │ │ ├── Contents.json │ │ ├── people.imageset │ │ │ ├── 15H.jpg │ │ │ └── Contents.json │ │ ├── food.imageset │ │ │ ├── 6835629512_6b8bffe0af_b.jpg │ │ │ └── Contents.json │ │ ├── minions.imageset │ │ │ ├── minions-2095845_1280.jpg │ │ │ └── Contents.json │ │ ├── nature.imageset │ │ │ ├── forrest-cavale-13484.jpg │ │ │ └── Contents.json │ │ ├── building.imageset │ │ │ ├── 22095344795_72707dea6d_k.jpg │ │ │ └── Contents.json │ │ ├── japan.imageset │ │ │ ├── Kinkakuji_in_snow_(5314109935).jpg │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Views │ │ ├── PlayerCollectionViewCell.swift │ │ ├── ImageCollectionViewCell.swift │ │ ├── PlayerView.swift │ │ ├── UICollectionViewPagingFlowLayout.swift │ │ ├── ImageCollectionViewCell.xib │ │ └── PlayerCollectionViewCell.xib │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.xib │ ├── Model │ │ └── ResourceModel.swift │ ├── Info.plist │ ├── AppDelegate.swift │ └── ViewControllers │ │ ├── AnimationListViewController.storyboard │ │ ├── ScaleAnimationViewController.swift │ │ ├── YawRotationViewController.swift │ │ ├── CubeViewController.swift │ │ ├── PitchRotationViewController.swift │ │ ├── RollRotationViewController.swift │ │ ├── CubeViewController.storyboard │ │ ├── YawRotationViewController.storyboard │ │ ├── CircleRotationViewController.storyboard │ │ ├── ScaleAnimationViewController.storyboard │ │ ├── RollRotationViewController.storyboard │ │ ├── PitchRotationViewController.storyboard │ │ ├── CustomAnimationViewController.storyboard │ │ ├── CustomAnimationViewController.swift │ │ ├── CircleRotationViewController.swift │ │ └── AnimationListViewController.swift ├── Podfile ├── Pods │ ├── Target Support Files │ │ ├── Gemini │ │ │ ├── Gemini.modulemap │ │ │ ├── Gemini-dummy.m │ │ │ ├── Gemini-prefix.pch │ │ │ ├── Gemini-umbrella.h │ │ │ ├── Gemini.xcconfig │ │ │ └── Info.plist │ │ └── Pods-Gemini_Example │ │ │ ├── Pods-Gemini_Example.modulemap │ │ │ ├── Pods-Gemini_Example-dummy.m │ │ │ ├── Pods-Gemini_Example-umbrella.h │ │ │ ├── Pods-Gemini_Example.debug.xcconfig │ │ │ ├── Pods-Gemini_Example.release.xcconfig │ │ │ ├── Info.plist │ │ │ ├── Pods-Gemini_Example-acknowledgements.markdown │ │ │ ├── Pods-Gemini_Example-acknowledgements.plist │ │ │ ├── Pods-Gemini_Example-resources.sh │ │ │ └── Pods-Gemini_Example-frameworks.sh │ ├── Manifest.lock │ ├── Local Podspecs │ │ └── Gemini.podspec.json │ └── Pods.xcodeproj │ │ └── xcshareddata │ │ └── xcschemes │ │ └── Gemini.xcscheme ├── Gemini.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Gemini.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Podfile.lock └── Tests │ ├── Info.plist │ └── Tests.swift ├── Makefile ├── Gemini ├── GeminiCell.swift ├── EasingAnimatable.swift ├── CubeAnimatable.swift ├── UIViewExtension.swift ├── ScaleAnimatable.swift ├── YawRotationAnimatable.swift ├── RollRotationAnimatable.swift ├── PitchRotationAnimatable.swift ├── CircleRotateAnimatable.swift ├── UIAppearanceAnimatable.swift ├── CustomAnimatable.swift ├── GeminiCollectionView.swift ├── EasingFunction.swift └── GeminiAnimationModel.swift ├── .gitignore ├── .travis.yml ├── Gemini.podspec ├── LICENSE ├── Gemfile.lock ├── CHANGELOG.md └── README.md /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'cocoapods', '1.7.2' 4 | -------------------------------------------------------------------------------- /Example/Gemini/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '10.0' 2 | 3 | use_frameworks! 4 | 5 | target 'Gemini_Example' do 6 | pod 'Gemini', :path => '../' 7 | end 8 | -------------------------------------------------------------------------------- /Example/Gemini/Images.xcassets/people.imageset/15H.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoheiyokoyama/Gemini/HEAD/Example/Gemini/Images.xcassets/people.imageset/15H.jpg -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Gemini/Gemini.modulemap: -------------------------------------------------------------------------------- 1 | framework module Gemini { 2 | umbrella header "Gemini-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Gemini/Gemini-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Gemini : NSObject 3 | @end 4 | @implementation PodsDummy_Gemini 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Gemini/Images.xcassets/food.imageset/6835629512_6b8bffe0af_b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoheiyokoyama/Gemini/HEAD/Example/Gemini/Images.xcassets/food.imageset/6835629512_6b8bffe0af_b.jpg -------------------------------------------------------------------------------- /Example/Gemini/Images.xcassets/minions.imageset/minions-2095845_1280.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoheiyokoyama/Gemini/HEAD/Example/Gemini/Images.xcassets/minions.imageset/minions-2095845_1280.jpg -------------------------------------------------------------------------------- /Example/Gemini/Images.xcassets/nature.imageset/forrest-cavale-13484.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoheiyokoyama/Gemini/HEAD/Example/Gemini/Images.xcassets/nature.imageset/forrest-cavale-13484.jpg -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install-gems: 2 | bundle install --path vendor/bundle 3 | 4 | lint-lib: 5 | bundle exec pod lib lint --allow-warnings 6 | 7 | release-pod: 8 | bundle exec pod trunk push --allow-warnings -------------------------------------------------------------------------------- /Example/Gemini/Images.xcassets/building.imageset/22095344795_72707dea6d_k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoheiyokoyama/Gemini/HEAD/Example/Gemini/Images.xcassets/building.imageset/22095344795_72707dea6d_k.jpg -------------------------------------------------------------------------------- /Example/Gemini/Images.xcassets/japan.imageset/Kinkakuji_in_snow_(5314109935).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoheiyokoyama/Gemini/HEAD/Example/Gemini/Images.xcassets/japan.imageset/Kinkakuji_in_snow_(5314109935).jpg -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Gemini_Example/Pods-Gemini_Example.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_Gemini_Example { 2 | umbrella header "Pods-Gemini_Example-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Gemini_Example/Pods-Gemini_Example-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_Gemini_Example : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_Gemini_Example 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Gemini.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Gemini/Gemini-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Example/Gemini.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/Gemini.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Gemini.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Gemini (1.3.0) 3 | 4 | DEPENDENCIES: 5 | - Gemini (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | Gemini: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | Gemini: 0a844a4a1a6543669ff13cb79c411c0dcc410a23 13 | 14 | PODFILE CHECKSUM: f18b0319dbc9599bc714dbfcce03717dab800856 15 | 16 | COCOAPODS: 1.5.3 17 | -------------------------------------------------------------------------------- /Example/Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Gemini (1.3.0) 3 | 4 | DEPENDENCIES: 5 | - Gemini (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | Gemini: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | Gemini: 0a844a4a1a6543669ff13cb79c411c0dcc410a23 13 | 14 | PODFILE CHECKSUM: f18b0319dbc9599bc714dbfcce03717dab800856 15 | 16 | COCOAPODS: 1.5.3 17 | -------------------------------------------------------------------------------- /Gemini/GeminiCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | open class GeminiCell: UICollectionViewCell { 4 | override open func prepareForReuse() { 5 | super.prepareForReuse() 6 | 7 | adjustAnchorPoint() 8 | layer.transform = CATransform3DIdentity 9 | } 10 | 11 | open var shadowView: UIView? { 12 | return nil 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | vendor/bundle 25 | 26 | Carthage 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Gemini/Gemini-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double GeminiVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char GeminiVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Gemini/Images.xcassets/people.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "15H.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Gemini/Images.xcassets/nature.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "forrest-cavale-13484.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Gemini/Images.xcassets/food.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "6835629512_6b8bffe0af_b.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Gemini/Images.xcassets/minions.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "minions-2095845_1280.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Gemini/EasingAnimatable.swift: -------------------------------------------------------------------------------- 1 | public protocol EasingAnimatable { 2 | /// The easing function based on distance of scroll. the default value is `GeminiEasing.linear`. 3 | @discardableResult func ease(_ easing: GeminiEasing) -> Self 4 | } 5 | 6 | extension GeminiAnimationModel: EasingAnimatable { 7 | @discardableResult 8 | public func ease(_ easing: GeminiEasing) -> Self { 9 | self.easing = easing 10 | return self 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Gemini/Images.xcassets/building.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "22095344795_72707dea6d_k.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Gemini/Images.xcassets/japan.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Kinkakuji_in_snow_(5314109935).jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Gemini_Example/Pods-Gemini_Example-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_Gemini_ExampleVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_Gemini_ExampleVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Gemini/Views/PlayerCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Gemini 3 | 4 | final class PlayerCollectionViewCell: GeminiCell { 5 | @IBOutlet private(set) weak var playerView: PlayerView! 6 | @IBOutlet private weak var blackShadowView: UIView! 7 | 8 | override var shadowView: UIView? { 9 | return blackShadowView 10 | } 11 | 12 | func configure(with url: URL) { 13 | playerView.setVideoURL(url) 14 | playerView.play() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Gemini/Gemini.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Gemini 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 4 | PODS_BUILD_DIR = ${BUILD_DIR} 5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 6 | PODS_ROOT = ${SRCROOT} 7 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../.. 8 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 9 | SKIP_INSTALL = YES 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * http://www.objc.io/issue-6/travis-ci.html 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | osx_image: xcode7.3 6 | language: objective-c 7 | # cache: cocoapods 8 | # podfile: Example/Podfile 9 | # before_install: 10 | # - gem install cocoapods # Since Travis is not always on latest version 11 | # - pod install --project-directory=Example 12 | script: 13 | - set -o pipefail && xcodebuild test -workspace Example/Gemini.xcworkspace -scheme Gemini-Example -sdk iphonesimulator9.3 ONLY_ACTIVE_ARCH=NO | xcpretty 14 | - pod lib lint 15 | -------------------------------------------------------------------------------- /Gemini/CubeAnimatable.swift: -------------------------------------------------------------------------------- 1 | public protocol CubeAnimatable: EasingAnimatable, UIAppearanceAnimatable { 2 | /// Cube degree for the x-vector in the range 0.0 to 90.0. 3 | /// If cubeDegree is 90, it moves like a regular hexahedron. 4 | /// The default value is 90.0. 5 | @discardableResult func cubeDegree(_ degree: CGFloat) -> CubeAnimatable 6 | } 7 | 8 | extension GeminiAnimationModel: CubeAnimatable { 9 | @discardableResult 10 | public func cubeDegree(_ degree: CGFloat) -> CubeAnimatable { 11 | cubeDegree = degree 12 | return self 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Example/Gemini/Views/ImageCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Gemini 3 | 4 | final class ImageCollectionViewCell: GeminiCell { 5 | @IBOutlet private weak var blackShadowView: UIView! 6 | @IBOutlet private weak var sampleImageView: UIImageView! 7 | 8 | override var shadowView: UIView? { 9 | return blackShadowView 10 | } 11 | 12 | override func awakeFromNib() { 13 | super.awakeFromNib() 14 | layer.cornerRadius = 5 15 | } 16 | 17 | func configure(with image: UIImage) { 18 | sampleImageView.image = image 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Example/Gemini/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Gemini_Example/Pods-Gemini_Example.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Gemini" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "${PODS_CONFIGURATION_BUILD_DIR}/Gemini/Gemini.framework/Headers" 6 | OTHER_LDFLAGS = $(inherited) -framework "Gemini" 7 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Gemini_Example/Pods-Gemini_Example.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Gemini" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "${PODS_CONFIGURATION_BUILD_DIR}/Gemini/Gemini.framework/Headers" 6 | OTHER_LDFLAGS = $(inherited) -framework "Gemini" 7 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | -------------------------------------------------------------------------------- /Gemini/UIViewExtension.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | func adjustAnchorPoint(_ anchorPoint: CGPoint = CGPoint(x: 0.5, y: 0.5)) { 5 | var newPoint = CGPoint(x: bounds.size.width * anchorPoint.x, y: bounds.size.height * anchorPoint.y) 6 | var oldPoint = CGPoint(x: bounds.size.width * layer.anchorPoint.x, y: bounds.size.height * layer.anchorPoint.y) 7 | 8 | newPoint = newPoint.applying(transform) 9 | oldPoint = oldPoint.applying(transform) 10 | 11 | var position = layer.position 12 | position.x -= oldPoint.x 13 | position.x += newPoint.x 14 | 15 | position.y -= oldPoint.y 16 | position.y += newPoint.y 17 | 18 | layer.position = position 19 | layer.anchorPoint = anchorPoint 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Example/Tests/Tests.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import XCTest 3 | 4 | class Tests: XCTestCase { 5 | 6 | override func setUp() { 7 | super.setUp() 8 | // Put setup code here. This method is called before the invocation of each test method in the class. 9 | } 10 | 11 | override func tearDown() { 12 | // Put teardown code here. This method is called after the invocation of each test method in the class. 13 | super.tearDown() 14 | } 15 | 16 | func testExample() { 17 | // This is an example of a functional test case. 18 | XCTAssert(true, "Pass") 19 | } 20 | 21 | func testPerformanceExample() { 22 | // This is an example of a performance test case. 23 | self.measure() { 24 | // Put the code you want to measure the time of here. 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Example/Gemini/Model/ResourceModel.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | enum Resource { 4 | case image 5 | case movie 6 | } 7 | 8 | extension Resource { 9 | var images: [UIImage] { 10 | return resourceNames.compactMap { UIImage(named: $0) } 11 | } 12 | 13 | var urls: [URL] { 14 | return resourceNames.compactMap(URL.init) 15 | } 16 | 17 | private var resourceNames: [String] { 18 | switch self { 19 | case .image: 20 | return ["building", "food", "japan", "minions", "nature", "people"] 21 | case .movie: 22 | return ["https://yt-dash-mse-test.commondatastorage.googleapis.com/media/car-20120827-85.mp4", 23 | "https://yt-dash-mse-test.commondatastorage.googleapis.com/media/motion-20120802-85.mp4", 24 | "https://yt-dash-mse-test.commondatastorage.googleapis.com/media/oops-20120802-85.mp4"] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Gemini/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.3.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Gemini_Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Local Podspecs/Gemini.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Gemini", 3 | "version": "1.3.0", 4 | "summary": "Gemini is rich scroll animation framework for iOS, written in Swift.", 5 | "description": "Gemini is rich scroll animation framework for iOS, written in Swift. \n\nYou can easily use GeminiCollectionView, which is a subclass of UICollectionView.\n\nYou are available multiple animation which has various and customizable properties, and moreover can create your own custom scroll animation.\n\nGemini also provide a fluent interface based on method chaining. you can use this intuitively and simply.", 6 | "homepage": "https://github.com/shoheiyokoyama/Gemini", 7 | "license": { 8 | "type": "MIT", 9 | "file": "LICENSE" 10 | }, 11 | "authors": { 12 | "shoheiyokoyama": "shohei.yok0602@gmail.com" 13 | }, 14 | "source": { 15 | "git": "https://github.com/shoheiyokoyama/Gemini.git", 16 | "tag": "1.3.0" 17 | }, 18 | "platforms": { 19 | "ios": "8.0" 20 | }, 21 | "source_files": "Gemini/**/*" 22 | } 23 | -------------------------------------------------------------------------------- /Gemini/ScaleAnimatable.swift: -------------------------------------------------------------------------------- 1 | public enum GeminScaleEffect { 2 | /// `scaleUp` gradually increases frame size of item. 3 | case scaleUp 4 | 5 | /// `scaleDown` gradually decreases frame size of item. 6 | case scaleDown 7 | } 8 | 9 | public protocol ScaleAnimatable: EasingAnimatable { 10 | /// The Scale based on 2-Dimensional vector. 11 | /// The default value is 1. 12 | /// The range 0.0 to 1.0. 13 | @discardableResult func scale(_ scale: CGFloat) -> Self 14 | 15 | /// The option of `GeminiAnimation.scale`. the default value is `GeminScaleEffect.scaleUp`. 16 | @discardableResult func scaleEffect(_ effect: GeminScaleEffect) -> Self 17 | } 18 | 19 | extension GeminiAnimationModel: ScaleAnimatable { 20 | @discardableResult 21 | public func scale(_ scale: CGFloat) -> Self { 22 | self.scale = scale 23 | return self 24 | } 25 | 26 | @discardableResult 27 | public func scaleEffect(_ effect: GeminScaleEffect) -> Self { 28 | scaleEffect = effect 29 | return self 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Example/Gemini/Views/PlayerView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import AVFoundation 3 | 4 | final class PlayerView: UIView { 5 | private var playerLayer: AVPlayerLayer { 6 | return layer as! AVPlayerLayer 7 | } 8 | 9 | private var player: AVPlayer? { 10 | return playerLayer.player 11 | } 12 | 13 | override class var layerClass: AnyClass { 14 | return AVPlayerLayer.self 15 | } 16 | 17 | override init(frame: CGRect) { 18 | super.init(frame: frame) 19 | playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill 20 | } 21 | 22 | required init?(coder aDecoder: NSCoder) { 23 | super.init(coder: aDecoder) 24 | playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill 25 | } 26 | 27 | func setVideoURL(_ url: URL) { 28 | let playerItem = AVPlayerItem(asset: AVURLAsset(url: url)) 29 | playerLayer.player = AVPlayer(playerItem: playerItem) 30 | } 31 | 32 | func play() { 33 | player?.play() 34 | } 35 | 36 | func pause() { 37 | player?.pause() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Example/Gemini/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /Gemini.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'Gemini' 3 | s.version = '1.4.0' 4 | s.summary = 'Gemini is rich scroll animation framework for iOS, written in Swift.' 5 | s.description = <<-DESC 6 | Gemini is rich scroll animation framework for iOS, written in Swift. 7 | 8 | You can easily use GeminiCollectionView, which is a subclass of UICollectionView. 9 | 10 | You are available multiple animation which has various and customizable properties, and moreover can create your own custom scroll animation. 11 | 12 | Gemini also provide a fluent interface based on method chaining. you can use this intuitively and simply. 13 | DESC 14 | 15 | s.homepage = 'https://github.com/shoheiyokoyama/Gemini' 16 | s.license = { :type => 'MIT', :file => 'LICENSE' } 17 | s.author = { 'shoheiyokoyama' => 'shohei.yok0602@gmail.com' } 18 | s.source = { :git => 'https://github.com/shoheiyokoyama/Gemini.git', :tag => s.version.to_s } 19 | s.ios.deployment_target = '8.0' 20 | s.source_files = 'Gemini/**/*' 21 | s.swift_version = '5.0' 22 | end 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 shoheiyokoyama 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Gemini/YawRotationAnimatable.swift: -------------------------------------------------------------------------------- 1 | public enum YawRotationEffect { 2 | case yawUp 3 | case yawDown 4 | case sineWave 5 | case reverseSineWave 6 | } 7 | 8 | public protocol YawRotationAnimatable: ScaleAnimatable, UIAppearanceAnimatable { 9 | /// The degree of rotation in the yaw direction. the default value is 90.0. 10 | /// - SeeAlso: [Pitch, roll, and yaw axes](https://github.com/shoheiyokoyama/Assets/blob/master/Gemini/attitude_rotation.png) 11 | @discardableResult func degree(_ degree: CGFloat) -> YawRotationAnimatable 12 | 13 | /// The option of `GeminiAnimation.yawRotation`. the default value is `YawRotationEffect.yawUp`. 14 | /// `GeminiEasing` is applied to `YawRotationEffect`. 15 | @discardableResult func yawEffect(_ effect: YawRotationEffect) -> YawRotationAnimatable 16 | } 17 | 18 | extension GeminiAnimationModel: YawRotationAnimatable { 19 | @discardableResult 20 | public func degree(_ degree: CGFloat) -> YawRotationAnimatable { 21 | yawDegree = degree 22 | return self 23 | } 24 | 25 | @discardableResult 26 | public func yawEffect(_ effect: YawRotationEffect) -> YawRotationAnimatable { 27 | yawEffect = effect 28 | return self 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Gemini/RollRotationAnimatable.swift: -------------------------------------------------------------------------------- 1 | public enum RollRotationEffect { 2 | case rollUp 3 | case rollDown 4 | case sineWave 5 | case reverseSineWave 6 | } 7 | 8 | public protocol RollRotationAnimatable: ScaleAnimatable, UIAppearanceAnimatable { 9 | /// The degree of rotation in the roll direction. the default value is 90. 10 | /// - SeeAlso: [Pitch, roll, and yaw axes](https://github.com/shoheiyokoyama/Assets/blob/master/Gemini/attitude_rotation.png) 11 | @discardableResult func degree(_ degree: CGFloat) -> RollRotationAnimatable 12 | 13 | /// The option of `GeminiAnimation.rollRotation`. the default value is `RollRotationEffect.rollUp`. 14 | /// `GeminiEasing` is applied to `RollRotationEffect`. 15 | @discardableResult func rollEffect(_ effect: RollRotationEffect) -> RollRotationAnimatable 16 | } 17 | 18 | extension GeminiAnimationModel: RollRotationAnimatable { 19 | @discardableResult 20 | public func degree(_ degree: CGFloat) -> RollRotationAnimatable { 21 | rollDegree = degree 22 | return self 23 | } 24 | 25 | @discardableResult 26 | public func rollEffect(_ effect: RollRotationEffect) -> RollRotationAnimatable { 27 | rollEffect = effect 28 | return self 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Gemini/PitchRotationAnimatable.swift: -------------------------------------------------------------------------------- 1 | public enum PitchRotationEffect { 2 | case pitchUp 3 | case pitchDown 4 | case sineWave 5 | case reverseSineWave 6 | } 7 | 8 | public protocol PitchRotationAnimatable: ScaleAnimatable, UIAppearanceAnimatable { 9 | /// The degree of rotation in the pitch direction. the default value is 90. 10 | /// - SeeAlso: [Pitch, roll, and yaw axes](https://github.com/shoheiyokoyama/Assets/blob/master/Gemini/attitude_rotation.png) 11 | @discardableResult func degree(_ degree: CGFloat) -> PitchRotationAnimatable 12 | 13 | /// The option of `GeminiAnimation.pitchRotation`. the default value is `PitchRotationEffect.pitchUp`. 14 | /// `GeminiEasing` is applied to `PitchRotationEffect`. 15 | @discardableResult func pitchEffect(_ effect: PitchRotationEffect) -> PitchRotationAnimatable 16 | } 17 | 18 | extension GeminiAnimationModel: PitchRotationAnimatable { 19 | @discardableResult 20 | public func degree(_ degree: CGFloat) -> PitchRotationAnimatable { 21 | pitchDegree = degree 22 | return self 23 | } 24 | 25 | @discardableResult 26 | public func pitchEffect(_ effect: PitchRotationEffect) -> PitchRotationAnimatable { 27 | pitchEffect = effect 28 | return self 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Gemini_Example/Pods-Gemini_Example-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## Gemini 5 | 6 | Copyright (c) 2017 shoheiyokoyama 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | 26 | Generated by CocoaPods - https://cocoapods.org 27 | -------------------------------------------------------------------------------- /Example/Gemini/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UIRequiresFullScreen 34 | 35 | UIStatusBarHidden 36 | 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | 41 | UIViewControllerBasedStatusBarAppearance 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Gemini/CircleRotateAnimatable.swift: -------------------------------------------------------------------------------- 1 | public enum CircleRotationDirection { 2 | /// Item rotate in clockwise direction. 3 | case clockwise 4 | 5 | /// Item rotate in anticlockwise direction. 6 | case anticlockwise 7 | } 8 | 9 | public protocol CircleRotationAnimatable: ScaleAnimatable, UIAppearanceAnimatable { 10 | /// The radius of the circle. The default value is 100. 11 | @discardableResult func radius(_ radius: CGFloat) -> CircleRotationAnimatable 12 | 13 | /// The direction of rotation. The default value is `CircleRotationDirection.clockwise`. 14 | @discardableResult func rotateDirection(_ direction: CircleRotationDirection) -> CircleRotationAnimatable 15 | 16 | /// A Boolean value indicating whether the item rotates or not. 17 | @discardableResult func itemRotationEnabled(_ isEnabled: Bool) -> CircleRotationAnimatable 18 | } 19 | 20 | extension GeminiAnimationModel: CircleRotationAnimatable { 21 | @discardableResult 22 | public func radius(_ radius: CGFloat) -> CircleRotationAnimatable { 23 | circleRadius = radius 24 | return self 25 | } 26 | 27 | @discardableResult 28 | public func rotateDirection(_ direction: CircleRotationDirection) -> CircleRotationAnimatable { 29 | rotateDirection = direction 30 | return self 31 | } 32 | 33 | @discardableResult 34 | public func itemRotationEnabled(_ isEnabled: Bool) -> CircleRotationAnimatable { 35 | isItemRotationEnabled = isEnabled 36 | return self 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Example/Gemini/Views/UICollectionViewPagingFlowLayout.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class UICollectionViewPagingFlowLayout: UICollectionViewFlowLayout { 4 | override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { 5 | guard let collectionView = collectionView else { return proposedContentOffset } 6 | 7 | let offset = isVertical ? collectionView.contentOffset.y : collectionView.contentOffset.x 8 | let velocity = isVertical ? velocity.y : velocity.x 9 | 10 | let flickVelocityThreshold: CGFloat = 0.2 11 | let currentPage = offset / pageSize 12 | 13 | if abs(velocity) > flickVelocityThreshold { 14 | let nextPage = velocity > 0.0 ? ceil(currentPage) : floor(currentPage) 15 | let nextPosition = nextPage * pageSize 16 | return isVertical ? CGPoint(x: proposedContentOffset.x, y: nextPosition) : CGPoint(x: nextPosition, y: proposedContentOffset.y) 17 | } else { 18 | let nextPosition = round(currentPage) * pageSize 19 | return isVertical ? CGPoint(x: proposedContentOffset.x, y: nextPosition) : CGPoint(x: nextPosition, y: proposedContentOffset.y) 20 | } 21 | } 22 | 23 | private var isVertical: Bool { 24 | return scrollDirection == .vertical 25 | } 26 | 27 | private var pageSize: CGFloat { 28 | if isVertical { 29 | return itemSize.height + minimumInteritemSpacing 30 | } else { 31 | return itemSize.width + minimumLineSpacing 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.0) 5 | activesupport (4.2.11.1) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | atomos (0.1.3) 11 | claide (1.0.2) 12 | cocoapods (1.7.2) 13 | activesupport (>= 4.0.2, < 5) 14 | claide (>= 1.0.2, < 2.0) 15 | cocoapods-core (= 1.7.2) 16 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 17 | cocoapods-downloader (>= 1.2.2, < 2.0) 18 | cocoapods-plugins (>= 1.0.0, < 2.0) 19 | cocoapods-search (>= 1.0.0, < 2.0) 20 | cocoapods-stats (>= 1.0.0, < 2.0) 21 | cocoapods-trunk (>= 1.3.1, < 2.0) 22 | cocoapods-try (>= 1.1.0, < 2.0) 23 | colored2 (~> 3.1) 24 | escape (~> 0.0.4) 25 | fourflusher (>= 2.3.0, < 3.0) 26 | gh_inspector (~> 1.0) 27 | molinillo (~> 0.6.6) 28 | nap (~> 1.0) 29 | ruby-macho (~> 1.4) 30 | xcodeproj (>= 1.10.0, < 2.0) 31 | cocoapods-core (1.7.2) 32 | activesupport (>= 4.0.2, < 6) 33 | fuzzy_match (~> 2.0.4) 34 | nap (~> 1.0) 35 | cocoapods-deintegrate (1.0.4) 36 | cocoapods-downloader (1.2.2) 37 | cocoapods-plugins (1.0.0) 38 | nap 39 | cocoapods-search (1.0.0) 40 | cocoapods-stats (1.1.0) 41 | cocoapods-trunk (1.3.1) 42 | nap (>= 0.8, < 2.0) 43 | netrc (~> 0.11) 44 | cocoapods-try (1.1.0) 45 | colored2 (3.1.2) 46 | concurrent-ruby (1.1.5) 47 | escape (0.0.4) 48 | fourflusher (2.3.1) 49 | fuzzy_match (2.0.4) 50 | gh_inspector (1.1.3) 51 | i18n (0.9.5) 52 | concurrent-ruby (~> 1.0) 53 | minitest (5.11.3) 54 | molinillo (0.6.6) 55 | nanaimo (0.2.6) 56 | nap (1.1.0) 57 | netrc (0.11.0) 58 | ruby-macho (1.4.0) 59 | thread_safe (0.3.6) 60 | tzinfo (1.2.5) 61 | thread_safe (~> 0.1) 62 | xcodeproj (1.10.0) 63 | CFPropertyList (>= 2.3.3, < 4.0) 64 | atomos (~> 0.1.3) 65 | claide (>= 1.0.2, < 2.0) 66 | colored2 (~> 3.1) 67 | nanaimo (~> 0.2.6) 68 | 69 | PLATFORMS 70 | ruby 71 | 72 | DEPENDENCIES 73 | cocoapods (= 1.7.2) 74 | 75 | BUNDLED WITH 76 | 1.17.3 77 | -------------------------------------------------------------------------------- /Gemini/UIAppearanceAnimatable.swift: -------------------------------------------------------------------------------- 1 | public enum ShadowEffect { 2 | case fadeIn 3 | case nextFadeIn 4 | case previousFadeIn 5 | case fadeOut 6 | case none 7 | } 8 | 9 | public protocol UIAppearanceAnimatable { 10 | /// The option of `shadowView` in `GeminiCell`. the default value is `ShadowEffect.none`. 11 | @discardableResult func shadowEffect(_ effect: ShadowEffect) -> Self 12 | 13 | /// The maxmin alpha of `shadowView` in `GeminiCell`. the default value is 1.0. 14 | @discardableResult func maxShadowAlpha(_ alpha: CGFloat) -> Self 15 | 16 | /// The minimum alpha of `shadowView` in `GeminiCell`. the default value is 0.0. 17 | @discardableResult func minShadowAlpha(_ alpha: CGFloat) -> Self 18 | 19 | /// The item’s animatable alpha value in the range 0.0 to 1.0. the default value is 1.0. 20 | @discardableResult func alpha(_ alpha: CGFloat) -> Self 21 | 22 | /// The radius to use when drawing rounded corners. the default value is 0.0. 23 | @discardableResult func cornerRadius(_ radius: CGFloat) -> Self 24 | 25 | /// The item’s animatable backgroundColor. item’s backgroundColor changes from startColor to endColor. 26 | @discardableResult func backgroundColor(startColor: UIColor, endColor: UIColor) -> Self 27 | } 28 | 29 | extension GeminiAnimationModel: UIAppearanceAnimatable { 30 | @discardableResult 31 | public func shadowEffect(_ effect: ShadowEffect) -> Self { 32 | shadowEffect = effect 33 | return self 34 | } 35 | 36 | @discardableResult 37 | public func minShadowAlpha(_ alpha: CGFloat) -> Self { 38 | minShadowAlpha = alpha 39 | return self 40 | } 41 | 42 | @discardableResult 43 | public func maxShadowAlpha(_ alpha: CGFloat) -> Self { 44 | maxShadowAlpha = alpha 45 | return self 46 | } 47 | 48 | @discardableResult 49 | public func alpha(_ alpha: CGFloat) -> Self { 50 | self.alpha = alpha 51 | return self 52 | } 53 | 54 | @discardableResult 55 | public func cornerRadius(_ radius: CGFloat) -> Self { 56 | cornerRadius = radius 57 | return self 58 | } 59 | 60 | @discardableResult 61 | public func backgroundColor(startColor: UIColor, endColor: UIColor) -> Self { 62 | startBackgroundColor = startColor 63 | endBackgroundColor = endColor 64 | return self 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Gemini_Example/Pods-Gemini_Example-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Copyright (c) 2017 shoheiyokoyama <shohei.yok0602@gmail.com> 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy 20 | of this software and associated documentation files (the "Software"), to deal 21 | in the Software without restriction, including without limitation the rights 22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | copies of the Software, and to permit persons to whom the Software is 24 | furnished to do so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in 27 | all copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 35 | THE SOFTWARE. 36 | 37 | License 38 | MIT 39 | Title 40 | Gemini 41 | Type 42 | PSGroupSpecifier 43 | 44 | 45 | FooterText 46 | Generated by CocoaPods - https://cocoapods.org 47 | Title 48 | 49 | Type 50 | PSGroupSpecifier 51 | 52 | 53 | StringsTable 54 | Acknowledgements 55 | Title 56 | Acknowledgements 57 | 58 | 59 | -------------------------------------------------------------------------------- /Gemini/CustomAnimatable.swift: -------------------------------------------------------------------------------- 1 | extension GeminiAnimationModel { 2 | struct Coordinate { 3 | var x: CGFloat = 1 4 | var y: CGFloat = 1 5 | var z: CGFloat = 1 6 | } 7 | } 8 | 9 | public protocol CustomAnimatable: EasingAnimatable, UIAppearanceAnimatable { 10 | /// The Scale in 3-Dimensional vector. 11 | /// The default value is (x: 1, y: 1, z: 1). 12 | /// The range 0.0 to 1.0. 13 | @discardableResult func scale(x: CGFloat, y: CGFloat, z: CGFloat) -> CustomAnimatable 14 | 15 | @discardableResult func scaleEffect(_ effect: GeminScaleEffect) -> CustomAnimatable 16 | 17 | /// The Angre of rotation in 3-Dimensional vector. 18 | /// The default value is (x: 0, y: 0, z: 0). 19 | /// The range 0.0 to 90.0. 20 | @discardableResult func rotationAngle(x: CGFloat, y: CGFloat, z: CGFloat) -> CustomAnimatable 21 | 22 | /// The translation in 3-Dimensional vector. 23 | /// The default value is (x: 0, y: 0, z: 0). 24 | @discardableResult func translation(x: CGFloat, y: CGFloat, z: CGFloat) -> CustomAnimatable 25 | 26 | /// The anchor point of the layer's bounds rectangle. 27 | /// The default value is (x: 0.5, y: 0.5). 28 | /// - SeeAlso: [anchorPoint on Apple Developer Documentation](https://developer.apple.com/documentation/quartzcore/calayer/1410817-anchorpoint) 29 | @discardableResult func anchorPoint(_ anchorPoint: CGPoint) -> CustomAnimatable 30 | } 31 | 32 | extension GeminiAnimationModel: CustomAnimatable { 33 | public func scale(x: CGFloat = 1, y: CGFloat = 1, z: CGFloat = 1) -> CustomAnimatable { 34 | scaleCoordinate.x = x 35 | scaleCoordinate.y = y 36 | scaleCoordinate.z = z 37 | return self 38 | } 39 | 40 | @discardableResult 41 | public func scaleEffect(_ effect: GeminScaleEffect) -> CustomAnimatable { 42 | scaleEffect = effect 43 | return self 44 | } 45 | 46 | @discardableResult 47 | public func rotationAngle(x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) -> CustomAnimatable { 48 | rotationCoordinate.x = x 49 | rotationCoordinate.y = y 50 | rotationCoordinate.z = z 51 | return self 52 | } 53 | 54 | @discardableResult 55 | public func translation(x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) -> CustomAnimatable { 56 | translationCoordinate.x = x 57 | translationCoordinate.y = y 58 | translationCoordinate.z = z 59 | return self 60 | } 61 | 62 | @discardableResult 63 | public func anchorPoint(_ anchorPoint: CGPoint) -> CustomAnimatable { 64 | self.anchorPoint = anchorPoint 65 | return self 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Master 2 | 3 | ##### Breaking 4 | 5 | * None. 6 | 7 | ##### Enhancements 8 | 9 | * None. 10 | 11 | ##### Bug Fixes 12 | 13 | * [Fixed default values](https://github.com/shoheiyokoyama/Gemini/commit/6a37e6729df20f3f9aef590b0a3ecdafa1ca3ea5) 14 | 15 | ## [1.4.0](https://github.com/shoheiyokoyama/Gemini/releases/tag/1.4.0) 16 | 17 | ##### Breaking 18 | 19 | * None. 20 | 21 | ##### Enhancements 22 | 23 | * [Support Swift5](https://github.com/shoheiyokoyama/Gemini/pull/27/commits/5be4816f6c46326fef06f2edca6442422cef6121) 24 | * Refactoring 25 | * [Delete header comment](https://github.com/shoheiyokoyama/Gemini/pull/27/commits/b158445496004044cd231d49c02bd3541fc52d6f) 26 | * [Fix syntax](https://github.com/shoheiyokoyama/Gemini/pull/27/commits/014b5ed410cb555f0db9d3420e7eefcd29b0e510) 27 | * [Setup Gemfile](https://github.com/shoheiyokoyama/Gemini/commit/b06397e2efe96030cb75c8e9d3ba0816668e563d) 28 | * [Setup Makefile](https://github.com/shoheiyokoyama/Gemini/commit/64803caa2ae5a71f6a1470ae2dc27bb3fa7d3fbb) 29 | 30 | ##### Bug Fixes 31 | 32 | * None. 33 | 34 | ## [1.3.1](https://github.com/shoheiyokoyama/Gemini/releases/tag/1.3.1) 35 | 36 | ##### Breaking 37 | 38 | * None. 39 | 40 | ##### Enhancements 41 | 42 | * Support compatible with Xcode 10 43 | * Refactoring 44 | 45 | ##### Bug Fixes 46 | 47 | * None. 48 | 49 | ## [1.3.0](https://github.com/shoheiyokoyama/Gemini/releases/tag/1.3.0) 50 | 51 | ##### Breaking 52 | 53 | * None. 54 | 55 | ##### Enhancements 56 | 57 | * Fixed settings 58 | * [Set SWIFT_SWIFT3_OBJC_INFERENCE to Default](https://github.com/shoheiyokoyama/Gemini/commit/9ae0fc35840c627d8ec74b75dafd50c6aaef77d1) 59 | * Update project setting #16 60 | * Support Swift4.1 #17 61 | 62 | ##### Bug Fixes 63 | 64 | * Supports layout for iPhoneX in Example project #15 65 | 66 | ## [1.2.0](https://github.com/shoheiyokoyama/Gemini/releases/tag/1.2.0) 67 | 68 | ##### Breaking 69 | 70 | * None. 71 | 72 | ##### Enhancements 73 | 74 | * Adds `isItemRotationEnabled` for circleRotation. 75 | * Supports Xcode 9 and Fix warning. 76 | * [Update project settings for xcode 9](https://github.com/shoheiyokoyama/Gemini/commit/d3c01551843ed49d40b421ef8b0454051428e4e7) 77 | * [Delete redudant protocol](https://github.com/shoheiyokoyama/Gemini/commit/6d223150368f341f8886f0fe1bc5083751317111) 78 | * Supports Swift4. 79 | * [Supports swift4](https://github.com/shoheiyokoyama/Gemini/commit/a7eda4f12ea1d5ee769479257cfb625076b64bb3) 80 | 81 | ##### Bug Fixes 82 | 83 | * Fixed compiler error for Swift 4 84 | * [Refactoring backgroundColor(withDistanceRatio:)](https://github.com/shoheiyokoyama/Gemini/commit/a293931b94e09ad5450be005309c78e8bc009567) 85 | -------------------------------------------------------------------------------- /Example/Pods/Pods.xcodeproj/xcshareddata/xcschemes/Gemini.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 66 | 67 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /Example/Gemini/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | 6 | var window: UIWindow? 7 | 8 | 9 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 10 | window = UIWindow(frame: UIScreen.main.bounds) 11 | let storyboard = UIStoryboard(name: "AnimationListViewController", bundle: nil) 12 | let viewController = storyboard.instantiateViewController(withIdentifier: "AnimationListViewController") as! AnimationListViewController 13 | window?.rootViewController = UINavigationController(rootViewController: viewController) 14 | window?.makeKeyAndVisible() 15 | 16 | UINavigationBar.appearance().tintColor = .white 17 | UINavigationBar.appearance().titleTextAttributes = [NSAttributedString.Key.foregroundColor : UIColor.white] 18 | UINavigationBar.appearance().setBackgroundImage(image(of: UIColor.black.withAlphaComponent(0.75)), for: .default) 19 | UINavigationBar.appearance().shadowImage = UIImage() 20 | 21 | return true 22 | } 23 | 24 | private func image(of color: UIColor) -> UIImage? { 25 | let size = CGSize(width: 1, height: 1) 26 | UIGraphicsBeginImageContext(size) 27 | guard let contextRef = UIGraphicsGetCurrentContext() else { return nil } 28 | contextRef.setFillColor(color.cgColor) 29 | contextRef.fill(CGRect(origin: .zero, size: size)) 30 | guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return nil } 31 | return image 32 | } 33 | 34 | func applicationWillResignActive(_ application: UIApplication) { 35 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 36 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 37 | } 38 | 39 | func applicationDidEnterBackground(_ application: UIApplication) { 40 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 41 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 42 | } 43 | 44 | func applicationWillEnterForeground(_ application: UIApplication) { 45 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 46 | } 47 | 48 | func applicationDidBecomeActive(_ application: UIApplication) { 49 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 50 | } 51 | 52 | func applicationWillTerminate(_ application: UIApplication) { 53 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 54 | } 55 | 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /Example/Gemini/ViewControllers/AnimationListViewController.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Gemini/GeminiCollectionView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public final class GeminiCollectionView: UICollectionView { 4 | public let gemini: Gemini = GeminiAnimationModel() 5 | 6 | private var animationModel: GeminiAnimationModel? { 7 | return gemini as? GeminiAnimationModel 8 | } 9 | 10 | override public var collectionViewLayout: UICollectionViewLayout { 11 | didSet { 12 | updateScrollDirection(with: collectionViewLayout) 13 | } 14 | } 15 | 16 | required public init?(coder aDecoder: NSCoder) { 17 | super.init(coder: aDecoder) 18 | updateScrollDirection(with: collectionViewLayout) 19 | } 20 | 21 | override public init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { 22 | super.init(frame: frame, collectionViewLayout: layout) 23 | updateScrollDirection(with: layout) 24 | } 25 | 26 | override public func setCollectionViewLayout(_ layout: UICollectionViewLayout, animated: Bool) { 27 | super.setCollectionViewLayout(layout, animated: animated) 28 | updateScrollDirection(with: layout) 29 | } 30 | 31 | override public func setCollectionViewLayout(_ layout: UICollectionViewLayout, animated: Bool, completion: ((Bool) -> Swift.Void)? = nil) { 32 | super.setCollectionViewLayout(layout, animated: animated, completion: completion) 33 | updateScrollDirection(with: layout) 34 | } 35 | 36 | /// Call this method in `scrollViewDidScroll(_:)`. 37 | public func animateVisibleCells() { 38 | guard let model = animationModel, model.isEnabled else { return } 39 | 40 | visibleCells 41 | .compactMap { $0 as? GeminiCell } 42 | .forEach(animateCell) 43 | } 44 | 45 | /// Call this method `collectionView(_:cellForItemAt:)` and `collectionView(_:willDisplay:forItemAt:)`. 46 | public func animateCell(_ cell: GeminiCell) { 47 | guard let model = animationModel, model.isEnabled else { return } 48 | 49 | let convertedFrame = convert(cell.frame, to: superview) 50 | let distance = model.distanceFromCenter(withParentFrame: frame, cellFrame: convertedFrame) 51 | 52 | if model.needsCheckDistance && 53 | abs(distance) >= model.visibleMaxDistance(withParentFrame: frame, cellFrame: convertedFrame) { 54 | return 55 | } 56 | 57 | let ratio = model.distanceRatio(withParentFrame: frame, cellFrame: convertedFrame) 58 | let easingRatio = model.easing.value(withRatio: ratio) 59 | 60 | // Configure cell appearance properties 61 | cell.shadowView?.alpha = model.shadowAlpha(withDistanceRatio: easingRatio) 62 | if let alpha = model.alpha(withDistanceRatio: easingRatio) { 63 | cell.alpha = alpha 64 | } 65 | if let cornerRadius = model.cornerRadius(withDistanceRatio: easingRatio) { 66 | cell.layer.cornerRadius = cornerRadius 67 | } 68 | if let backgroundColor = model.backgroundColor(withDistanceRatio: easingRatio) { 69 | cell.backgroundColor = backgroundColor 70 | } 71 | 72 | // Configure transform of CALayer 73 | // Needs set anchor point before setting transform 74 | cell.adjustAnchorPoint(model.anchorPoint(withDistanceRatio: easingRatio)) 75 | cell.layer.transform = model.transform(withParentFrame: frame, cellFrame: convertedFrame) 76 | } 77 | 78 | private func updateScrollDirection(with layout: UICollectionViewLayout) { 79 | (layout as? UICollectionViewFlowLayout).map { animationModel?.scrollDirection = $0.scrollDirection } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Example/Gemini/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Example/Gemini/Views/ImageCollectionViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Example/Gemini/Views/PlayerCollectionViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Example/Gemini/ViewControllers/ScaleAnimationViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Gemini 3 | 4 | final class ScaleAnimationViewController: UIViewController { 5 | @IBOutlet private weak var collectionView: GeminiCollectionView! { 6 | didSet { 7 | let nib = UINib(nibName: cellIdentifier, bundle: nil) 8 | collectionView.register(nib, forCellWithReuseIdentifier: cellIdentifier) 9 | collectionView.delegate = self 10 | collectionView.dataSource = self 11 | 12 | if #available(iOS 11.0, *) { 13 | collectionView.contentInsetAdjustmentBehavior = .never 14 | } 15 | 16 | collectionView.gemini 17 | .scaleAnimation() 18 | .scale(0.75) 19 | .scaleEffect(scaleEffect) 20 | .ease(.easeOutQuart) 21 | } 22 | } 23 | 24 | private let cellIdentifier = String(describing: ImageCollectionViewCell.self) 25 | private var scrollDirection = UICollectionView.ScrollDirection.horizontal 26 | private var scaleEffect = GeminScaleEffect.scaleUp 27 | private let images = Resource.image.images 28 | 29 | static func make(scrollDirection: UICollectionView.ScrollDirection, scaleEffect: GeminScaleEffect) -> ScaleAnimationViewController { 30 | let storyboard = UIStoryboard(name: "ScaleAnimationViewController", bundle: nil) 31 | let viewController = storyboard.instantiateViewController(withIdentifier: "ScaleAnimationViewController") as! ScaleAnimationViewController 32 | viewController.scrollDirection = scrollDirection 33 | viewController.scaleEffect = scaleEffect 34 | return viewController 35 | } 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | navigationController?.setNavigationBarHidden(true, animated: false) 41 | let gesture = UITapGestureRecognizer(target: self, action: #selector(toggleNavigationBarHidden(_:))) 42 | gesture.cancelsTouchesInView = false 43 | view.addGestureRecognizer(gesture) 44 | 45 | let layout = UICollectionViewPagingFlowLayout() 46 | layout.scrollDirection = scrollDirection 47 | layout.itemSize = CGSize(width: view.bounds.width - 80, height: view.bounds.height - 400) 48 | layout.sectionInset = UIEdgeInsets(top: 200, left: 40, bottom: 200, right: 40) 49 | layout.minimumLineSpacing = 40 50 | layout.minimumInteritemSpacing = 40 51 | collectionView.collectionViewLayout = layout 52 | collectionView.decelerationRate = UIScrollView.DecelerationRate.fast 53 | } 54 | 55 | @objc private func toggleNavigationBarHidden(_ gestureRecognizer: UITapGestureRecognizer) { 56 | let isNavigationBarHidden = navigationController?.isNavigationBarHidden ?? true 57 | navigationController?.setNavigationBarHidden(!isNavigationBarHidden, animated: true) 58 | } 59 | } 60 | 61 | // MARK: - UIScrollViewDelegate 62 | 63 | extension ScaleAnimationViewController { 64 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 65 | collectionView.animateVisibleCells() 66 | } 67 | } 68 | 69 | // MARK: - UICollectionViewDelegate 70 | 71 | extension ScaleAnimationViewController: UICollectionViewDelegate { 72 | func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 73 | if let cell = cell as? GeminiCell { 74 | self.collectionView.animateCell(cell) 75 | } 76 | } 77 | } 78 | 79 | // MARK: - UICollectionViewDataSource 80 | 81 | extension ScaleAnimationViewController: UICollectionViewDataSource { 82 | func numberOfSections(in collectionView: UICollectionView) -> Int { 83 | return 1 84 | } 85 | 86 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 87 | return images.count 88 | } 89 | 90 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 91 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! ImageCollectionViewCell 92 | cell.configure(with: images[indexPath.row]) 93 | self.collectionView.animateCell(cell) 94 | return cell 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Example/Gemini/ViewControllers/YawRotationViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Gemini 3 | 4 | final class YawRotationViewController: UIViewController { 5 | @IBOutlet private weak var collectionView: GeminiCollectionView! { 6 | didSet { 7 | let nib = UINib(nibName: cellIdentifier, bundle: nil) 8 | collectionView.register(nib, forCellWithReuseIdentifier: cellIdentifier) 9 | collectionView.delegate = self 10 | collectionView.dataSource = self 11 | collectionView.backgroundColor = UIColor(red: 255 / 255, green: 212 / 255, blue: 100 / 255, alpha: 1) 12 | 13 | if #available(iOS 11.0, *) { 14 | collectionView.contentInsetAdjustmentBehavior = .never 15 | } 16 | 17 | collectionView.gemini 18 | .yawRotationAnimation() 19 | .scale(0.7) 20 | .yawEffect(rotationEffect) 21 | } 22 | } 23 | 24 | private let cellIdentifier = String(describing: ImageCollectionViewCell.self) 25 | private var rotationEffect = YawRotationEffect.yawUp 26 | private var scrollDirection = UICollectionView.ScrollDirection.horizontal 27 | private let images = Resource.image.images 28 | 29 | static func make(scrollDirection: UICollectionView.ScrollDirection, effect: YawRotationEffect) -> YawRotationViewController { 30 | let storyboard = UIStoryboard(name: "YawRotationViewController", bundle: nil) 31 | let viewController = storyboard.instantiateViewController(withIdentifier: "YawRotationViewController") as! YawRotationViewController 32 | viewController.rotationEffect = effect 33 | viewController.scrollDirection = scrollDirection 34 | return viewController 35 | } 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | navigationController?.setNavigationBarHidden(true, animated: false) 41 | let gesture = UITapGestureRecognizer(target: self, action: #selector(toggleNavigationBarHidden(_:))) 42 | gesture.cancelsTouchesInView = false 43 | view.addGestureRecognizer(gesture) 44 | 45 | let layout = UICollectionViewPagingFlowLayout() 46 | layout.scrollDirection = scrollDirection 47 | layout.itemSize = CGSize(width: view.bounds.width - 50, height: view.bounds.width - 50) 48 | let cellHeight: CGFloat = view.bounds.width - 50 49 | layout.sectionInset = UIEdgeInsets(top: (view.bounds.height - cellHeight) / 2, left: 25, bottom: (view.bounds.height - cellHeight) / 2, right: 25) 50 | layout.minimumLineSpacing = 80 51 | layout.minimumInteritemSpacing = 80 52 | collectionView.collectionViewLayout = layout 53 | collectionView.decelerationRate = UIScrollView.DecelerationRate.fast 54 | } 55 | 56 | @objc private func toggleNavigationBarHidden(_ gestureRecognizer: UITapGestureRecognizer) { 57 | let isNavigationBarHidden = navigationController?.isNavigationBarHidden ?? true 58 | navigationController?.setNavigationBarHidden(!isNavigationBarHidden, animated: true) 59 | } 60 | } 61 | 62 | // MARK: - UIScrollViewDelegate 63 | 64 | extension YawRotationViewController { 65 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 66 | collectionView.animateVisibleCells() 67 | } 68 | } 69 | 70 | // MARK: - UICollectionViewDelegate 71 | 72 | extension YawRotationViewController: UICollectionViewDelegate { 73 | func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 74 | if let cell = cell as? GeminiCell { 75 | self.collectionView.animateCell(cell) 76 | } 77 | } 78 | } 79 | 80 | // MARK: - UICollectionViewDataSource 81 | 82 | extension YawRotationViewController: UICollectionViewDataSource { 83 | func numberOfSections(in collectionView: UICollectionView) -> Int { 84 | return 1 85 | } 86 | 87 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 88 | return images.count 89 | } 90 | 91 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 92 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! ImageCollectionViewCell 93 | cell.configure(with: images[indexPath.row]) 94 | self.collectionView.animateCell(cell) 95 | return cell 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Example/Gemini/ViewControllers/CubeViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Gemini 3 | 4 | final class CubeViewController: UIViewController { 5 | @IBOutlet private weak var collectionView: GeminiCollectionView! { 6 | didSet { 7 | let nib = UINib(nibName: cellIdentifier, bundle: nil) 8 | collectionView.register(nib, forCellWithReuseIdentifier: cellIdentifier) 9 | collectionView.delegate = self 10 | collectionView.dataSource = self 11 | collectionView.isPagingEnabled = true 12 | 13 | if #available(iOS 11.0, *) { 14 | collectionView.contentInsetAdjustmentBehavior = .never 15 | } 16 | 17 | collectionView.gemini 18 | .cubeAnimation() 19 | .shadowEffect(.fadeIn) 20 | } 21 | } 22 | 23 | private let cellIdentifier = String(describing: PlayerCollectionViewCell.self) 24 | private var direction: UICollectionView.ScrollDirection = .horizontal 25 | private var movieURLs = Resource.movie.urls 26 | 27 | static func make(scrollDirection: UICollectionView.ScrollDirection) -> CubeViewController { 28 | let storyboard = UIStoryboard(name: "CubeViewController", bundle: nil) 29 | let viewController = storyboard.instantiateViewController(withIdentifier: "CubeViewController") as! CubeViewController 30 | viewController.direction = scrollDirection 31 | return viewController 32 | } 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | 37 | if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout { 38 | layout.scrollDirection = direction 39 | collectionView.collectionViewLayout = layout 40 | } 41 | 42 | navigationController?.setNavigationBarHidden(true, animated: false) 43 | let gesture = UITapGestureRecognizer(target: self, action: #selector(toggleNavigationBarHidden(_:))) 44 | gesture.cancelsTouchesInView = false 45 | view.addGestureRecognizer(gesture) 46 | } 47 | 48 | @objc private func toggleNavigationBarHidden(_ gestureRecognizer: UITapGestureRecognizer) { 49 | let isNavigationBarHidden = navigationController?.isNavigationBarHidden ?? true 50 | navigationController?.setNavigationBarHidden(!isNavigationBarHidden, animated: true) 51 | } 52 | } 53 | 54 | // MARK: - UIScrollViewDelegate 55 | 56 | extension CubeViewController { 57 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 58 | collectionView.animateVisibleCells() 59 | 60 | collectionView.visibleCells 61 | .compactMap { $0 as? PlayerCollectionViewCell } 62 | .forEach { $0.playerView.pause() } 63 | } 64 | 65 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 66 | (collectionView.visibleCells.first as? PlayerCollectionViewCell)?.playerView.play() 67 | } 68 | } 69 | 70 | // MARK: - UICollectionViewDataSource 71 | 72 | extension CubeViewController: UICollectionViewDataSource { 73 | func numberOfSections(in collectionView: UICollectionView) -> Int { 74 | return 1 75 | } 76 | 77 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 78 | return movieURLs.count 79 | } 80 | 81 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 82 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! PlayerCollectionViewCell 83 | cell.configure(with: movieURLs[indexPath.row]) 84 | return cell 85 | } 86 | } 87 | 88 | // MARK: - UICollectionViewDelegateFlowLayout 89 | 90 | extension CubeViewController: UICollectionViewDelegateFlowLayout { 91 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 92 | return CGSize(width: collectionView.frame.width, height: collectionView.frame.height) 93 | } 94 | 95 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 96 | return .zero 97 | } 98 | 99 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 100 | return 0 101 | } 102 | 103 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 104 | return 0 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Example/Gemini/ViewControllers/PitchRotationViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Gemini 3 | 4 | final class PitchRotationViewController: UIViewController { 5 | @IBOutlet private weak var collectionView: GeminiCollectionView! { 6 | didSet { 7 | let nib = UINib(nibName: cellIdentifier, bundle: nil) 8 | collectionView.register(nib, forCellWithReuseIdentifier: cellIdentifier) 9 | collectionView.delegate = self 10 | collectionView.dataSource = self 11 | collectionView.backgroundColor = .clear 12 | 13 | if #available(iOS 11.0, *) { 14 | collectionView.contentInsetAdjustmentBehavior = .never 15 | } 16 | 17 | collectionView.gemini 18 | .pitchRotationAnimation() 19 | .scale(0.7) 20 | .pitchEffect(rotationEffect) 21 | } 22 | } 23 | 24 | private let cellIdentifier = String(describing: ImageCollectionViewCell.self) 25 | private var rotationEffect = PitchRotationEffect.pitchUp 26 | private var scrollDirection = UICollectionView.ScrollDirection.horizontal 27 | private let images = Resource.image.images 28 | 29 | static func make(scrollDirection: UICollectionView.ScrollDirection, effect: PitchRotationEffect) -> PitchRotationViewController { 30 | let storyboard = UIStoryboard(name: "PitchRotationViewController", bundle: nil) 31 | let viewController = storyboard.instantiateViewController(withIdentifier: "PitchRotationViewController") as! PitchRotationViewController 32 | viewController.rotationEffect = effect 33 | viewController.scrollDirection = scrollDirection 34 | return viewController 35 | } 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | navigationController?.setNavigationBarHidden(true, animated: false) 41 | let gesture = UITapGestureRecognizer(target: self, action: #selector(toggleNavigationBarHidden(_:))) 42 | gesture.cancelsTouchesInView = false 43 | view.addGestureRecognizer(gesture) 44 | 45 | let layout = UICollectionViewPagingFlowLayout() 46 | layout.scrollDirection = scrollDirection 47 | layout.itemSize = CGSize(width: view.bounds.width - 60, height: view.bounds.height - 100) 48 | layout.sectionInset = UIEdgeInsets(top: 50, left: 30, bottom: 50, right: 30) 49 | layout.minimumLineSpacing = 30 50 | layout.minimumInteritemSpacing = 30 51 | collectionView.collectionViewLayout = layout 52 | collectionView.decelerationRate = UIScrollView.DecelerationRate.fast 53 | 54 | let startColor = UIColor(red: 238 / 255, green: 156 / 255, blue: 167 / 255, alpha: 1) 55 | let endColor = UIColor(red: 225 / 255, green: 221 / 255, blue: 225 / 255, alpha: 1) 56 | let colors: [CGColor] = [startColor.cgColor, endColor.cgColor] 57 | let gradientLayer = CAGradientLayer() 58 | gradientLayer.colors = colors 59 | gradientLayer.frame.size = view.frame.size 60 | view.layer.insertSublayer(gradientLayer, at: 0) 61 | } 62 | 63 | @objc func toggleNavigationBarHidden(_ gestureRecognizer: UITapGestureRecognizer) { 64 | let isNavigationBarHidden = navigationController?.isNavigationBarHidden ?? true 65 | navigationController?.setNavigationBarHidden(!isNavigationBarHidden, animated: true) 66 | } 67 | } 68 | 69 | // MARK: - UIScrollViewDelegate 70 | 71 | extension PitchRotationViewController { 72 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 73 | collectionView.animateVisibleCells() 74 | } 75 | } 76 | 77 | // MARK: - UICollectionViewDelegate 78 | 79 | extension PitchRotationViewController: UICollectionViewDelegate { 80 | func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 81 | if let cell = cell as? GeminiCell { 82 | self.collectionView.animateCell(cell) 83 | } 84 | } 85 | } 86 | 87 | // MARK: - UICollectionViewDataSource 88 | 89 | extension PitchRotationViewController: UICollectionViewDataSource { 90 | func numberOfSections(in collectionView: UICollectionView) -> Int { 91 | return 1 92 | } 93 | 94 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 95 | return images.count 96 | } 97 | 98 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 99 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! ImageCollectionViewCell 100 | cell.configure(with: images[indexPath.row]) 101 | self.collectionView.animateCell(cell) 102 | return cell 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Example/Gemini/ViewControllers/RollRotationViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Gemini 3 | 4 | final class RollRotationViewController: UIViewController { 5 | @IBOutlet private weak var collectionView: GeminiCollectionView! { 6 | didSet { 7 | let nib = UINib(nibName: cellIdentifier, bundle: nil) 8 | collectionView.register(nib, forCellWithReuseIdentifier: cellIdentifier) 9 | collectionView.backgroundColor = .clear 10 | collectionView.delegate = self 11 | collectionView.dataSource = self 12 | 13 | if #available(iOS 11.0, *) { 14 | collectionView.contentInsetAdjustmentBehavior = .never 15 | } 16 | 17 | collectionView.gemini 18 | .rollRotationAnimation() 19 | .rollEffect(rotationEffect) 20 | .scale(0.7) 21 | } 22 | } 23 | 24 | private let cellIdentifier = String(describing: ImageCollectionViewCell.self) 25 | private var rotationEffect = RollRotationEffect.rollUp 26 | private var scrollDirection = UICollectionView.ScrollDirection.horizontal 27 | private let images = Resource.image.images 28 | 29 | static func make(scrollDirection: UICollectionView.ScrollDirection, effect: RollRotationEffect) -> RollRotationViewController { 30 | let storyboard = UIStoryboard(name: "RollRotationViewController", bundle: nil) 31 | let viewController = storyboard.instantiateViewController(withIdentifier: "RollRotationViewController") as! RollRotationViewController 32 | viewController.rotationEffect = effect 33 | viewController.scrollDirection = scrollDirection 34 | return viewController 35 | } 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | let layout = UICollectionViewPagingFlowLayout() 41 | layout.scrollDirection = scrollDirection 42 | layout.itemSize = CGSize(width: view.bounds.width - 60, height: view.bounds.height - 100) 43 | layout.sectionInset = UIEdgeInsets(top: 50, left: 30, bottom: 50, right: 30) 44 | layout.minimumLineSpacing = 30 45 | layout.minimumInteritemSpacing = 30 46 | collectionView.collectionViewLayout = layout 47 | collectionView.decelerationRate = UIScrollView.DecelerationRate.fast 48 | 49 | navigationController?.setNavigationBarHidden(true, animated: false) 50 | let gesture = UITapGestureRecognizer(target: self, action: #selector(toggleNavigationBarHidden(_:))) 51 | gesture.cancelsTouchesInView = false 52 | view.addGestureRecognizer(gesture) 53 | 54 | let startColor = UIColor(red: 29 / 255, green: 44 / 255, blue: 76 / 255, alpha: 1) 55 | let endColor = UIColor(red: 3 / 255, green: 7 / 255, blue: 20 / 255, alpha: 1) 56 | let colors: [CGColor] = [startColor.cgColor, endColor.cgColor] 57 | let gradientLayer = CAGradientLayer() 58 | gradientLayer.colors = colors 59 | gradientLayer.frame.size = view.bounds.size 60 | view.layer.insertSublayer(gradientLayer, at: 0) 61 | } 62 | 63 | override func viewDidAppear(_ animated: Bool) { 64 | super.viewDidAppear(animated) 65 | } 66 | 67 | @objc private func toggleNavigationBarHidden(_ gestureRecognizer: UITapGestureRecognizer) { 68 | let isNavigationBarHidden = navigationController?.isNavigationBarHidden ?? true 69 | navigationController?.setNavigationBarHidden(!isNavigationBarHidden, animated: true) 70 | } 71 | } 72 | 73 | // MARK: - UIScrollViewDelegate 74 | 75 | extension RollRotationViewController { 76 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 77 | collectionView.animateVisibleCells() 78 | } 79 | } 80 | 81 | // MARK: - UICollectionViewDelegate 82 | 83 | extension RollRotationViewController: UICollectionViewDelegate { 84 | func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 85 | if let cell = cell as? GeminiCell { 86 | self.collectionView.animateCell(cell) 87 | } 88 | } 89 | } 90 | 91 | // MARK: - UICollectionViewDataSource 92 | 93 | extension RollRotationViewController: UICollectionViewDataSource { 94 | func numberOfSections(in collectionView: UICollectionView) -> Int { 95 | return 1 96 | } 97 | 98 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 99 | return images.count 100 | } 101 | 102 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 103 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! ImageCollectionViewCell 104 | cell.configure(with: images[indexPath.row]) 105 | self.collectionView.animateCell(cell) 106 | return cell 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Example/Gemini/ViewControllers/CubeViewController.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Example/Gemini/ViewControllers/YawRotationViewController.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Example/Gemini/ViewControllers/CircleRotationViewController.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Example/Gemini/ViewControllers/ScaleAnimationViewController.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Example/Gemini/ViewControllers/RollRotationViewController.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Example/Gemini/ViewControllers/PitchRotationViewController.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Example/Gemini/ViewControllers/CustomAnimationViewController.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Example/Gemini/ViewControllers/CustomAnimationViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Gemini 3 | 4 | enum CustomAnimationType { 5 | case custom1 6 | case custom2 7 | 8 | fileprivate func layout(withParentView parentView: UIView) -> UICollectionViewFlowLayout { 9 | switch self { 10 | case .custom1: 11 | let layout = UICollectionViewPagingFlowLayout() 12 | layout.itemSize = CGSize(width: parentView.bounds.width - 100, height: parentView.bounds.height - 200) 13 | layout.sectionInset = UIEdgeInsets(top: 0, left: 50, bottom: 0, right: 50) 14 | layout.minimumLineSpacing = 10 15 | layout.scrollDirection = .horizontal 16 | return layout 17 | 18 | case .custom2: 19 | let layout = UICollectionViewFlowLayout() 20 | layout.itemSize = CGSize(width: 150, height: 150) 21 | layout.sectionInset = UIEdgeInsets(top: 15, 22 | left: (parentView.bounds.width - 150) / 2, 23 | bottom: 15, 24 | right: (parentView.bounds.width - 150) / 2) 25 | layout.minimumLineSpacing = 15 26 | layout.scrollDirection = .vertical 27 | return layout 28 | } 29 | } 30 | } 31 | 32 | final class CustomAnimationViewController: UIViewController { 33 | @IBOutlet private weak var collectionView: GeminiCollectionView! { 34 | didSet { 35 | let nib = UINib(nibName: cellIdentifier, bundle: nil) 36 | collectionView.register(nib, forCellWithReuseIdentifier: cellIdentifier) 37 | collectionView.delegate = self 38 | collectionView.dataSource = self 39 | 40 | if #available(iOS 11.0, *) { 41 | collectionView.contentInsetAdjustmentBehavior = .never 42 | } 43 | } 44 | } 45 | 46 | private let cellIdentifier = String(describing: ImageCollectionViewCell.self) 47 | private let images = Resource.image.images 48 | private var animationType = CustomAnimationType.custom2 49 | 50 | static func make(animationType: CustomAnimationType) -> CustomAnimationViewController { 51 | let storyboard = UIStoryboard(name: "CustomAnimationViewController", bundle: nil) 52 | let viewController = storyboard.instantiateViewController(withIdentifier: "CustomAnimationViewController") as! CustomAnimationViewController 53 | viewController.animationType = animationType 54 | return viewController 55 | } 56 | 57 | override func viewDidLoad() { 58 | super.viewDidLoad() 59 | 60 | navigationController?.setNavigationBarHidden(true, animated: false) 61 | let gesture = UITapGestureRecognizer(target: self, action: #selector(toggleNavigationBarHidden(_:))) 62 | gesture.cancelsTouchesInView = false 63 | view.addGestureRecognizer(gesture) 64 | 65 | switch animationType { 66 | case .custom1: 67 | collectionView.collectionViewLayout = animationType.layout(withParentView: view) 68 | collectionView.decelerationRate = UIScrollView.DecelerationRate.fast 69 | collectionView.gemini 70 | .customAnimation() 71 | .translation(y: 50) 72 | .rotationAngle(y: 13) 73 | .ease(.easeOutExpo) 74 | .shadowEffect(.fadeIn) 75 | .maxShadowAlpha(0.3) 76 | 77 | case .custom2: 78 | collectionView.collectionViewLayout = animationType.layout(withParentView: view) 79 | collectionView.gemini 80 | .customAnimation() 81 | .backgroundColor(startColor: UIColor(red: 38 / 255, green: 194 / 255, blue: 129 / 255, alpha: 1), 82 | endColor: UIColor(red: 89 / 255, green: 171 / 255, blue: 227 / 255, alpha: 1)) 83 | .ease(.easeOutSine) 84 | .cornerRadius(75) 85 | } 86 | } 87 | 88 | @objc private func toggleNavigationBarHidden(_ gestureRecognizer: UITapGestureRecognizer) { 89 | let isNavigationBarHidden = navigationController?.isNavigationBarHidden ?? true 90 | navigationController?.setNavigationBarHidden(!isNavigationBarHidden, animated: true) 91 | } 92 | } 93 | 94 | // MARK: - UIScrollViewDelegate 95 | 96 | extension CustomAnimationViewController { 97 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 98 | collectionView.animateVisibleCells() 99 | } 100 | } 101 | 102 | // MARK: - UICollectionViewDelegate 103 | 104 | extension CustomAnimationViewController: UICollectionViewDelegate { 105 | func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 106 | if let cell = cell as? GeminiCell { 107 | self.collectionView.animateCell(cell) 108 | } 109 | } 110 | } 111 | 112 | // MARK: - UICollectionViewDataSource 113 | 114 | extension CustomAnimationViewController: UICollectionViewDataSource { 115 | func numberOfSections(in collectionView: UICollectionView) -> Int { 116 | return 1 117 | } 118 | 119 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 120 | return images.count 121 | } 122 | 123 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 124 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! ImageCollectionViewCell 125 | 126 | if animationType == .custom1 { 127 | cell.configure(with: images[indexPath.row]) 128 | } 129 | 130 | self.collectionView.animateCell(cell) 131 | return cell 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Example/Gemini/ViewControllers/CircleRotationViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Gemini 3 | 4 | final class CircleRotationViewController: UIViewController { 5 | static func make(scrollDirection: UICollectionView.ScrollDirection, rotateDirection: CircleRotationDirection) -> CircleRotationViewController { 6 | let storyboard = UIStoryboard(name: "CircleRotationViewController", bundle: nil) 7 | let viewController = storyboard.instantiateViewController(withIdentifier: "CircleRotationViewController") as! CircleRotationViewController 8 | viewController.scrollDirection = scrollDirection 9 | viewController.rotateDirection = rotateDirection 10 | return viewController 11 | } 12 | 13 | private let cellIdentifier = String(describing: ImageCollectionViewCell.self) 14 | private let images = Resource.image.images 15 | private var scrollDirection = UICollectionView.ScrollDirection.horizontal 16 | private var rotateDirection = CircleRotationDirection.clockwise 17 | 18 | @IBOutlet private weak var collectionView: GeminiCollectionView! { 19 | didSet { 20 | let nib = UINib(nibName: cellIdentifier, bundle: nil) 21 | collectionView.register(nib, forCellWithReuseIdentifier: cellIdentifier) 22 | collectionView.delegate = self 23 | collectionView.dataSource = self 24 | collectionView.backgroundColor = UIColor(red: 234 / 255, green: 242 / 255, blue: 248 / 255, alpha: 1) 25 | 26 | if #available(iOS 11.0, *) { 27 | collectionView.contentInsetAdjustmentBehavior = .never 28 | } 29 | 30 | collectionView.gemini 31 | .circleRotationAnimation() 32 | .radius(450) 33 | .rotateDirection(rotateDirection) 34 | .itemRotationEnabled(true) 35 | } 36 | } 37 | 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | 41 | if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout { 42 | layout.scrollDirection = scrollDirection 43 | collectionView.collectionViewLayout = layout 44 | } 45 | 46 | navigationController?.setNavigationBarHidden(true, animated: false) 47 | let gesture = UITapGestureRecognizer(target: self, action: #selector(toggleNavigationBarHidden(_:))) 48 | gesture.cancelsTouchesInView = false 49 | view.addGestureRecognizer(gesture) 50 | } 51 | 52 | @objc private func toggleNavigationBarHidden(_ gestureRecognizer: UITapGestureRecognizer) { 53 | let isNavigationBarHidden = navigationController?.isNavigationBarHidden ?? true 54 | navigationController?.setNavigationBarHidden(!isNavigationBarHidden, animated: true) 55 | } 56 | } 57 | 58 | // MARK: - UIScrollViewDelegate 59 | 60 | extension CircleRotationViewController { 61 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 62 | collectionView.animateVisibleCells() 63 | } 64 | } 65 | 66 | // MARK: - UICollectionViewDelegate 67 | 68 | extension CircleRotationViewController: UICollectionViewDelegate { 69 | func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 70 | if let cell = cell as? GeminiCell { 71 | self.collectionView.animateCell(cell) 72 | } 73 | } 74 | } 75 | 76 | // MARK: - UICollectionViewDataSource 77 | 78 | extension CircleRotationViewController: UICollectionViewDataSource { 79 | func numberOfSections(in collectionView: UICollectionView) -> Int { 80 | return 1 81 | } 82 | 83 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 84 | return images.count 85 | } 86 | 87 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 88 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! ImageCollectionViewCell 89 | cell.configure(with: images[indexPath.row]) 90 | self.collectionView.animateCell(cell) 91 | return cell 92 | } 93 | } 94 | 95 | // MARK: - UICollectionViewDelegateFlowLayout 96 | 97 | extension CircleRotationViewController: UICollectionViewDelegateFlowLayout { 98 | private enum Const { 99 | static let collectionViewSize = CGSize(width: 200, height: 350) 100 | } 101 | 102 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 103 | return Const.collectionViewSize 104 | } 105 | 106 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 107 | guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { 108 | return UIEdgeInsets(top: 50, left: 50, bottom: 50, right: 50) 109 | } 110 | 111 | switch layout.scrollDirection { 112 | case .horizontal: 113 | let verticalMargin: CGFloat = (collectionView.bounds.height - Const.collectionViewSize.height) / 2 114 | return UIEdgeInsets(top: 50 + verticalMargin, 115 | left: 50, 116 | bottom: 50 + verticalMargin, 117 | right: 50) 118 | 119 | case .vertical: 120 | let horizontalMargin: CGFloat = (collectionView.bounds.width - Const.collectionViewSize.width) / 2 121 | return UIEdgeInsets(top: 50, 122 | left: 50 + horizontalMargin, 123 | bottom: 50, 124 | right: 50 + horizontalMargin) 125 | } 126 | } 127 | 128 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 129 | return 10 130 | } 131 | 132 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 133 | return 0 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Gemini_Example/Pods-Gemini_Example-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | if [ -z ${UNLOCALIZED_RESOURCES_FOLDER_PATH+x} ]; then 7 | # If UNLOCALIZED_RESOURCES_FOLDER_PATH is not set, then there's nowhere for us to copy 8 | # resources to, so exit 0 (signalling the script phase was successful). 9 | exit 0 10 | fi 11 | 12 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 13 | 14 | RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt 15 | > "$RESOURCES_TO_COPY" 16 | 17 | XCASSET_FILES=() 18 | 19 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 20 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 21 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 22 | 23 | case "${TARGETED_DEVICE_FAMILY:-}" in 24 | 1,2) 25 | TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" 26 | ;; 27 | 1) 28 | TARGET_DEVICE_ARGS="--target-device iphone" 29 | ;; 30 | 2) 31 | TARGET_DEVICE_ARGS="--target-device ipad" 32 | ;; 33 | 3) 34 | TARGET_DEVICE_ARGS="--target-device tv" 35 | ;; 36 | 4) 37 | TARGET_DEVICE_ARGS="--target-device watch" 38 | ;; 39 | *) 40 | TARGET_DEVICE_ARGS="--target-device mac" 41 | ;; 42 | esac 43 | 44 | install_resource() 45 | { 46 | if [[ "$1" = /* ]] ; then 47 | RESOURCE_PATH="$1" 48 | else 49 | RESOURCE_PATH="${PODS_ROOT}/$1" 50 | fi 51 | if [[ ! -e "$RESOURCE_PATH" ]] ; then 52 | cat << EOM 53 | error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. 54 | EOM 55 | exit 1 56 | fi 57 | case $RESOURCE_PATH in 58 | *.storyboard) 59 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true 60 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 61 | ;; 62 | *.xib) 63 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true 64 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 65 | ;; 66 | *.framework) 67 | echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true 68 | mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 69 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true 70 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 71 | ;; 72 | *.xcdatamodel) 73 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" || true 74 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" 75 | ;; 76 | *.xcdatamodeld) 77 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" || true 78 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" 79 | ;; 80 | *.xcmappingmodel) 81 | echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" || true 82 | xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" 83 | ;; 84 | *.xcassets) 85 | ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" 86 | XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") 87 | ;; 88 | *) 89 | echo "$RESOURCE_PATH" || true 90 | echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" 91 | ;; 92 | esac 93 | } 94 | 95 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 96 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 97 | if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then 98 | mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 99 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 100 | fi 101 | rm -f "$RESOURCES_TO_COPY" 102 | 103 | if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "${XCASSET_FILES:-}" ] 104 | then 105 | # Find all other xcassets (this unfortunately includes those of path pods and other targets). 106 | OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) 107 | while read line; do 108 | if [[ $line != "${PODS_ROOT}*" ]]; then 109 | XCASSET_FILES+=("$line") 110 | fi 111 | done <<<"$OTHER_XCASSETS" 112 | 113 | if [ -z ${ASSETCATALOG_COMPILER_APPICON_NAME+x} ]; then 114 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 115 | else 116 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" --app-icon "${ASSETCATALOG_COMPILER_APPICON_NAME}" --output-partial-info-plist "${TARGET_TEMP_DIR}/assetcatalog_generated_info_cocoapods.plist" 117 | fi 118 | fi 119 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Gemini_Example/Pods-Gemini_Example-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then 7 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy 8 | # frameworks to, so exit 0 (signalling the script phase was successful). 9 | exit 0 10 | fi 11 | 12 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 13 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 14 | 15 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" 16 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 17 | 18 | # Used as a return value for each invocation of `strip_invalid_archs` function. 19 | STRIP_BINARY_RETVAL=0 20 | 21 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 22 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 23 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 24 | 25 | # Copies and strips a vendored framework 26 | install_framework() 27 | { 28 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 29 | local source="${BUILT_PRODUCTS_DIR}/$1" 30 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 31 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 32 | elif [ -r "$1" ]; then 33 | local source="$1" 34 | fi 35 | 36 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 37 | 38 | if [ -L "${source}" ]; then 39 | echo "Symlinked..." 40 | source="$(readlink "${source}")" 41 | fi 42 | 43 | # Use filter instead of exclude so missing patterns don't throw errors. 44 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 45 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 46 | 47 | local basename 48 | basename="$(basename -s .framework "$1")" 49 | binary="${destination}/${basename}.framework/${basename}" 50 | if ! [ -r "$binary" ]; then 51 | binary="${destination}/${basename}" 52 | fi 53 | 54 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 55 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 56 | strip_invalid_archs "$binary" 57 | fi 58 | 59 | # Resign the code if required by the build settings to avoid unstable apps 60 | code_sign_if_enabled "${destination}/$(basename "$1")" 61 | 62 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 63 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 64 | local swift_runtime_libs 65 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]}) 66 | for lib in $swift_runtime_libs; do 67 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 68 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 69 | code_sign_if_enabled "${destination}/${lib}" 70 | done 71 | fi 72 | } 73 | 74 | # Copies and strips a vendored dSYM 75 | install_dsym() { 76 | local source="$1" 77 | if [ -r "$source" ]; then 78 | # Copy the dSYM into a the targets temp dir. 79 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 80 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 81 | 82 | local basename 83 | basename="$(basename -s .framework.dSYM "$source")" 84 | binary="${DERIVED_FILES_DIR}/${basename}.framework.dSYM/Contents/Resources/DWARF/${basename}" 85 | 86 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 87 | if [[ "$(file "$binary")" == *"Mach-O dSYM companion"* ]]; then 88 | strip_invalid_archs "$binary" 89 | fi 90 | 91 | if [[ $STRIP_BINARY_RETVAL == 1 ]]; then 92 | # Move the stripped file into its final destination. 93 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 94 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.framework.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 95 | else 96 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 97 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.framework.dSYM" 98 | fi 99 | fi 100 | } 101 | 102 | # Signs a framework with the provided identity 103 | code_sign_if_enabled() { 104 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 105 | # Use the current code_sign_identitiy 106 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 107 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" 108 | 109 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 110 | code_sign_cmd="$code_sign_cmd &" 111 | fi 112 | echo "$code_sign_cmd" 113 | eval "$code_sign_cmd" 114 | fi 115 | } 116 | 117 | # Strip invalid architectures 118 | strip_invalid_archs() { 119 | binary="$1" 120 | # Get architectures for current target binary 121 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 122 | # Intersect them with the architectures we are building for 123 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 124 | # If there are no archs supported by this binary then warn the user 125 | if [[ -z "$intersected_archs" ]]; then 126 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 127 | STRIP_BINARY_RETVAL=0 128 | return 129 | fi 130 | stripped="" 131 | for arch in $binary_archs; do 132 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 133 | # Strip non-valid architectures in-place 134 | lipo -remove "$arch" -output "$binary" "$binary" || exit 1 135 | stripped="$stripped $arch" 136 | fi 137 | done 138 | if [[ "$stripped" ]]; then 139 | echo "Stripped $binary of architectures:$stripped" 140 | fi 141 | STRIP_BINARY_RETVAL=1 142 | } 143 | 144 | 145 | if [[ "$CONFIGURATION" == "Debug" ]]; then 146 | install_framework "${BUILT_PRODUCTS_DIR}/Gemini/Gemini.framework" 147 | fi 148 | if [[ "$CONFIGURATION" == "Release" ]]; then 149 | install_framework "${BUILT_PRODUCTS_DIR}/Gemini/Gemini.framework" 150 | fi 151 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 152 | wait 153 | fi 154 | -------------------------------------------------------------------------------- /Gemini/EasingFunction.swift: -------------------------------------------------------------------------------- 1 | private typealias EasingParameter = (_ t: CGFloat, _ b: CGFloat, _ c: CGFloat, _ d: CGFloat) -> CGFloat 2 | 3 | private struct EasingFunction { 4 | static let linear: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 5 | return c * t / d + b 6 | } 7 | 8 | static let easeInQuad: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 9 | let t = t / d 10 | return c * t * t + b 11 | } 12 | 13 | static let easeOutQuad: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 14 | let t = t / d 15 | return -c * t * (t - 2) + b 16 | } 17 | 18 | static let easeInOutQuad: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 19 | var t = t / (d / 2) 20 | if t < 1 { 21 | return c / 2 * t * t + b 22 | } 23 | t -= 1 24 | return -c / 2 * (t * (t - 2) - 1) + b 25 | } 26 | 27 | static let easeInCubic: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 28 | let t = t / d 29 | return c * t * t * t + b 30 | } 31 | 32 | static let easeOutCubic: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 33 | var t = t / d 34 | t -= 1 35 | return c * (t * t * t + 1) + b 36 | } 37 | 38 | static let easeInOutCubic: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 39 | var t = t / (d / 2) 40 | if t < 1 { 41 | return c / 2 * t * t * t + b 42 | } 43 | t -= 2 44 | return c / 2 * (t * t * t + 2) + b 45 | } 46 | 47 | static let easeInQuart: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 48 | let t = t / d 49 | return c * t * t * t * t + b 50 | } 51 | 52 | static let easeOutQuart: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 53 | var t = t / d 54 | t -= 1 55 | return -c * ( t * t * t * t - 1) + b 56 | } 57 | 58 | static let easeInOutQuart: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 59 | var t = t / (d / 2) 60 | if t < 1 { 61 | return c / 2 * t * t * t * t + b 62 | } 63 | t -= 2 64 | return -c / 2 * (t * t * t * t - 2) + b 65 | } 66 | 67 | static let easeInQuint: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 68 | let t = t / d 69 | return c * t * t * t * t * t + b 70 | } 71 | 72 | static let easeOutQuint: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 73 | var t = t / d 74 | t -= 1 75 | return c * (t * t * t * t * t + 1) + b 76 | } 77 | 78 | static let easeInOutQuint: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 79 | var t = t / (d / 2) 80 | if t < 1 { 81 | return c / 2 * t * t * t * t * t + b 82 | } 83 | t -= 2 84 | return c / 2 * (t * t * t * t * t + 2) + b 85 | } 86 | 87 | static let easeInSine: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 88 | return -c * cos(t / d * (CGFloat.pi / 2)) + c + b 89 | } 90 | 91 | static let easeOutSine: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 92 | return c * sin(t / d * (CGFloat.pi / 2)) + b 93 | } 94 | 95 | static let easeInOutSine: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 96 | return -c / 2 * (cos(CGFloat.pi * t / d) - 1) + b 97 | } 98 | 99 | static let easeInExpo: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 100 | return c * pow(2, 10 * (t / d - 1) ) + b 101 | } 102 | 103 | static let easeOutExpo: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 104 | return c * (-pow(2, -10 * t / d) + 1) + b 105 | } 106 | 107 | static let easeInOutExpo: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 108 | var t = t / (d / 2) 109 | if t < 1 { 110 | return c / 2 * pow(2, 10 * (t - 1)) + b 111 | } 112 | t -= 1 113 | return c/2 * (-pow(2, -10 * t) + 2) + b 114 | } 115 | 116 | static let easeInCirc: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 117 | let t = t / d 118 | return -c * (sqrt(1 - t * t) - 1) + b 119 | } 120 | 121 | static let easeOutCirc: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 122 | var t = t / d 123 | t -= 1 124 | return c * sqrt(1 - t * t) + b 125 | } 126 | 127 | static let easeInOutCirc: EasingParameter = { (t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) in 128 | var t = t / (d / 2) 129 | if t < 1 { 130 | return -c / 2 * (sqrt(1 - t * t) - 1) + b 131 | } 132 | t -= 2 133 | return c / 2 * (sqrt(1 - t * t) + 1) + b 134 | } 135 | } 136 | 137 | public enum GeminiEasing { 138 | case linear 139 | case easeInQuad 140 | case easeOutQuad 141 | case easeInOutQuad 142 | case easeInCubic 143 | case easeOutCubic 144 | case easeInOutCubic 145 | case easeInQuart 146 | case easeOutQuart 147 | case easeInOutQuart 148 | case easeInQuint 149 | case easeOutQuint 150 | case easeInOutQuint 151 | case easeInSine 152 | case easeOutSine 153 | case easeInOutSine 154 | case easeInExpo 155 | case easeOutExpo 156 | case easeInOutExpo 157 | case easeInCirc 158 | case easeOutCirc 159 | case easeInOutCirc 160 | } 161 | 162 | extension GeminiEasing { 163 | func value(withRatio ratio: CGFloat) -> CGFloat { 164 | let isNegative = ratio < 0 165 | let result = easingFunction(isNegative ? -ratio : ratio, 0, 1, 1) 166 | return isNegative ? -result : result 167 | } 168 | 169 | private var easingFunction: EasingParameter { 170 | typealias F = EasingFunction 171 | 172 | switch self { 173 | case .linear: 174 | return F.linear 175 | case .easeInQuad: 176 | return F.easeInQuad 177 | case .easeOutQuad: 178 | return F.easeOutQuad 179 | case .easeInOutQuad: 180 | return F.easeInOutQuad 181 | case .easeInCubic: 182 | return F.easeInCubic 183 | case .easeOutCubic: 184 | return F.easeOutCubic 185 | case .easeInOutCubic: 186 | return F.easeInOutCubic 187 | case .easeInQuart: 188 | return F.easeInQuart 189 | case .easeOutQuart: 190 | return F.easeOutQuart 191 | case .easeInOutQuart: 192 | return F.easeInOutQuart 193 | case .easeInQuint: 194 | return F.easeInQuint 195 | case .easeOutQuint: 196 | return F.easeOutQuint 197 | case .easeInOutQuint: 198 | return F.easeInOutQuint 199 | case .easeInSine: 200 | return F.easeInSine 201 | case .easeOutSine: 202 | return F.easeOutSine 203 | case .easeInOutSine: 204 | return F.easeInOutSine 205 | case .easeInExpo: 206 | return F.easeInExpo 207 | case .easeOutExpo: 208 | return F.easeOutExpo 209 | case .easeInOutExpo: 210 | return F.easeInOutExpo 211 | case .easeInCirc: 212 | return F.easeInCirc 213 | case .easeOutCirc: 214 | return F.easeOutCirc 215 | case .easeInOutCirc: 216 | return F.easeInOutCirc 217 | } 218 | } 219 | } 220 | 221 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Overview 6 | 7 | 8 | 9 | ## What is the `Gemini`? 10 | 11 | `Gemini` is rich scroll based animation framework for iOS, written in Swift. You can easily use `GeminiCollectionView`, which is a subclass of `UICollectionView`. 12 | 13 | It enables you to make multiple animation which has various and customizable properties, and moreover can create your own custom scroll animation. 14 | 15 | `Gemini` also provides a fluent interface based on method chaining. you can use this intuitively and simply. 16 | 17 | 18 | ```swift 19 | collectionView.gemini 20 | .circleRotationAnimation() 21 | .radius(400) 22 | .rotateDirection(.clockwise) 23 | ``` 24 | 25 | # Features 26 | 27 | ![Platform](http://img.shields.io/badge/platform-ios-blue.svg?style=flat 28 | ) 29 | [![Cocoapods](https://img.shields.io/badge/Cocoapods-compatible-brightgreen.svg)](https://img.shields.io/badge/Cocoapods-compatible-brightgreen.svg) 30 | [![Carthage compatible](https://img.shields.io/badge/Carthage-Compatible-brightgreen.svg?style=flat)](https://github.com/Carthage/Carthage) 31 | [![License](http://img.shields.io/badge/license-MIT-lightgrey.svg?style=flat 32 | )](http://mit-license.org) 33 | ![Swift](https://img.shields.io/badge/swift-5.0-orange.svg) 34 | ![pod](https://img.shields.io/badge/pod-v1.4.0-red.svg) 35 | 36 | 37 | 38 | - [x] Rich animation with scrolling 39 | - [x] Easily usable 40 | - [x] Highly customizable 41 | - [x] Several types of animations and properties 42 | - [x] Supports vertical and horizontal flow layout 43 | - [x] Supports easing function 44 | - [x] Supports `Swift5.0` 45 | - [x] Fluent interfaces based on method chaining 46 | - [x] Compatible with `Carthage` 47 | - [x] Compatible with `CocoaPods` 48 | - [x] Example project with lots of stock animations 49 | - [x] And More... 50 | 51 | # Contents 52 | - [Animation Types and properties](#animation-types) 53 | - [Usage](#usage) 54 | - [Requirements](#requirements) 55 | - [Installation](#installation) 56 | - [Author](#author) 57 | 58 | # Animation Types and properties 59 | 60 | The following animation types are available. See sample code [here](https://github.com/shoheiyokoyama/Gemini/tree/master/Example/Gemini) for details. 61 | 62 | - [Cube](#cube) 63 | - [Circle Rotation](#circle-rotation) You can configure direction of rotation using the `CircleRotationDirection` 64 | - [3D vector rotation](#3d-vector-rotation) Each rotation types provide multiple rotation effect 65 | - [Roll Rotation](#roll-rotation) 66 | - [Pitch Rotation](#pitch-rotation) 67 | - [Yaw Rotation](#yaw-rotation) 68 | - [Scale](#scale) 69 | - [Custom](#custom) You can create your own custom scroll animation using multiple properties, rotation, scale, translation, etc. 70 | 71 | In addition, you can also customize the following properties for the above animation types. 72 | 73 | - BackgroundColor 74 | - CornerRadius 75 | - Alpha 76 | - [Easings](#easing-function) 77 | - [Shadow Effect](#shadow-effect) 78 | 79 | ## Cube 80 | 81 |

82 | 83 | 84 |

85 | 86 | It's a cube animation like Instagram. 87 | If you would like to customize the cube animation, change `cubeDegree`. 88 | If `cubeDegree` is 90, it moves like a regular hexahedron. 89 | 90 | ```swift 91 | collectionView.gemini 92 | .cubeAnimation() 93 | .cubeDegree(90) 94 | ``` 95 | 96 | ##
CircleRotation 97 | 98 |

99 | 100 | 101 |

102 | 103 | An animation moves in a circle. You can change `circleRadius` and `CircleRotationDirection`. 104 | 105 | ```swift 106 | collectionView.gemini 107 | .circleRotationAnimation() 108 | .radius(450) // The radius of the circle 109 | .rotateDirection(.clockwise) // Direction of rotation. 110 | .itemRotationEnabled(true) // Whether the item rotates or not. 111 | ``` 112 | 113 | ##
3D vector rotation 114 | 115 | Available for `Roll`, `Pitch` and `Yaw` animation. These rotation animation are designed based on 3-Dimensional vector. Figure-1 shows direction of rotation based on device. 116 | 117 | ###### ***Figure-1*** Pitch, roll, and yaw axes ###### 118 |

119 | 120 |

121 | 122 | ###### Reference: [Event Handling Guide for UIKit Apps](https://developer.apple.com/library/content/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/HandlingProcessedDeviceMotionData.html#//apple_ref/doc/uid/TP40009541-CH27-SW1) 123 | 124 | ###
Roll Rotation 125 | 126 |

127 | 128 | 129 |

130 | 131 | ###
Pitch Rotation 132 | 133 |

134 | 135 | 136 |

137 | 138 | ###
Yaw Rotation 139 | 140 |

141 | 142 | 143 |

144 | 145 | 146 | Each types of rotation animation has `RotationEffect`(e.g. `RollRotationEffect`) and degree of rotation. 147 | 148 | Customize `RotationEffect` (`up`, `down`, `sineWave`, `reverseSineWave`) and degree of rotation. 149 | 150 | In the case of `rollRotation`, like this: 151 | ```swift 152 | collectionView.gemini 153 | .rollRotationAnimation() 154 | .degree(45) 155 | .rollEffect(.rollUp) 156 | ``` 157 | 158 | ##
Scale 159 | 160 |

161 | 162 | 163 |

164 | 165 | The `scaleUp` gradually increases frame size, `scaleDown` decreases. 166 | 167 | ```swift 168 | collectionView.gemini 169 | .scaleAnimation() 170 | .scale(0.75) 171 |    .scaleEffect(.scaleUp) // or .scaleDown 172 | ``` 173 | 174 | ##
Custom 175 | 176 |

177 | 178 | 179 |

180 | 181 | You can flexibly and easily customize scroll animation. Customize properties of `GeminiAnimation.custom` such as `scale`, `scaleEffect`, `rotationAngle`, `translation`, `easing`, `shadowEffect`, `alpha`, `cornerRadius`, `backgroundColor`, `anchorPoint`, etc. 182 | 183 | The animation of gif is customized in the following way: 184 | 185 | ```swift 186 | collectionView.gemini 187 | .customAnimation() 188 | .translation(y: 50) 189 | .rotationAngle(y: 13) 190 | .ease(.easeOutExpo) 191 | .shadowEffect(.fadeIn) 192 | .maxShadowAlpha(0.3) 193 | ``` 194 | 195 | Or right side of gifs is customized as follows: 196 | 197 | ```swift 198 | collectionView.gemini 199 | .customAnimation() 200 | .backgroundColor(startColor: lightGreenColor, endColor: lightBlueColor) 201 | .ease(.easeOutSine) 202 | .cornerRadius(75) 203 | ``` 204 | 205 | There are more sample code at [CustomAnimationViewController.swift](https://github.com/shoheiyokoyama/Gemini/blob/master/Example/Gemini/ViewControllers/CustomAnimationViewController.swift). 206 | 207 | ##
Easing function 208 | `Gemini` supports various easing functions based on distance of scroll. 209 | 210 | - linear 211 | - easeInQuad 212 | - easeOutQuad 213 | - easeInOutQuad 214 | - easeInCubic 215 | - easeOutCubic 216 | - easeInOutCubic 217 | - easeInQuart 218 | - easeOutQuart 219 | - easeInOutQuart 220 | - easeInQuint 221 | - easeOutQuint 222 | - easeInOutQuint 223 | - easeInSine 224 | - easeOutSine 225 | - easeInOutSine 226 | - easeInExpo 227 | - easeOutExpo 228 | - easeInOutExpo 229 | - easeInCirc 230 | - easeOutCirc 231 | - easeInOutCirc 232 | 233 | ## Shadow effect 234 | Default value is `ShadowEffect.none`. Return `shadowView` in your custom class, which is a subclass of `GeminiCell`. 235 | 236 | - fadeIn 237 | - nextFadeIn 238 | - previousFadeIn 239 | - fadeOut 240 | - none 241 | 242 | ```swift 243 | class CustomCollectionViewCell: GeminiCell { 244 | @IBOutlet weak var customShadowView: UIView! 245 | override var shadowView: UIView? { 246 | return customShadowView 247 | } 248 | } 249 | ``` 250 | 251 | # Usage 252 | 253 | 1. ***Use Gemini classes*** 254 | 255 | `Gemini` is designed to be easy to use. Use `GeminiCollectionView` and `GeminiCell`. These classes is subclass of `UICollectionView`, `UICollectionViewCell`. 256 | 257 | 2. ***Configure animation*** 258 | 259 | Configure animation with fluent interface based on method chaining. You can develop expressive code that enhances readability. 260 | 261 | 3. ***Call function for animation*** 262 | 263 | Finally, call `animateVisibleCells()` in `scrollViewDidScroll(_:)` 264 | 265 | > NOTE: If you want to adapt animation immediately after view is displayed, call `animateCell(_:)` in `collectionView(_:cellForItemAt:)` and `collectionView(_:willDisplay:forItemAt:)`. 266 | 267 | 268 | ```swift 269 | // Import Gemini 270 | import Gemini 271 | 272 | // Inherite GeminiCell 273 | class CustomCell: GeminiCell { 274 | ... 275 | } 276 | 277 | // Conform to UICollectionViewDelegate and UICollectionViewDataSource 278 | class CustomViewController: UIViewController: UICollectionViewDelegate, UICollectionViewDataSource { 279 | 280 | // Inherite GeminiCollectionView 281 | @IBOutlet weak var collectionView: GeminiCollectionView! 282 | 283 | ... 284 | 285 | // Configure animation and properties 286 | func configureAnimation() { 287 | collectionView.gemini 288 | .circleRotationAnimation() 289 | .radius(400) 290 | .rotateDirection(.clockwise) 291 | } 292 | 293 | // Call animation function 294 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 295 | collectionView.animateVisibleCells() 296 | } 297 | 298 | func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 299 | if let cell = cell as? GeminiCell { 300 | self.collectionView.animateCell(cell) 301 | } 302 | } 303 | 304 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 305 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell 306 | self.collectionView.animateCell(cell) 307 | return cell 308 | } 309 | ``` 310 | 311 | See [Example](https://github.com/shoheiyokoyama/Gemini/tree/master/Example/Gemini), for more details. 312 | 313 | To run the example project, clone the repo, and run `pod install` from the Example directory first. 314 | 315 | ## Requirements 316 | 317 | - Xcode 10.2.1 318 | - Swift 5.0 319 | 320 | ## Installation 321 | 322 | ### CocoaPods 323 | 324 | Gemini is available through [CocoaPods](http://cocoapods.org). To install 325 | it, simply add the following line to your Podfile: 326 | 327 | ```ruby 328 | pod "Gemini" 329 | ``` 330 | 331 | ### Carthage 332 | 333 | Add the following line to your `Cartfile`: 334 | 335 | ```ruby 336 | github "shoheiyokoyama/Gemini" 337 | ``` 338 | 339 | ## Author 340 | 341 | Shohei Yokoyama 342 | 343 | - [GitHub](https://github.com/shoheiyokoyama) 344 | - [Facebook](https://www.facebook.com/shohei.yokoyama.96) 345 | - [Twitter](https://twitter.com/shoheiyokoyam) 346 | - Gmail: shohei.yok0602@gmail.com 347 | 348 | ## License 349 | 350 | Gemini is available under the MIT license. See the [LICENSE file](https://github.com/shoheiyokoyama/Gemini/blob/master/LICENSE) for more info. 351 | -------------------------------------------------------------------------------- /Example/Gemini/ViewControllers/AnimationListViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class AnimationListViewController: UIViewController { 4 | private let cellIdentifier = "tableViewCell" 5 | private let sectionTitles = ["Cube", 6 | "CircleRotation", 7 | "RollRotation", 8 | "PitchRotation", 9 | "YawRotation", 10 | "ScaleAnimation", 11 | "Custom"] 12 | 13 | private let cellTitles: [[String]] = [ 14 | // Cube 15 | ["Horizontal cube", 16 | "Vertical cube"], 17 | // Circle rotations 18 | ["Horizontal clockwise rotation", 19 | "Horizontal anticlockwise rotation", 20 | "Vertical clockwise rotation", 21 | "Vertical anticlockwise rotation"], 22 | // Roll rotation 23 | ["Horizontal roll up", 24 | "Horizontal roll down", 25 | "Horizontal sine wave", 26 | "Horizontal reverse sine wave", 27 | "Vertical roll up", 28 | "Vertical roll down", 29 | "Vertical sine wave", 30 | "Vertical reverse sine wave"], 31 | // Pitch rotation 32 | ["Horizontal pitch up", 33 | "Horizontal pitch down", 34 | "Horizontal sine wave", 35 | "Horizontal reverse sine wave", 36 | "Vertical pitch up", 37 | "Vertical pitch down", 38 | "Vertical sine wave", 39 | "Vertical reverse sine wave"], 40 | // Yaw rotation 41 | ["Horizontal yaw up", 42 | "Horizontal yaw down", 43 | "Horizontal sine wave", 44 | "Horizontal reverse sine wave", 45 | "Vertical yaw up", 46 | "Vertical yaw down", 47 | "Vertical sine wave", 48 | "Vertical reverse sine wave"], 49 | // Scale 50 | ["Horizontal scale up", 51 | "Horizontal scale down", 52 | "Vertical scale up", 53 | "Vertical scale down"], 54 | // Custom 55 | ["Custom animation1", 56 | "Custom animation2"] 57 | ] 58 | 59 | @IBOutlet private weak var tableView: UITableView! { 60 | didSet { 61 | tableView.delegate = self 62 | tableView.dataSource = self 63 | } 64 | } 65 | 66 | override func viewWillAppear(_ animated: Bool) { 67 | super.viewWillAppear(animated) 68 | navigationController?.isNavigationBarHidden = true 69 | } 70 | } 71 | 72 | // MARK: - UITableViewDelegate 73 | 74 | extension AnimationListViewController: UITableViewDelegate { 75 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 76 | switch (indexPath.section, indexPath.row) { 77 | // Cube Animation 78 | case (0, _): 79 | let direction: UICollectionView.ScrollDirection = indexPath.row == 0 ? .horizontal : .vertical 80 | let viewController = CubeViewController.make(scrollDirection: direction) 81 | navigationController?.pushViewController(viewController, animated: true) 82 | 83 | // Circle Rotation Animation 84 | case (1, 0): 85 | let viewController = CircleRotationViewController.make(scrollDirection: .horizontal, rotateDirection: .clockwise) 86 | navigationController?.pushViewController(viewController, animated: true) 87 | case (1, 1): 88 | let viewController = CircleRotationViewController.make(scrollDirection: .horizontal, rotateDirection: .anticlockwise) 89 | navigationController?.pushViewController(viewController, animated: true) 90 | case (1, 2): 91 | let viewController = CircleRotationViewController.make(scrollDirection: .vertical, rotateDirection: .clockwise) 92 | navigationController?.pushViewController(viewController, animated: true) 93 | case (1, 3): 94 | let viewController = CircleRotationViewController.make(scrollDirection: .vertical, rotateDirection: .anticlockwise) 95 | navigationController?.pushViewController(viewController, animated: true) 96 | 97 | // Roll Rotation Animation 98 | case (2, 0): 99 | let viewController = RollRotationViewController.make(scrollDirection: .horizontal, effect: .rollUp) 100 | navigationController?.pushViewController(viewController, animated: true) 101 | case (2, 1): 102 | let viewController = RollRotationViewController.make(scrollDirection: .horizontal, effect: .rollDown) 103 | navigationController?.pushViewController(viewController, animated: true) 104 | case (2, 2): 105 | let viewController = RollRotationViewController.make(scrollDirection: .horizontal, effect: .sineWave) 106 | navigationController?.pushViewController(viewController, animated: true) 107 | case (2, 3): 108 | let viewController = RollRotationViewController.make(scrollDirection: .horizontal, effect: .reverseSineWave) 109 | navigationController?.pushViewController(viewController, animated: true) 110 | case (2, 4): 111 | let viewController = RollRotationViewController.make(scrollDirection: .vertical, effect: .rollUp) 112 | navigationController?.pushViewController(viewController, animated: true) 113 | case (2, 5): 114 | let viewController = RollRotationViewController.make(scrollDirection: .vertical, effect: .rollDown) 115 | navigationController?.pushViewController(viewController, animated: true) 116 | case (2, 6): 117 | let viewController = RollRotationViewController.make(scrollDirection: .vertical, effect: .sineWave) 118 | navigationController?.pushViewController(viewController, animated: true) 119 | case (2, 7): 120 | let viewController = RollRotationViewController.make(scrollDirection: .vertical, effect: .reverseSineWave) 121 | navigationController?.pushViewController(viewController, animated: true) 122 | 123 | // Pitch Rotation Animation 124 | case (3, 0): 125 | let viewController = PitchRotationViewController.make(scrollDirection: .horizontal, effect: .pitchUp) 126 | navigationController?.pushViewController(viewController, animated: true) 127 | case (3, 1): 128 | let viewController = PitchRotationViewController.make(scrollDirection: .horizontal, effect: .pitchDown) 129 | navigationController?.pushViewController(viewController, animated: true) 130 | case (3, 2): 131 | let viewController = PitchRotationViewController.make(scrollDirection: .horizontal, effect: .sineWave) 132 | navigationController?.pushViewController(viewController, animated: true) 133 | case (3, 3): 134 | let viewController = PitchRotationViewController.make(scrollDirection: .horizontal, effect: .reverseSineWave) 135 | navigationController?.pushViewController(viewController, animated: true) 136 | case (3, 4): 137 | let viewController = PitchRotationViewController.make(scrollDirection: .vertical, effect: .pitchUp) 138 | navigationController?.pushViewController(viewController, animated: true) 139 | case (3, 5): 140 | let viewController = PitchRotationViewController.make(scrollDirection: .vertical, effect: .pitchDown) 141 | navigationController?.pushViewController(viewController, animated: true) 142 | case (3, 6): 143 | let viewController = PitchRotationViewController.make(scrollDirection: .vertical, effect: .sineWave) 144 | navigationController?.pushViewController(viewController, animated: true) 145 | case (3, 7): 146 | let viewController = PitchRotationViewController.make(scrollDirection: .vertical, effect: .reverseSineWave) 147 | navigationController?.pushViewController(viewController, animated: true) 148 | 149 | // Yaw Rotation Animation 150 | case (4, 0): 151 | let viewController = YawRotationViewController.make(scrollDirection: .horizontal, effect: .yawUp) 152 | navigationController?.pushViewController(viewController, animated: true) 153 | case (4, 1): 154 | let viewController = YawRotationViewController.make(scrollDirection: .horizontal, effect: .yawDown) 155 | navigationController?.pushViewController(viewController, animated: true) 156 | case (4, 2): 157 | let viewController = YawRotationViewController.make(scrollDirection: .horizontal, effect: .sineWave) 158 | navigationController?.pushViewController(viewController, animated: true) 159 | case (4, 3): 160 | let viewController = YawRotationViewController.make(scrollDirection: .horizontal, effect: .reverseSineWave) 161 | navigationController?.pushViewController(viewController, animated: true) 162 | case (4, 4): 163 | let viewController = YawRotationViewController.make(scrollDirection: .vertical, effect: .yawUp) 164 | navigationController?.pushViewController(viewController, animated: true) 165 | case (4, 5): 166 | let viewController = YawRotationViewController.make(scrollDirection: .vertical, effect: .yawDown) 167 | navigationController?.pushViewController(viewController, animated: true) 168 | case (4, 6): 169 | let viewController = YawRotationViewController.make(scrollDirection: .vertical, effect: .sineWave) 170 | navigationController?.pushViewController(viewController, animated: true) 171 | case (4, 7): 172 | let viewController = YawRotationViewController.make(scrollDirection: .vertical, effect: .reverseSineWave) 173 | navigationController?.pushViewController(viewController, animated: true) 174 | 175 | // Scale Rotation Animation 176 | case (5, 0): 177 | let viewController = ScaleAnimationViewController.make(scrollDirection: .horizontal, scaleEffect: .scaleUp) 178 | navigationController?.pushViewController(viewController, animated: true) 179 | case (5, 1): 180 | let viewController = ScaleAnimationViewController.make(scrollDirection: .horizontal, scaleEffect: .scaleDown) 181 | navigationController?.pushViewController(viewController, animated: true) 182 | case (5, 2): 183 | let viewController = ScaleAnimationViewController.make(scrollDirection: .vertical, scaleEffect: .scaleUp) 184 | navigationController?.pushViewController(viewController, animated: true) 185 | case (5, 3): 186 | let viewController = ScaleAnimationViewController.make(scrollDirection: .vertical, scaleEffect: .scaleDown) 187 | navigationController?.pushViewController(viewController, animated: true) 188 | 189 | // Custom Animation 190 | case (6, 0): 191 | let viewController = CustomAnimationViewController.make(animationType: .custom1) 192 | navigationController?.pushViewController(viewController, animated: true) 193 | 194 | case (6, 1): 195 | let viewController = CustomAnimationViewController.make(animationType: .custom2) 196 | navigationController?.pushViewController(viewController, animated: true) 197 | 198 | default: 199 | () 200 | } 201 | } 202 | 203 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 204 | return 50 205 | } 206 | 207 | func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 208 | let headerView = UIView() 209 | headerView.backgroundColor = UIColor(red: 65 / 255, green: 106 / 255, blue: 166 / 255, alpha: 0.9) 210 | let titleLabel = UILabel(frame: CGRect(origin: CGPoint(x: 15, y: 20), size: .zero)) 211 | titleLabel.font = UIFont.boldSystemFont(ofSize: 19) 212 | titleLabel.text = sectionTitles[section] 213 | titleLabel.textColor = .white 214 | titleLabel.sizeToFit() 215 | headerView.addSubview(titleLabel) 216 | return headerView 217 | } 218 | 219 | func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 220 | return 60 221 | } 222 | } 223 | 224 | // MARK: - UITableViewDataSource 225 | 226 | extension AnimationListViewController: UITableViewDataSource { 227 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 228 | let cell: UITableViewCell 229 | if let dequeuedCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) { 230 | cell = dequeuedCell 231 | } else { 232 | cell = UITableViewCell(style: .default, reuseIdentifier: cellIdentifier) 233 | } 234 | cell.selectionStyle = .none 235 | cell.textLabel?.text = cellTitles[indexPath.section][indexPath.row] 236 | return cell 237 | } 238 | 239 | func numberOfSections(in tableView: UITableView) -> Int { 240 | return sectionTitles.count 241 | } 242 | 243 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 244 | return cellTitles[section].count 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /Gemini/GeminiAnimationModel.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | enum GeminiAnimation { 4 | case cube 5 | case circleRotation 6 | case rollRotation 7 | case pitchRotation 8 | case yawRotation 9 | case custom 10 | case scale 11 | case none 12 | } 13 | 14 | public protocol Gemini { 15 | /// `isEnabled` is false if `animation` is GeminiAnimation.none 16 | var isEnabled: Bool { get } 17 | 18 | // GeminiAnimation 19 | 20 | @discardableResult func cubeAnimation() -> CubeAnimatable 21 | @discardableResult func customAnimation() -> CustomAnimatable 22 | @discardableResult func circleRotationAnimation() -> CircleRotationAnimatable 23 | @discardableResult func rollRotationAnimation() -> RollRotationAnimatable 24 | @discardableResult func pitchRotationAnimation() -> PitchRotationAnimatable 25 | @discardableResult func yawRotationAnimation() -> YawRotationAnimatable 26 | @discardableResult func scaleAnimation() -> ScaleAnimatable 27 | } 28 | 29 | extension GeminiAnimationModel: Gemini { 30 | public var isEnabled: Bool { 31 | return animation != .none 32 | } 33 | 34 | @discardableResult 35 | public func cubeAnimation() -> CubeAnimatable { 36 | animation = .cube 37 | return self 38 | } 39 | 40 | @discardableResult 41 | public func customAnimation() -> CustomAnimatable { 42 | animation = .custom 43 | return self 44 | } 45 | 46 | @discardableResult 47 | public func circleRotationAnimation() -> CircleRotationAnimatable { 48 | animation = .circleRotation 49 | return self 50 | } 51 | 52 | @discardableResult 53 | public func rollRotationAnimation() -> RollRotationAnimatable { 54 | animation = .rollRotation 55 | return self 56 | } 57 | 58 | @discardableResult 59 | public func pitchRotationAnimation() -> PitchRotationAnimatable { 60 | animation = .pitchRotation 61 | return self 62 | } 63 | 64 | @discardableResult 65 | public func yawRotationAnimation() -> YawRotationAnimatable { 66 | animation = .yawRotation 67 | return self 68 | } 69 | 70 | @discardableResult 71 | public func scaleAnimation() -> ScaleAnimatable { 72 | animation = .scale 73 | return self 74 | } 75 | } 76 | 77 | final class GeminiAnimationModel { 78 | // Animation types 79 | 80 | var animation: GeminiAnimation = .none 81 | 82 | // EasingAnimatable 83 | 84 | var easing: GeminiEasing = .linear 85 | 86 | // Cube animation properties 87 | 88 | var cubeDegree: CGFloat = 90 89 | 90 | // CircleRotate animation properties 91 | 92 | var circleRadius: CGFloat = 100 93 | var rotateDirection: CircleRotationDirection = .clockwise 94 | var isItemRotationEnabled: Bool = true 95 | 96 | // Scale animation properties 97 | 98 | var scale: CGFloat = 1 99 | var scaleEffect: GeminScaleEffect = .scaleUp 100 | 101 | // Roll rotation animation properties 102 | 103 | var rollDegree: CGFloat = 90 104 | var rollEffect: RollRotationEffect = .rollUp 105 | 106 | // Pitch rotation animation properties 107 | 108 | var pitchDegree: CGFloat = 90 109 | var pitchEffect: PitchRotationEffect = .pitchUp 110 | 111 | // Yaw rotation animation properties 112 | 113 | var yawDegree: CGFloat = 90 114 | var yawEffect: YawRotationEffect = .yawUp 115 | 116 | // CustomAnimatable properties 117 | 118 | lazy var scaleCoordinate = Coordinate() 119 | lazy var rotationCoordinate = Coordinate() 120 | lazy var translationCoordinate = Coordinate() 121 | var anchorPoint = CGPoint(x: 0.5, y: 0.5) 122 | 123 | // UIAppearanceAnimatable properties 124 | 125 | var alpha: CGFloat? 126 | var cornerRadius: CGFloat? 127 | var startBackgroundColor: UIColor? 128 | var endBackgroundColor: UIColor? 129 | var maxShadowAlpha: CGFloat = 1 130 | var minShadowAlpha: CGFloat = 0 131 | var shadowEffect: ShadowEffect = .none 132 | 133 | var scrollDirection: UICollectionView.ScrollDirection = .vertical 134 | 135 | fileprivate lazy var transform3DIdentity: CATransform3D = { 136 | var identity = CATransform3DIdentity 137 | identity.m34 = 1 / 1000 138 | return identity 139 | }() 140 | 141 | /// For radian calculation in `GeminiAnimation.circleRotation`. 142 | var needsCheckDistance: Bool { 143 | return animation == .circleRotation 144 | } 145 | 146 | func shadowAlpha(withDistanceRatio ratio: CGFloat) -> CGFloat { 147 | switch shadowEffect { 148 | case .fadeIn: 149 | return minShadowAlpha + abs(ratio) * maxShadowAlpha 150 | case .nextFadeIn: 151 | return ratio > 0 ? ratio * maxShadowAlpha : 0 152 | case .previousFadeIn: 153 | return ratio < 0 ? -ratio * maxShadowAlpha : 0 154 | case .fadeOut: 155 | return (1 - abs(ratio)) * maxShadowAlpha + minShadowAlpha 156 | case .none: 157 | return 0 158 | } 159 | } 160 | 161 | func alpha(withDistanceRatio ratio: CGFloat) -> CGFloat? { 162 | guard let alpha = alpha else { return nil } 163 | return (alpha - 1) * abs(ratio) + 1 164 | } 165 | 166 | func cornerRadius(withDistanceRatio ratio: CGFloat) -> CGFloat? { 167 | guard let cornerRadius = cornerRadius else { return nil } 168 | return cornerRadius * abs(ratio) 169 | } 170 | 171 | func backgroundColor(withDistanceRatio ratio: CGFloat) -> UIColor? { 172 | let startColorComponents = startBackgroundColor?.cgColor.components ?? [] 173 | let endColorComponents = endBackgroundColor?.cgColor.components ?? [] 174 | 175 | guard startColorComponents.count >= 3 && endColorComponents.count >= 3 else { 176 | return nil 177 | } 178 | 179 | let components = (0...3).map { index -> CGFloat in 180 | (endColorComponents[index] - startColorComponents[index]) * abs(ratio) + startColorComponents[index] 181 | } 182 | return UIColor(red: components[0], green: components[1], blue: components[2], alpha: components[3]) 183 | } 184 | 185 | func transform(withParentFrame parentFrame: CGRect, cellFrame: CGRect) -> CATransform3D { 186 | let _ratio = distanceRatio(withParentFrame: parentFrame, cellFrame: cellFrame) 187 | let ratio = _ratio < 0 ? max(-1, _ratio) : min(1, _ratio) 188 | let easingRatio = easing.value(withRatio: ratio) 189 | 190 | switch animation { 191 | case .cube: 192 | let toDegree: CGFloat = max(0, min(90, cubeDegree)) 193 | let degree: CGFloat 194 | switch scrollDirection { 195 | case .vertical: 196 | degree = easingRatio * toDegree 197 | return CATransform3DRotate(transform3DIdentity, degree * .pi / 180, 1, 0, 0) 198 | case .horizontal: 199 | degree = easingRatio * -toDegree 200 | return CATransform3DRotate(transform3DIdentity, degree * .pi / 180, 0, 1, 0) 201 | } 202 | 203 | case .circleRotation: 204 | let distance = distanceFromCenter(withParentFrame: parentFrame, cellFrame: cellFrame) 205 | let middle = scrollDirection == .vertical ? parentFrame.midY : parentFrame.midX 206 | let maxCircleRadius = scrollDirection == .vertical ? middle + cellFrame.height / 2 : middle + cellFrame.width / 2 207 | let radius: CGFloat = max(maxCircleRadius, circleRadius) 208 | let _radian = asin(distance / radius) 209 | let radian = rotateDirection == .clockwise ? -_radian : _radian 210 | 211 | let rotateTransform, translateTransform: CATransform3D 212 | switch scrollDirection { 213 | case .vertical: 214 | let _x = radius * (1 - cos(_radian)) 215 | let x = rotateDirection == .clockwise ? _x : -_x 216 | rotateTransform = CATransform3DRotate(transform3DIdentity, radian, 0, 0, 1) 217 | translateTransform = CATransform3DTranslate(transform3DIdentity, x, 0, 0) 218 | case .horizontal: 219 | let _y = radius * (1 - cos(_radian)) 220 | let y = rotateDirection == .clockwise ? -_y : _y 221 | rotateTransform = CATransform3DRotate(transform3DIdentity, radian, 0, 0, 1) 222 | translateTransform = CATransform3DTranslate(transform3DIdentity, 0, y, 0) 223 | } 224 | 225 | let scale = self.calculatedScale(withRatio: easingRatio) 226 | let scaleTransform = CATransform3DScale(transform3DIdentity, scale, scale, 1) 227 | let circleTransform = isItemRotationEnabled ? CATransform3DConcat(rotateTransform, translateTransform) : translateTransform 228 | return CATransform3DConcat(circleTransform, scaleTransform) 229 | 230 | case .rollRotation: 231 | let toDegree: CGFloat = max(0, min(90, rollDegree)) 232 | let _degree: CGFloat = easingRatio * toDegree 233 | 234 | let degree: CGFloat 235 | switch rollEffect { 236 | case .rollUp : 237 | degree = _degree 238 | case .rollDown: 239 | degree = -_degree 240 | case .sineWave: 241 | degree = abs(_degree) 242 | case .reverseSineWave: 243 | degree = -abs(_degree) 244 | } 245 | 246 | let scale = self.calculatedScale(withRatio: easingRatio) 247 | let scaleTransform = CATransform3DScale(transform3DIdentity, scale, scale, 1) 248 | let rotateTransform = CATransform3DRotate(transform3DIdentity, degree * .pi / 180, 0, 1, 0) 249 | return CATransform3DConcat(scaleTransform, rotateTransform) 250 | 251 | case .pitchRotation: 252 | let toDegree: CGFloat = max(0, min(90, pitchDegree)) 253 | let _degree: CGFloat = easingRatio * toDegree 254 | 255 | let degree: CGFloat 256 | switch pitchEffect { 257 | case .pitchUp : 258 | degree = -_degree 259 | case .pitchDown: 260 | degree = _degree 261 | case .sineWave: 262 | degree = -abs(_degree) 263 | case .reverseSineWave: 264 | degree = abs(_degree) 265 | } 266 | 267 | let scale = self.calculatedScale(withRatio: easingRatio) 268 | let scaleTransform = CATransform3DScale(transform3DIdentity, scale, scale, 1) 269 | let rotateTransform = CATransform3DRotate(transform3DIdentity, degree * .pi / 180, 1, 0, 0) 270 | return CATransform3DConcat(scaleTransform, rotateTransform) 271 | 272 | case .yawRotation: 273 | let toDegree: CGFloat = max(0, min(90, pitchDegree)) 274 | let _degree: CGFloat = easingRatio * toDegree 275 | 276 | let degree: CGFloat 277 | switch yawEffect { 278 | case .yawUp : 279 | degree = _degree 280 | case .yawDown: 281 | degree = -_degree 282 | case .sineWave: 283 | degree = abs(_degree) 284 | case .reverseSineWave: 285 | degree = -abs(_degree) 286 | } 287 | 288 | let scale = self.calculatedScale(withRatio: easingRatio) 289 | let scaleTransform = CATransform3DScale(transform3DIdentity, scale, scale, 1) 290 | let rotateTransform = CATransform3DRotate(transform3DIdentity, degree * .pi / 180, 0, 0, 1) 291 | return CATransform3DConcat(scaleTransform, rotateTransform) 292 | 293 | case .scale: 294 | let scale = self.calculatedScale(withRatio: easingRatio) 295 | return CATransform3DScale(transform3DIdentity, scale, scale, 1) 296 | 297 | case .custom: 298 | let scaleX = calculatedScale(ofScale: scaleCoordinate.x, withRatio: easingRatio) 299 | let scaleY = calculatedScale(ofScale: scaleCoordinate.y, withRatio: easingRatio) 300 | let scaleZ = calculatedScale(ofScale: scaleCoordinate.z, withRatio: easingRatio) 301 | let scaleTransform = CATransform3DScale( 302 | transform3DIdentity, 303 | scaleCoordinate.x == 1 ? 1 : scaleX, 304 | scaleCoordinate.y == 1 ? 1 : scaleY, 305 | scaleCoordinate.z == 1 ? 1 : scaleZ 306 | ) 307 | 308 | let _vectorXDegree: CGFloat = max(0, min(90, rotationCoordinate.x)) 309 | let vectorXDegree: CGFloat = _vectorXDegree * easingRatio 310 | let rotationX = CATransform3DRotate( 311 | transform3DIdentity, 312 | vectorXDegree * .pi / 180, 313 | rotationCoordinate.x == 0 ? 0 : 1, 314 | 0, 315 | 0 316 | ) 317 | 318 | let _vectorYDegree: CGFloat = max(0, min(90, rotationCoordinate.y)) 319 | let vectorYDegree: CGFloat = _vectorYDegree * easingRatio 320 | let rotationY = CATransform3DRotate( 321 | transform3DIdentity, 322 | vectorYDegree * .pi / 180, 323 | 0, 324 | rotationCoordinate.y == 0 ? 0 : 1, 325 | 0 326 | ) 327 | 328 | let _vectorZDegree: CGFloat = max(0, min(90, rotationCoordinate.z)) 329 | let vectorZDegree: CGFloat = _vectorZDegree * easingRatio 330 | let rotationZ = CATransform3DRotate( 331 | transform3DIdentity, 332 | vectorZDegree * .pi / 180, 333 | 0, 334 | 0, 335 | rotationCoordinate.z == 0 ? 0 : 1 336 | ) 337 | 338 | let concatedRotateTransform = CATransform3DConcat(rotationX, CATransform3DConcat(rotationY, rotationZ)) 339 | 340 | let translateX = easingRatio > 0 ? translationCoordinate.x : -translationCoordinate.x 341 | let translateY = easingRatio > 0 ? translationCoordinate.y : -translationCoordinate.y 342 | let translateZ = easingRatio > 0 ? translationCoordinate.z : -translationCoordinate.z 343 | let translateTransform = CATransform3DTranslate( 344 | transform3DIdentity, 345 | translateX * easingRatio, 346 | translateY * easingRatio, 347 | translateZ * easingRatio 348 | ) 349 | 350 | return CATransform3DConcat(CATransform3DConcat(scaleTransform, concatedRotateTransform), translateTransform) 351 | 352 | case .none: 353 | return CATransform3DIdentity 354 | } 355 | } 356 | 357 | func anchorPoint(withDistanceRatio ratio: CGFloat) -> CGPoint { 358 | switch animation { 359 | case .cube: 360 | switch scrollDirection { 361 | case .vertical where ratio > 0: 362 | return CGPoint(x: 0.5, y: 0) 363 | case .vertical where ratio < 0: 364 | return CGPoint(x: 0.5, y: 1) 365 | case .horizontal where ratio > 0: 366 | return CGPoint(x: 0, y: 0.5) 367 | case .horizontal where ratio < 0: 368 | return CGPoint(x: 1, y: 0.5) 369 | default: 370 | return CGPoint(x: 0.5, y: 0.5) 371 | } 372 | 373 | case .circleRotation: 374 | switch (rotateDirection, scrollDirection) { 375 | case (.clockwise, .horizontal): 376 | return CGPoint(x: 0.5, y: 0) 377 | case (.anticlockwise, .horizontal): 378 | return CGPoint(x: 0.5, y: 1) 379 | case (.clockwise, .vertical): 380 | return CGPoint(x: 1, y: 0.5) 381 | case (.anticlockwise, .vertical): 382 | return CGPoint(x: 0, y: 0.5) 383 | } 384 | 385 | case .custom: 386 | return anchorPoint 387 | 388 | case .rollRotation, 389 | .pitchRotation, 390 | .yawRotation, 391 | .scale, 392 | .none: 393 | return CGPoint(x: 0.5, y: 0.5) 394 | } 395 | } 396 | 397 | func distanceFromCenter(withParentFrame parentFrame: CGRect, cellFrame: CGRect) -> CGFloat { 398 | switch scrollDirection { 399 | case .vertical: return cellFrame.midY - parentFrame.midY 400 | case .horizontal: return cellFrame.midX - parentFrame.midX 401 | } 402 | } 403 | 404 | func distanceRatio(withParentFrame parentFrame: CGRect, cellFrame: CGRect) -> CGFloat { 405 | let distance = distanceFromCenter(withParentFrame: parentFrame, cellFrame: cellFrame) 406 | switch scrollDirection { 407 | case .vertical: 408 | return distance / (parentFrame.height / 2 + cellFrame.height / 2) 409 | case .horizontal: 410 | return distance / (parentFrame.width / 2 + cellFrame.width / 2) 411 | } 412 | } 413 | 414 | func visibleMaxDistance(withParentFrame parentFrame: CGRect, cellFrame: CGRect) -> CGFloat { 415 | switch scrollDirection { 416 | case .vertical: 417 | return parentFrame.midY + cellFrame.height / 2 418 | case .horizontal: 419 | return parentFrame.midX + cellFrame.width / 2 420 | } 421 | } 422 | 423 | private func calculatedScale(withRatio ratio: CGFloat) -> CGFloat { 424 | return calculatedScale(ofScale: scale, withRatio: ratio) 425 | } 426 | 427 | private func calculatedScale(ofScale scale: CGFloat, withRatio ratio: CGFloat) -> CGFloat { 428 | let scale: CGFloat = min(max(scale, 0), 1) 429 | switch scaleEffect { 430 | case .scaleUp: 431 | return 1 - (1 - scale) * abs(ratio) 432 | case .scaleDown: 433 | return scale + (1 - scale) * abs(ratio) 434 | } 435 | } 436 | } 437 | 438 | 439 | --------------------------------------------------------------------------------