mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Avatar Bloom View in home screen navigation bar (#1630)
* experimental code * veyr close to a perfect solution but i need to make it crop * code improvementds * for now this looks like the best solution but there is a weird animation happening * hard edge solution that completely works. * dark mode support * 17 support * code improvement and dark mode improvement * bloom gradient, it works but it needs a fix for landscape + the gradient should get hidden while in search mode * hides when searching * crash free constraint * fixed everything only the scroll bar issue needs fixing now * oblique gradient effect * removed unused code * removed unused code * updated the bloom effects * hairline restored for everything aside from the home screen * code improvements * possible final implementation * pr suggestion * hairline view added * Update ElementX/Sources/Other/ScrollViewAdapter.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> * code adjustments * typo fix * typo fix --------- Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
This commit is contained in:
@@ -49,11 +49,19 @@ class ScrollViewAdapter: NSObject, UIScrollViewDelegate {
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private let isAtTopEdgeSubject: CurrentValueSubject<Bool, Never> = .init(false)
|
||||
var isAtTopEdge: CurrentValuePublisher<Bool, Never> {
|
||||
isAtTopEdgeSubject
|
||||
.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
// MARK: - UIScrollViewDelegate
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
didScrollSubject.send(())
|
||||
updateScrollDirection(scrollView)
|
||||
let insetContentOffset = scrollView.contentOffset.y + scrollView.contentInset.top
|
||||
isAtTopEdgeSubject.send(insetContentOffset >= 3)
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct BloomView: View {
|
||||
@ObservedObject var context: HomeScreenViewModel.Context
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
avatar
|
||||
.blur(radius: 64)
|
||||
.blendMode(colorScheme == .dark ? .exclusion : .hardLight)
|
||||
.opacity(colorScheme == .dark ? 0.50 : 0.20)
|
||||
avatar
|
||||
.blur(radius: 64)
|
||||
.blendMode(.color)
|
||||
.opacity(colorScheme == .dark ? 0.20 : 0.80)
|
||||
}
|
||||
}
|
||||
|
||||
private var avatar: some View {
|
||||
LoadableAvatarImage(url: context.viewState.userAvatarURL,
|
||||
name: context.viewState.userDisplayName,
|
||||
contentID: context.viewState.userID,
|
||||
avatarSize: .custom(256),
|
||||
imageProvider: context.imageProvider)
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,13 @@ struct HomeScreen: View {
|
||||
@State private var scrollViewAdapter = ScrollViewAdapter()
|
||||
@State private var isSearching = false
|
||||
|
||||
// Bloom components
|
||||
@State private var bloomView: UIView?
|
||||
@State private var leftBarButtonView: UIView?
|
||||
@State private var gradientView: UIView?
|
||||
@State private var navigationBarContainer: UIView?
|
||||
@State private var hairlineView: UIView?
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView {
|
||||
@@ -56,7 +63,7 @@ struct HomeScreen: View {
|
||||
.disableAutocorrection(true)
|
||||
}
|
||||
}
|
||||
.introspect(.scrollView, on: .iOS(.v16)) { scrollView in
|
||||
.introspect(.scrollView, on: .iOS(.v16, .v17)) { scrollView in
|
||||
guard scrollView != scrollViewAdapter.scrollView else { return }
|
||||
scrollViewAdapter.scrollView = scrollView
|
||||
}
|
||||
@@ -87,10 +94,96 @@ struct HomeScreen: View {
|
||||
.toolbar { toolbar }
|
||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
||||
.track(screen: .home)
|
||||
.introspect(.viewController, on: .iOS(.v16, .v17)) { controller in
|
||||
Task {
|
||||
if bloomView == nil {
|
||||
makeBloomView(controller: controller)
|
||||
}
|
||||
}
|
||||
let isHidden = controller.navigationController?.topViewController != controller || isSearching
|
||||
bloomView?.isHidden = isHidden
|
||||
gradientView?.isHidden = isHidden
|
||||
navigationBarContainer?.clipsToBounds = !isHidden
|
||||
hairlineView?.isHidden = isHidden || !scrollViewAdapter.isAtTopEdge.value
|
||||
if !isHidden {
|
||||
updateBloomCenter()
|
||||
}
|
||||
}
|
||||
.onReceive(scrollViewAdapter.isAtTopEdge.removeDuplicates()) { value in
|
||||
hairlineView?.isHidden = !value
|
||||
guard let gradientView else {
|
||||
return
|
||||
}
|
||||
if value {
|
||||
UIView.transition(with: gradientView, duration: 0.3, options: .curveEaseIn) {
|
||||
gradientView.alpha = 0
|
||||
}
|
||||
} else {
|
||||
gradientView.alpha = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var bloomGradient: some View {
|
||||
LinearGradient(colors: [.clear, .compound.bgCanvasDefault], startPoint: .top, endPoint: .bottom)
|
||||
.mask {
|
||||
LinearGradient(stops: [.init(color: .white, location: 0.75), .init(color: .clear, location: 1.0)],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing)
|
||||
}
|
||||
.ignoresSafeArea(edges: .all)
|
||||
}
|
||||
|
||||
private func makeBloomView(controller: UIViewController) {
|
||||
guard let navigationBarContainer = controller.navigationController?.navigationBar.subviews.first,
|
||||
let leftBarButtonView = controller.navigationItem.leadingItemGroups.first?.barButtonItems.first?.customView else {
|
||||
return
|
||||
}
|
||||
|
||||
let bloomController = UIHostingController(rootView: bloom)
|
||||
bloomController.view.translatesAutoresizingMaskIntoConstraints = true
|
||||
bloomController.view.backgroundColor = .clear
|
||||
navigationBarContainer.insertSubview(bloomController.view, at: 0)
|
||||
self.leftBarButtonView = leftBarButtonView
|
||||
bloomView = bloomController.view
|
||||
self.navigationBarContainer = navigationBarContainer
|
||||
updateBloomCenter()
|
||||
|
||||
let gradientController = UIHostingController(rootView: bloomGradient)
|
||||
gradientController.view.backgroundColor = .clear
|
||||
gradientController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
navigationBarContainer.insertSubview(gradientController.view, aboveSubview: bloomController.view)
|
||||
|
||||
let constraints = [gradientController.view.bottomAnchor.constraint(equalTo: navigationBarContainer.bottomAnchor),
|
||||
gradientController.view.trailingAnchor.constraint(equalTo: navigationBarContainer.trailingAnchor),
|
||||
gradientController.view.leadingAnchor.constraint(equalTo: navigationBarContainer.leadingAnchor),
|
||||
gradientController.view.heightAnchor.constraint(equalToConstant: 40)]
|
||||
constraints.forEach { $0.isActive = true }
|
||||
gradientView = gradientController.view
|
||||
|
||||
let dividerController = UIHostingController(rootView: Divider().ignoresSafeArea())
|
||||
dividerController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
navigationBarContainer.addSubview(dividerController.view)
|
||||
let dividerConstraints = [dividerController.view.bottomAnchor.constraint(equalTo: gradientController.view.bottomAnchor),
|
||||
dividerController.view.widthAnchor.constraint(equalTo: gradientController.view.widthAnchor),
|
||||
dividerController.view.leadingAnchor.constraint(equalTo: gradientController.view.leadingAnchor)]
|
||||
dividerConstraints.forEach { $0.isActive = true }
|
||||
hairlineView = dividerController.view
|
||||
}
|
||||
|
||||
private func updateBloomCenter() {
|
||||
guard let leftBarButtonView,
|
||||
let bloomView,
|
||||
let navigationBarContainer = bloomView.superview else {
|
||||
return
|
||||
}
|
||||
|
||||
let center = leftBarButtonView.convert(leftBarButtonView.center, to: navigationBarContainer.coordinateSpace)
|
||||
bloomView.center = center
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
/// The session verification banner and invites button if either are needed.
|
||||
private var topSection: some View {
|
||||
@@ -155,6 +248,10 @@ struct HomeScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var bloom: some View {
|
||||
BloomView(context: context)
|
||||
}
|
||||
|
||||
private var newRoomButton: some View {
|
||||
Button {
|
||||
context.send(viewAction: .startChat)
|
||||
|
||||
Reference in New Issue
Block a user