From 98befac78c074022b98e3c114f03451f59666f83 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 24 Sep 2019 16:34:11 -0500 Subject: [PATCH] Animate reader view button --- Mac/MainWindow/ArticleExtractorButton.swift | 4 +- NetNewsWire.xcodeproj/project.pbxproj | 4 + iOS/AppAssets.swift | 22 +++++ iOS/Article/ArticleExtractorButton.swift | 78 ++++++++++++++++++ iOS/Article/ArticleViewController.swift | 18 +++- iOS/Base.lproj/Main.storyboard | 17 ++-- .../ArticleExtractorOff.pdf | Bin 0 -> 4289 bytes .../Contents.json | 15 ++++ .../ArticleExtractorOff.pdf | Bin 0 -> 4289 bytes .../Contents.json | 15 ++++ .../ArticleExtractorOn.pdf | Bin 0 -> 4197 bytes .../articleExtractorOn.imageset/Contents.json | 15 ++++ iOS/SceneCoordinator.swift | 10 ++- 13 files changed, 182 insertions(+), 16 deletions(-) create mode 100644 iOS/Article/ArticleExtractorButton.swift create mode 100644 iOS/Resources/Assets.xcassets/articleExtractorError.imageset/ArticleExtractorOff.pdf create mode 100644 iOS/Resources/Assets.xcassets/articleExtractorError.imageset/Contents.json create mode 100644 iOS/Resources/Assets.xcassets/articleExtractorOff.imageset/ArticleExtractorOff.pdf create mode 100644 iOS/Resources/Assets.xcassets/articleExtractorOff.imageset/Contents.json create mode 100644 iOS/Resources/Assets.xcassets/articleExtractorOn.imageset/ArticleExtractorOn.pdf create mode 100644 iOS/Resources/Assets.xcassets/articleExtractorOn.imageset/Contents.json diff --git a/Mac/MainWindow/ArticleExtractorButton.swift b/Mac/MainWindow/ArticleExtractorButton.swift index 4fd04bf45..69e250ed5 100644 --- a/Mac/MainWindow/ArticleExtractorButton.swift +++ b/Mac/MainWindow/ArticleExtractorButton.swift @@ -57,7 +57,7 @@ class ArticleExtractorButton: NSButton { case isError: addImageSublayer(to: hostedLayer, image: AppAssets.articleExtractorError, opacity: opacity) case isInProgress: - addProgressSublayer(to: hostedLayer) + addAnimatedSublayer(to: hostedLayer) default: addImageSublayer(to: hostedLayer, image: AppAssets.articleExtractor, opacity: opacity) } @@ -77,7 +77,7 @@ class ArticleExtractorButton: NSButton { hostedLayer.addSublayer(imageLayer) } - private func addProgressSublayer(to hostedLayer: CALayer) { + private func addAnimatedSublayer(to hostedLayer: CALayer) { let imageProgress1 = AppAssets.articleExtractorProgress1 let imageProgress2 = AppAssets.articleExtractorProgress2 let imageProgress3 = AppAssets.articleExtractorProgress3 diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 3ed1b7a91..4f88ef057 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 510BD15D232D765D002692E4 /* SettingsReaderAPIAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557EE1A522B6F4E1004206FA /* SettingsReaderAPIAccountView.swift */; }; + 51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */; }; 51126DA4225FDE2F00722696 /* RSImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */; }; 5115CAF42266301400B21BCE /* AddContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */; }; 511D43CF231FA62200FB1562 /* DetailKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */; }; @@ -777,6 +778,7 @@ 510D707D22B02A4B004E8F65 /* SettingsLocalAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsLocalAccountView.swift; sourceTree = ""; }; 510D707F22B02A5F004E8F65 /* SettingsFeedbinAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFeedbinAccountView.swift; sourceTree = ""; }; 510D708122B041CC004E8F65 /* SettingsAccountLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountLabelView.swift; sourceTree = ""; }; + 51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractorButton.swift; sourceTree = ""; }; 51121AA12265430A00BC0EC1 /* NetNewsWire_iOSapp_target.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSapp_target.xcconfig; sourceTree = ""; }; 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContainerViewController.swift; sourceTree = ""; }; 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-Extensions.swift"; sourceTree = ""; }; @@ -1377,6 +1379,7 @@ children = ( 51C4527E2265092C00C03939 /* ArticleViewController.swift */, 517630222336657E00E15FFF /* ArticleViewControllerWebViewProvider.swift */, + 51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */, ); path = Article; sourceTree = ""; @@ -2823,6 +2826,7 @@ 51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */, 51C452AF2265108300C03939 /* ArticleArray.swift in Sources */, 51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */, + 51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */, 84CAFCA522BC8C08007694F0 /* FetchRequestQueue.swift in Sources */, 51C4529C22650A1000C03939 /* SingleFaviconDownloader.swift in Sources */, 51E595A6228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */, diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift index 4cf148530..d669980d0 100644 --- a/iOS/AppAssets.swift +++ b/iOS/AppAssets.swift @@ -10,6 +10,28 @@ import RSCore struct AppAssets { + static var articleExtractorError: UIImage = { + return UIImage(named: "articleExtractorOff")! + }() + + static var articleExtractorOff: UIImage = { + return UIImage(named: "articleExtractorOff")! + }() + + static var articleExtractorOffTinted: UIImage = { + let image = UIImage(named: "articleExtractorOff")! + return image.maskWithColor(color: AppAssets.primaryAccentColor.cgColor)! + }() + + static var articleExtractorOn: UIImage = { + return UIImage(named: "articleExtractorOn")! + }() + + static var articleExtractorOnTinted: UIImage = { + let image = UIImage(named: "articleExtractorOn")! + return image.maskWithColor(color: AppAssets.primaryAccentColor.cgColor)! + }() + static var avatarBackgroundColor: UIColor = { return UIColor(named: "avatarBackgroundColor")! }() diff --git a/iOS/Article/ArticleExtractorButton.swift b/iOS/Article/ArticleExtractorButton.swift new file mode 100644 index 000000000..8c4641964 --- /dev/null +++ b/iOS/Article/ArticleExtractorButton.swift @@ -0,0 +1,78 @@ +// +// ArticleExtractorButton.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 9/24/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit + +enum ArticleExtractorButtonState { + case error + case animated + case on + case off +} + +class ArticleExtractorButton: UIButton { + + var buttonState: ArticleExtractorButtonState = .off { + didSet { + if buttonState != oldValue { + switch buttonState { + case .error: + stripSublayer() + setImage(AppAssets.articleExtractorError, for: .normal) + case .animated: + setImage(nil, for: .normal) + setNeedsLayout() + case .on: + stripSublayer() + setImage(AppAssets.articleExtractorOn, for: .normal) + case .off: + stripSublayer() + setImage(AppAssets.articleExtractorOff, for: .normal) + } + } + } + } + + override func layoutSubviews() { + super.layoutSubviews() + guard case .animated = buttonState else { + return + } + stripSublayer() + addAnimatedSublayer(to: layer) + } + + private func stripSublayer() { + if layer.sublayers?.count ?? 0 > 1 { + layer.sublayers?.last?.removeFromSuperlayer() + } + } + + private func addAnimatedSublayer(to hostedLayer: CALayer) { + let image1 = AppAssets.articleExtractorOffTinted.cgImage! + let image2 = AppAssets.articleExtractorOnTinted.cgImage! + let images = [image1, image2, image1] + + let imageLayer = CALayer() + let imageSize = AppAssets.articleExtractorOff.size + imageLayer.bounds = CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height) + imageLayer.position = CGPoint(x: bounds.midX, y: bounds.midY) + + hostedLayer.addSublayer(imageLayer) + + let animation = CAKeyframeAnimation(keyPath: "contents") + animation.calculationMode = CAAnimationCalculationMode.linear + animation.keyTimes = [0, 0.5, 1] + animation.duration = 2 + animation.values = images as [Any] + animation.repeatCount = HUGE + + imageLayer.add(animation, forKey: "contents") + } + +} diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index f60a5da49..5684e0572 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -22,7 +22,7 @@ enum ArticleViewState: Equatable { class ArticleViewController: UIViewController { - @IBOutlet private weak var readerViewBarButtonItem: UIBarButtonItem! + @IBOutlet private weak var articleExtractorButton: ArticleExtractorButton! @IBOutlet private weak var nextUnreadBarButtonItem: UIBarButtonItem! @IBOutlet private weak var prevArticleBarButtonItem: UIBarButtonItem! @IBOutlet private weak var nextArticleBarButtonItem: UIBarButtonItem! @@ -55,6 +55,15 @@ class ArticleViewController: UIViewController { } } + var articleExtractorButtonState: ArticleExtractorButtonState { + get { + return articleExtractorButton.buttonState + } + set { + articleExtractorButton.buttonState = newValue + } + } + private let keyboardManager = KeyboardManager(type: .detail) override var keyCommands: [UIKeyCommand]? { return keyboardManager.keyCommands @@ -74,6 +83,9 @@ class ArticleViewController: UIViewController { NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange(_:)), name: UIContentSizeCategory.didChangeNotification, object: nil) + // For some reason interface builder won't let me set this there. + articleExtractorButton.addTarget(self, action: #selector(toggleArticleExtractor(_:)), for: .touchUpInside) + ArticleViewControllerWebViewProvider.shared.dequeueWebView() { webView in self.webView = webView @@ -96,6 +108,7 @@ class ArticleViewController: UIViewController { func updateUI() { guard let article = currentArticle else { + articleExtractorButton.isEnabled = false nextUnreadBarButtonItem.isEnabled = false prevArticleBarButtonItem.isEnabled = false nextArticleBarButtonItem.isEnabled = false @@ -110,6 +123,7 @@ class ArticleViewController: UIViewController { prevArticleBarButtonItem.isEnabled = coordinator.isPrevArticleAvailable nextArticleBarButtonItem.isEnabled = coordinator.isNextArticleAvailable + articleExtractorButton.isEnabled = true readBarButtonItem.isEnabled = true starBarButtonItem.isEnabled = true browserBarButtonItem.isEnabled = true @@ -179,7 +193,7 @@ class ArticleViewController: UIViewController { // MARK: Actions - @IBAction func toggleReaderView(_ sender: Any) { + @IBAction func toggleArticleExtractor(_ sender: Any) { coordinator.toggleArticleExtractor() } diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard index ef74ea072..17f5e4a52 100644 --- a/iOS/Base.lproj/Main.storyboard +++ b/iOS/Base.lproj/Main.storyboard @@ -99,25 +99,24 @@ - - - - - - - + + + - @@ -240,8 +239,8 @@ + - diff --git a/iOS/Resources/Assets.xcassets/articleExtractorError.imageset/ArticleExtractorOff.pdf b/iOS/Resources/Assets.xcassets/articleExtractorError.imageset/ArticleExtractorOff.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2df7fdd06dcf42ba2126e3e59c29918e100ead66 GIT binary patch literal 4289 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�XTNJ7>TekJ4C!HwXK&@uI3ytcEab(ynR~gm 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�XTNJ7>TekJ4C!HwXK&@uI3ytcEab(ynR~gm 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 j99C9F79$V-?~