From 6c14fd1cb922f744021ff3bef2d7eed84b075648 Mon Sep 17 00:00:00 2001 From: Omar Basem Date: Thu, 19 Jan 2023 16:46:05 +0400 Subject: [PATCH] Scroll page animations (#14695) * feat: scroll page animations --- src/js/worklet_factory.js | 14 +- src/react_native/reanimated.cljs | 15 +- src/status_im2/common/scroll_page/style.cljs | 72 ++++++--- src/status_im2/common/scroll_page/view.cljs | 147 +++++++++++------- .../contexts/communities/overview/view.cljs | 16 +- 5 files changed, 159 insertions(+), 105 deletions(-) diff --git a/src/js/worklet_factory.js b/src/js/worklet_factory.js index 645b19300d..718c906dbf 100644 --- a/src/js/worklet_factory.js +++ b/src/js/worklet_factory.js @@ -5,36 +5,36 @@ import { useDerivedValue, interpolate } from 'react-native-reanimated'; export function applyAnimationsToStyle(animations, style) { return function() { 'worklet' - + var animatedStyle = {} - + for (var key in animations) { if (key == "transform") { var transforms = animations[key]; var animatedTransforms = [] - + for (var transform of transforms) { var transformKey = Object.keys(transform)[0]; animatedTransforms.push({ [transformKey]: transform[transformKey].value }) } - + animatedStyle[key] = animatedTransforms; } else { animatedStyle[key] = animations[key].value; } } - + return Object.assign(animatedStyle, style); }; }; -export function interpolateValue(sharedValue, inputRange, outputRange) { +export function interpolateValue(sharedValue, inputRange, outputRange, extrapolation) { return useDerivedValue( function () { 'worklet' - return interpolate(sharedValue.value, inputRange, outputRange); + return interpolate(sharedValue.value, inputRange, outputRange, extrapolation); } ); } diff --git a/src/react_native/reanimated.cljs b/src/react_native/reanimated.cljs index 24d3d811b5..84f3739380 100644 --- a/src/react_native/reanimated.cljs +++ b/src/react_native/reanimated.cljs @@ -1,6 +1,7 @@ (ns react-native.reanimated (:require ["react-native" :as rn] ["react-native-linear-gradient" :default LinearGradient] + ["@react-native-community/blur" :as blur] ["react-native-reanimated" :default reanimated :refer (useSharedValue useAnimatedStyle withTiming @@ -30,6 +31,7 @@ (def touchable-opacity (create-animated-component (.-TouchableOpacity ^js rn))) (def linear-gradient (create-animated-component LinearGradient)) +(def blur-view (create-animated-component (.-BlurView blur))) ;; Hooks (def use-shared-value useSharedValue) @@ -67,11 +69,14 @@ (def worklet-factory (js/require "../src/js/worklet_factory.js")) (defn interpolate - [shared-value input-range output-range] - (.interpolateValue ^js worklet-factory - shared-value - (clj->js input-range) - (clj->js output-range))) + ([shared-value input-range output-range] + (interpolate shared-value input-range output-range nil)) + ([shared-value input-range output-range extrapolation] + (.interpolateValue ^js worklet-factory + shared-value + (clj->js input-range) + (clj->js output-range) + (clj->js extrapolation)))) ;;;; Component Animations diff --git a/src/status_im2/common/scroll_page/style.cljs b/src/status_im2/common/scroll_page/style.cljs index c1346529eb..5ce94a92e1 100644 --- a/src/status_im2/common/scroll_page/style.cljs +++ b/src/status_im2/common/scroll_page/style.cljs @@ -1,29 +1,26 @@ (ns status-im2.common.scroll-page.style (:require [quo2.foundations.colors :as colors] - [react-native.platform :as platform])) + [react-native.platform :as platform] + [react-native.reanimated :as reanimated])) (defn image-slider - [height] + [size] {:top (if platform/ios? 0 -64) - ;; -64 is needed on android as the scroll doesn't - ;; bounce so this slider won't disapear otherwise - :height height + :height size + :width size :z-index 4 :flex 1}) (defn blur-slider - [height] - {:blur-amount 32 - :blur-type :xlight - :overlay-color (if platform/ios? colors/white-opa-70 :transparent) - :style {:z-index 5 - :top (if platform/ios? 0 -64) - ;; -64 is needed on android as the scroll doesn't - ;; bounce so this slider won't disapear otherwise - :position :absolute - :height height - :width "100%" - :flex 1}}) + [animation] + (reanimated/apply-animations-to-style + {:transform [{:translateY animation}]} + {:z-index 5 + :top 0 + :position :absolute + :height (if platform/ios? 100 124) + :width "100%" + :flex 1})) (defn scroll-view-container [border-radius] @@ -31,4 +28,43 @@ :top -48 :overflow :scroll :border-radius border-radius - :height "100%"}) \ No newline at end of file + :height "100%"}) + +(defn sticky-header-title + [animation] + (reanimated/apply-animations-to-style + {:opacity animation} + {:position :absolute + :flex-direction :row + :left 64 + :top 16})) + +(def sticky-header-image + {:border-radius 12 + :border-width 0 + :border-color :transparent + :width 24 + :height 24 + :margin-right 8}) + +(defn display-picture-container + [animation] + (reanimated/apply-animations-to-style + {:transform [{:scale animation}]} + {:border-radius 40 + :border-width 1 + :border-color colors/white + :position :absolute + :top -40 + :left 17 + :padding 2 + :background-color (colors/theme-colors + colors/white + colors/neutral-90)})) + +(def display-picture + {:border-radius 50 + :border-width 0 + :border-color :transparent + :width 80 + :height 80}) diff --git a/src/status_im2/common/scroll_page/view.cljs b/src/status_im2/common/scroll_page/view.cljs index bb852f9c5c..c2e6ffcd9e 100644 --- a/src/status_im2/common/scroll_page/view.cljs +++ b/src/status_im2/common/scroll_page/view.cljs @@ -3,11 +3,11 @@ [quo2.core :as quo] [quo2.foundations.colors :as colors] [react-native.core :as rn] - [react-native.blur :as blur] [react-native.platform :as platform] [reagent.core :as reagent] [status-im2.common.scroll-page.style :as style] - [utils.re-frame :as rf])) + [utils.re-frame :as rf] + [react-native.reanimated :as reanimated])) (defn icon-color [] @@ -15,24 +15,8 @@ colors/white-opa-40 colors/neutral-80-opa-40)) -(defn get-platform-value [value] (if platform/ios? (+ value 44) value)) (def negative-scroll-position-0 (if platform/ios? -44 0)) (def scroll-position-0 (if platform/ios? 44 0)) -(def scroll-position-1 (if platform/ios? 86 134)) -(def scroll-position-2 (if platform/ios? -26 18)) - -(defn get-header-size - [scroll-height] - (if (<= scroll-height scroll-position-2) - 0 - (->> - (+ (get-platform-value -17) scroll-height) - (* (if platform/ios? 3 1)) - (max 0) - (min (if platform/ios? 100 124))))) - -(def max-image-size 80) -(def min-image-size 32) (defn diff-with-max-min [value maximum minimum] @@ -42,54 +26,97 @@ (max minimum) (min maximum))) -(defn icon-top-fn - [scroll-height] - (if (<= scroll-height negative-scroll-position-0) - -40 - (->> (+ scroll-position-0 scroll-height) - (* (if platform/ios? 3 1)) - (+ -40) - (min 8)))) +(defn scroll-page-header + [scroll-height name page-nav cover sticky-header] + [:f> + (fn [] + (let [input-range (if platform/ios? [-47 10] [0 150]) + output-range (if platform/ios? [-100 0] [-169 -45]) + y (reanimated/use-shared-value @scroll-height) + translate-animation (reanimated/interpolate y + input-range + output-range + {:extrapolateLeft "clamp" + :extrapolateRight "clamp"}) + opacity-animation (reanimated/use-shared-value 0) + threshold (if platform/ios? 30 170)] + (rn/use-effect + #(do + (reanimated/set-shared-value y @scroll-height) + (reanimated/set-shared-value opacity-animation + (reanimated/with-timing (if (>= @scroll-height threshold) 1 0) + (clj->js {:duration 300})))) + [@scroll-height]) + [:<> + [reanimated/blur-view + {:blur-amount 32 + :blur-type :xlight + :overlay-color (if platform/ios? colors/white-opa-70 :transparent) + :style (style/blur-slider translate-animation)}] + [rn/view + {:style {:z-index 6 + :margin-top (if platform/ios? 44 0)}} + [reanimated/view + {:style (style/sticky-header-title opacity-animation)} + [rn/image + {:source cover + :style style/sticky-header-image}] + [quo/text + {:size :paragraph-1 + :weight :semi-bold + :style {:line-height 21}} + name]] + [quo/page-nav + {:horizontal-description? true + :one-icon-align-left? true + :align-mid? false + :page-nav-color :transparent + :mid-section {:type :text-with-description + :main-text nil + :description-img nil} + :right-section-buttons (:right-section-buttons page-nav) + :left-section {:icon :i/close + :icon-background-color (icon-color) + :on-press #(rf/dispatch [:navigate-back])}}] + (when sticky-header [sticky-header @scroll-height])]]))]) -(defn icon-size-fn - [scroll-height] - (->> (+ scroll-position-0 scroll-height) - (* (if platform/ios? 3 1)) - (- max-image-size) - (max min-image-size) - (min max-image-size))) +(defn display-picture + [scroll-height cover] + [:f> + (fn [] + (let [input-range (if platform/ios? [-67 10] [0 150]) + y (reanimated/use-shared-value @scroll-height) + animation (reanimated/interpolate y + input-range + [1.2 0.5] + {:extrapolateLeft "clamp" + :extrapolateRight "clamp"})] + (rn/use-effect #(do + (reanimated/set-shared-value y @scroll-height) + js/undefined) + [@scroll-height]) + [reanimated/view + {:style (style/display-picture-container animation)} + [rn/image + {:source cover + :style style/display-picture}]]))]) (defn scroll-page - [icon cover page-nav name] + [cover page-nav name] (let [scroll-height (reagent/atom negative-scroll-position-0)] (fn [sticky-header children] [:<> - [:<> - [rn/image - {:source cover - :position :absolute - :style (style/image-slider (get-header-size @scroll-height))}] - [blur/view (style/blur-slider (get-header-size @scroll-height))]] - [rn/view {:style {:z-index 6 :margin-top (if platform/ios? 44 0)}} - [quo/page-nav - {:horizontal-description? true - :one-icon-align-left? true - :align-mid? false - :page-nav-color :transparent - :page-nav-background-uri "" - :mid-section {:type :text-with-description - :main-text (when (>= @scroll-height scroll-position-1) name) - :description-img (when (>= @scroll-height scroll-position-1) icon)} - :right-section-buttons (:right-section-buttons page-nav) - :left-section {:icon :i/close - :icon-background-color (icon-color) - :on-press #(rf/dispatch [:navigate-back])}}] - (when sticky-header [sticky-header @scroll-height])] + [scroll-page-header scroll-height name page-nav cover sticky-header] [rn/scroll-view - {:style (style/scroll-view-container (diff-with-max-min @scroll-height 16 0)) + {:style (style/scroll-view-container + (diff-with-max-min @scroll-height 16 0)) :shows-vertical-scroll-indicator false - :scroll-event-throttle 4 - :on-scroll #(swap! scroll-height (fn [] (int (oops/oget % "nativeEvent.contentOffset.y"))))} + :scroll-event-throttle 16 + :on-scroll (fn [event] + (reset! scroll-height (int + (oops/oget + event + "nativeEvent.contentOffset.y"))))} [rn/view {:style {:height 151}} [rn/image {:source cover @@ -102,5 +129,5 @@ :background-color (colors/theme-colors colors/white colors/neutral-90)} - [children @scroll-height icon-top-fn icon-size-fn]])]]))) - + [display-picture scroll-height cover] + [children]])]]))) diff --git a/src/status_im2/contexts/communities/overview/view.cljs b/src/status_im2/contexts/communities/overview/view.cljs index d7251501b9..daa2eb544d 100644 --- a/src/status_im2/contexts/communities/overview/view.cljs +++ b/src/status_im2/contexts/communities/overview/view.cljs @@ -226,22 +226,9 @@ channel-heights first-channel-height] (let [pending? (pos? requested-to-join-at) thumbnail-image (get-in images [:thumbnail])] - (fn [scroll-height icon-top icon-size] + (fn [] [rn/view [rn/view {:padding-horizontal 20} - [rn/view - {:border-radius 40 - :border-width 1 - :border-color colors/white - :position :absolute - :top (icon-top scroll-height) - :left 17 - :padding 2 - :background-color (colors/theme-colors - colors/white - colors/neutral-90)} - [quo/community-icon community - (icon-size scroll-height)]] (when (and (not joined) (not pending?) (= status :gated)) @@ -324,7 +311,6 @@ (let [channel-heights (reagent/atom []) first-channel-height (reagent/atom 0) scroll-component (scroll-page/scroll-page - (fn [] [quo/community-icon community 24]) {:uri (get-in images [:large :uri])} {:right-section-buttons [{:icon :i/search :background-color (scroll-page/icon-color)}