Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement onboarding slide animation #15489

Merged
merged 1 commit into from
Mar 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed resources/images/ui2/[email protected]
Binary file not shown.
Binary file removed resources/images/ui2/[email protected]
Binary file not shown.
Binary file removed resources/images/ui2/[email protected]
Binary file not shown.
Binary file removed resources/images/ui2/[email protected]
Binary file not shown.
Binary file removed resources/images/ui2/[email protected]
Binary file not shown.
Binary file removed resources/images/ui2/[email protected]
Binary file not shown.
Binary file removed resources/images/ui2/[email protected]
Binary file not shown.
Binary file removed resources/images/ui2/[email protected]
Binary file not shown.
Binary file removed resources/images/ui2/[email protected]
Binary file not shown.
Binary file removed resources/images/ui2/[email protected]
Binary file not shown.
Binary file removed resources/images/ui2/[email protected]
Binary file not shown.
Binary file added resources/images/ui2/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/images/ui2/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions src/js/worklets/onboarding_carousel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useDerivedValue, withTiming, Easing } from 'react-native-reanimated';

const slideAnimationDuration = 300;

const easeOut = {
duration: slideAnimationDuration,
easing: Easing.bezier(0, 0, 0.58, 1),
}

// Derived Values
export function dynamicProgressBarWidth(staticProgressBarWidth, progress) {
return useDerivedValue(
function () {
'worklet'
return staticProgressBarWidth * (progress.value || 0) / 100;
}
);
}

export function carouselLeftPosition(windowWidth, progress) {
return useDerivedValue(
function () {
'worklet'
const progressValue = progress.value;
switch (true) {
case (progressValue < 25):
return 0;
case (progressValue === 25):
return withTiming(-windowWidth, easeOut);
case (progressValue < 50):
return -windowWidth;
case (progressValue === 50):
return withTiming(-2 * windowWidth, easeOut);
case (progressValue < 75):
return -2 * windowWidth;
case (progressValue === 75):
return withTiming(-3 * windowWidth, easeOut);
case (progressValue < 100):
return -3 * windowWidth;
case (progressValue === 100):
return withTiming(-4 * windowWidth, easeOut);
default:
return 0;
}
});
}

7 changes: 6 additions & 1 deletion src/mocks/js_dependencies.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,10 @@ globalThis.__STATUS_MOBILE_JS_IDENTITY_PROXY__ = new Proxy({}, {get() { return (
:createNativeWrapper identity
:default #js {}})

(def react-native-redash #js {:clamp nil})
(def react-native-redash
#js
{:clamp nil
:withPause (fn [])})

(def react-native-languages
(clj->js {:default {:language "en"
Expand Down Expand Up @@ -370,6 +373,7 @@ globalThis.__STATUS_MOBILE_JS_IDENTITY_PROXY__ = new Proxy({}, {get() { return (
"react-native-screens" (clj->js {})
"react-native-reanimated" react-native-reanimated
"react-native-redash/lib/module/v1" react-native-redash
"react-native-redash" react-native-redash
"react-native-fetch-polyfill" fetch
"react-native-status-keycard" status-keycard
"react-native-keychain" keychain
Expand Down Expand Up @@ -407,6 +411,7 @@ globalThis.__STATUS_MOBILE_JS_IDENTITY_PROXY__ = new Proxy({}, {get() { return (
"../src/js/worklets/bottom_sheet.js" #js {}
"../src/js/worklets/record_audio.js" #js {}
"../src/js/worklets/scroll_view.js" #js {}
"../src/js/worklets/onboarding_carousel.js" #js {}
"../src/js/worklets/lightbox.js" #js {}
"./fleets.js" default-fleets
"@walletconnect/client" wallet-connect-client
Expand Down
1 change: 1 addition & 0 deletions src/quo2/foundations/colors.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@

;;Solid
(def black "#000000")
(def onboarding-header-black "#000716")

;;;;Primary

Expand Down
5 changes: 4 additions & 1 deletion src/react_native/reanimated.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
withDelay
withSpring
withRepeat
withSequence
withDecay
Easing
Keyframe
Expand All @@ -17,8 +18,8 @@
SlideOutUp
LinearTransition)]
[reagent.core :as reagent]
["react-native-redash" :refer (withPause)]
[react-native.flat-list :as rn-flat-list]
[utils.collection]
[utils.worklets.core :as worklets.core]))

(def ^:const default-duration 300)
Expand Down Expand Up @@ -59,6 +60,8 @@
(def with-decay withDecay)
(def key-frame Keyframe)
(def with-repeat withRepeat)
(def with-sequence withSequence)
(def with-pause withPause)
(def cancel-animation cancelAnimation)

;; Easings
Expand Down
23 changes: 9 additions & 14 deletions src/status_im2/common/resources.cljs
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
(ns status-im2.common.resources)

(def ui
{:add-new-contact (js/require "../resources/images/ui2/add-contact.png")
:intro-1 (js/require "../resources/images/ui2/intro-1.png")
:intro-2 (js/require "../resources/images/ui2/intro-2.png")
:intro-3 (js/require "../resources/images/ui2/intro-3.png")
:intro-4 (js/require "../resources/images/ui2/intro-4.png")
:lifestyle (js/require "../resources/images/ui2/lifestyle.png")
:music (js/require "../resources/images/ui2/music.png")
:podcasts (js/require "../resources/images/ui2/podcasts.png")
:sync-device (js/require "../resources/images/ui2/sync-new-device-cover-background.png")
:onboarding-bg-1 (js/require "../resources/images/ui2/onboarding-bg-1.png")
:onboarding-blur-bg (js/require "../resources/images/ui2/onboarding_blur_bg.png")
:generate-keys (js/require "../resources/images/ui2/generate_keys.png")
:ethereum-address (js/require "../resources/images/ui2/ethereum_address.png")
:use-keycard (js/require "../resources/images/ui2/keycard.png")})
{:add-new-contact (js/require "../resources/images/ui2/add-contact.png")
:lifestyle (js/require "../resources/images/ui2/lifestyle.png")
:music (js/require "../resources/images/ui2/music.png")
:podcasts (js/require "../resources/images/ui2/podcasts.png")
:sync-device (js/require "../resources/images/ui2/sync-new-device-cover-background.png")
:generate-keys (js/require "../resources/images/ui2/generate_keys.png")
:ethereum-address (js/require "../resources/images/ui2/ethereum_address.png")
:use-keycard (js/require "../resources/images/ui2/keycard.png")
:onboarding-illustration (js/require "../resources/images/ui2/onboarding_illustration.png")})

(def mock-images
{:coinbase (js/require "../resources/images/mock2/coinbase.png")
Expand Down
10 changes: 1 addition & 9 deletions src/status_im2/contexts/onboarding/common/background/style.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,12 @@
{:background-color colors/neutral-95
:flex-direction :row
:position :absolute
:overflow :hidden
:top 0
:bottom 0
:left 0
:right 0})

(defn background-gradient-overlay
[dark-overlay?]
{:position :absolute
:height (if dark-overlay? 240 136)
:top 0
:left 0
:right 0
:bottom 0})

(def background-blur-overlay
{:position :absolute
:left 0
Expand Down
36 changes: 16 additions & 20 deletions src/status_im2/contexts/onboarding/common/background/view.cljs
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
(ns status-im2.contexts.onboarding.common.background.view
(:require [react-native.core :as rn]
[quo2.foundations.colors :as colors]
[status-im2.common.resources :as resources]
[react-native.linear-gradient :as linear-gradient]
[status-im2.contexts.onboarding.common.background.style :as style]))
[react-native.blur :as blur]
[status-im2.contexts.onboarding.common.carousel.view :as carousel]
[status-im2.contexts.onboarding.common.background.style :as style]
[status-im2.contexts.onboarding.common.carousel.animation :as carousel.animation]))

(defn view
[dark-overlay?]
[rn/view
{:style style/background-container}
[rn/image
{:blur-radius (if dark-overlay? 13 0)
:style {:height "100%"
:width "100%"}
;; Todo - get background image from sub using carousel index on landing page
:source (resources/get-image :onboarding-bg-1)}]
[linear-gradient/linear-gradient
{:colors [(if dark-overlay? (colors/custom-color :yin 50) "#000716")
(if dark-overlay? (colors/custom-color :yin 50 0) "#000716")]
:start {:x 0 :y 0}
:end {:x 0 :y 1}
:style (style/background-gradient-overlay dark-overlay?)}]
(when dark-overlay?
[:f>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend to put that functional component in a different function:

(defn my-f-component [props]
  ;;component code
  )
  
(defn component-exposed-to-the-world [props]
  [:f> my-f-component props])

(fn []
(carousel.animation/initialize-animation)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is the carousel here? 🤔
nearly all of the onboarding pages are using this background component except for the intro page and the intro page is using the carousel component.

Should we not create a separate background component for this use case?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nearly all of the onboarding pages are using this background component except for the intro page and the intro page is using the carousel component.

Hi, I don't understand what you mean, intro page is also using background component

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah you are right , it was not before. I understand now the other pages are using this for where the background image has stopped and the section being shown. Nicely done :D

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd use useEffect hook with empty array to do initialization stuff, any reason to not do it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @briansztamfater,
According to the react docs - Effects let you specify side effects that are caused by rendering itself
So, if I use effect here then the animation will be initialized after rendering.
But I need to initialize it before rendering, otherwise, shared-values will be nil and will throw an error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about useLayoutEffect? 🤔 https://react.dev/reference/react/useLayoutEffect

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, how this will be different then calling function before view?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% if this applies here, but useLayoutEffect works similar to useEffect except it fires before the screen is painted. It's probably safer to initialise this way in React if possible. But it's not a must have, just worth exploring :)

[rn/view
{:style style/background-blur-overlay}])])
{:style style/background-container}
[carousel/view dark-overlay?]
(when dark-overlay?
[blur/view
{:style style/background-blur-overlay
:blur-amount 30
:blur-radius 25
:blur-type :transparent
:overlay-color :transparent}])])])
50 changes: 50 additions & 0 deletions src/status_im2/contexts/onboarding/common/carousel/animation.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
(ns status-im2.contexts.onboarding.common.carousel.animation
(:require
[react-native.reanimated :as reanimated]
[utils.worklets.onboarding-carousel :as worklets.onboarding-carousel]))

(def progress (atom nil))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder is it possible to avoid these global atoms and instead have them scoped to the onboarding stack? 🤔

Copy link
Member Author

@Parveshdhull Parveshdhull Mar 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, we can initialize them on top of root and pass as params to other stacks.
Although we have two roots, we have to do this initialization in profiles & intro.
But just curious why we want to do that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can initialize them on top of root and pass as params to other stacks

It sounds complicated to pass shared values as params to navigated screens

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I believe we want to avoid global atoms as much as possible

I understand there are exceptions and perhaps it's needed/ not so much of an issue in this case

cc: @ilmotta @ulisesmac

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for cc'ing me @J-Son89. Let's be real, global state is often not a big issue since most of our global atoms or ratoms are used only in the scope of a single namespace, at least that's how I feel. Practically speaking, it's passable 🤷‍♂️

But having said that, I still think it's weird... In one hand Clojure(Script) promotes pure code as much as possible and controlled, local state. On the other hand, ClojureScript and Reagent allow us to define state used for views outside view components, something that's a no-no in React itself. That is to say, even raw React code feels more functional than our own ClojureScript code with global vars.

It's a discussion without a clear answer to me. As usual, almost all global state is created in the name of convenience, so this is not enough to justify, at least to me.

Given this discussion come and go every now and then, maybe it's about time we write something in the guidelines if we can all agree on something. Perhaps the discussion should be made outside this PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I think it would be nice to discuss this. As far as I see it one big drawback is this code becomes a lot less reusable. In this case we have other carousels in the new designs and so ideally we could use this component/code (or at least a good chunk of it) to create these carousels. Anyway if that is the case we can also let the developer who will work on these aim to refactor when they go to use this code. Ideally we can do as much as possible to enable that from the initial point code is added to the code base.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point, reusability is one big reason to favor local state, because there's no state to synchronize between components in the first place.

(def paused (atom nil))

(def ^:const progress-bar-animation-delay 300)
(def ^:const progress-bar-animation-duration 4000)

(defn slide-animation
[progress-percentage & [delay duration]]
(reanimated/with-delay
(or delay progress-bar-animation-delay)
(reanimated/with-timing
progress-percentage
(js-obj "duration" (or duration progress-bar-animation-duration)
"easing" (:linear reanimated/easings)))))

(defn animate-progress
[progress paused]
(reanimated/set-shared-value
progress
(reanimated/with-pause
(reanimated/with-repeat
(reanimated/with-sequence
(slide-animation 25)
(slide-animation 50)
(slide-animation 75)
(slide-animation 100)
(slide-animation 0 300 0))
-1)
paused)))

(defn initialize-animation
[]
(when-not @progress
(reset! progress (reanimated/use-shared-value 0))
(reset! paused (reanimated/use-shared-value false))
(animate-progress @progress @paused)))

;; Derived Values
(defn carousel-left-position
[window-width]
(worklets.onboarding-carousel/carousel-left-position window-width @progress))

(defn dynamic-progress-bar-width
[progress-bar-width]
(worklets.onboarding-carousel/dynamic-progress-bar-width progress-bar-width @progress))
73 changes: 73 additions & 0 deletions src/status_im2/contexts/onboarding/common/carousel/style.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
(ns status-im2.contexts.onboarding.common.carousel.style
(:require
[quo2.foundations.colors :as colors]
[react-native.reanimated :as reanimated]))

(defn header-container
[status-bar-height content-width index]
{:position :absolute
:top 0
:left (* content-width index)
:padding-top (+ 30 status-bar-height)
:width content-width
:height (+ 96 status-bar-height)
:flex-direction :row
:background-color colors/onboarding-header-black})

(defn header-text-view
[window-width]
{:flex-direction :column
:width window-width
:padding-left 20})

(def carousel-text
{:color colors/white})

(def carousel-sub-text
{:color colors/white
:margin-top 2})

(defn background-image
[content-width]
{:resize-mode :stretch
:width content-width})

(defn progress-bar-item
[static? end?]
{:height 2
:flex 1
:background-color (if static? colors/white-opa-10 colors/white)
:margin-right (if end? 0 8)
:border-radius 4})

(defn progress-bar
[width]
{:position :absolute
:top 0
:width width
:flex-direction :row})

(defn dynamic-progress-bar
[width]
(reanimated/apply-animations-to-style
{:width width}
{:height 2
:border-radius 4
:overflow :hidden}))

(defn progress-bar-container
[progress-bar-width status-bar-height]
{:position :absolute
:width progress-bar-width
:margin-left 20
:top (+ 12 status-bar-height)})

(defn carousel-container
[left]
(reanimated/apply-animations-to-style
{:left left}
{:position :absolute
:right 0
:top 0
:bottom 0
:flex-direction :row}))
Loading