diff --git a/src/quo2/components/animated_header_flatlist/style.cljs b/src/quo2/components/animated_header_flatlist/style.cljs new file mode 100644 index 0000000000..295ec03177 --- /dev/null +++ b/src/quo2/components/animated_header_flatlist/style.cljs @@ -0,0 +1,74 @@ +(ns quo2.components.animated-header-flatlist.style + (:require [quo2.foundations.colors :as colors] + [react-native.reanimated :as reanimated])) + +(defn container-view + [view-height] + {:position :absolute + :top 0 + :left 0 + :right 0 + ;; height must be set, otherwise list will not scroll + :height view-height}) + +(defn button-container + [position] + (merge + {:width 32 + :height 32 + :border-radius 10 + :justify-content :center + :align-items :center + :background-color (colors/theme-colors colors/white-opa-40 colors/neutral-80-opa-40) + :position :absolute + :top 56 + :z-index 3} + position)) + +(defn blur-view + [animation] + (reanimated/apply-animations-to-style + {:opacity animation} + {:position :absolute + :top 0 + :left 0 + :right 0 + :height 100 + :z-index 2 + :overflow :hidden})) + +(defn entity-picture + [animation] + (reanimated/apply-animations-to-style + {:width animation + :height animation} + {:transform [{:scale 1}] + :border-radius 40 + :position :absolute + :bottom 42 + :left 20 + :justify-content :center + :align-items :center + :background-color (colors/theme-colors colors/white colors/neutral-100) + :overflow :hidden})) + +(defn header-bottom-part + [animation] + (reanimated/apply-animations-to-style + {:border-top-right-radius animation + :border-top-left-radius animation} + {:position :absolute + :bottom 0 + :height 86 + :left 0 + :right 0 + :background-color (colors/theme-colors colors/white colors/neutral-100)})) + +(defn header-comp + [y-animation opacity-animation] + (reanimated/apply-animations-to-style + ;; here using `left` won't work on Android, so we are using `translateX` + {:transform [{:translateX (reanimated/use-shared-value 64)} {:translateY y-animation}] + :opacity opacity-animation} + {:position :absolute + :z-index 3})) diff --git a/src/quo2/components/animated_header_flatlist/view.cljs b/src/quo2/components/animated_header_flatlist/view.cljs new file mode 100644 index 0000000000..27069d5ce1 --- /dev/null +++ b/src/quo2/components/animated_header_flatlist/view.cljs @@ -0,0 +1,100 @@ +(ns quo2.components.animated-header-flatlist.view + (:require + [quo2.core :as quo] + [react-native.core :as rn] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [quo2.foundations.colors :as colors] + [status-im.ui.components.fast-image :as fast-image] + [quo2.components.animated-header-flatlist.style :as style] + [oops.core :as oops] + [utils.re-frame :as rf])) + +(def header-height 234) +(def cover-height 192) +(def blur-view-height 100) +(def threshold (- header-height blur-view-height)) + +(defn interpolate + [value input-range output-range] + (reanimated/interpolate value + input-range + output-range + {:extrapolateLeft "clamp" + :extrapolateRight "clamp"})) + +(defn scroll-handler + [event initial-y scroll-y] + (let [current-y (- (oops/oget event "nativeEvent.contentOffset.y") initial-y)] + (reanimated/set-shared-value scroll-y current-y))) + +(defn header + [{:keys [theme-color display-picture-comp cover-uri title-comp]} top-inset scroll-y] + (let [input-range [0 (* threshold 0.33)] + picture-scale-down 0.4 + size-animation (interpolate scroll-y input-range [80 (* 80 picture-scale-down)]) + image-animation (interpolate scroll-y input-range [72 (* 72 picture-scale-down)]) + border-animation (interpolate scroll-y input-range [12 0])] + [rn/view + {:style {:height header-height + :background-color (or theme-color (colors/theme-colors colors/white colors/neutral-95)) + :margin-top (when platform/ios? (- top-inset))}} + (when cover-uri + [fast-image/fast-image + {:style {:width "100%" + :height cover-height} + :source {:uri cover-uri}}]) + [reanimated/view {:style (style/header-bottom-part border-animation)} + [title-comp]] + [reanimated/view {:style (style/entity-picture size-animation)} + [display-picture-comp image-animation]]])) + + + +(defn animated-header-list + [{:keys [header-comp main-comp] :as parameters}] + [safe-area/consumer + (fn [insets] + (let [window-height (:height (rn/get-window)) + status-bar-height (rn/status-bar-height) + bottom-inset (:bottom insets) + ;; view height calculation is different because window height is different on iOS and Android: + view-height (if platform/ios? + (- window-height bottom-inset) + (+ window-height status-bar-height)) + initial-y (if platform/ios? (- (:top insets)) 0)] + [:f> + (fn [] + (let [scroll-y (reanimated/use-shared-value initial-y) + opacity-animation (interpolate scroll-y + [(* threshold 0.33) (* threshold 0.66)] + [0 1]) + translate-animation (interpolate scroll-y [(* threshold 0.66) threshold] [100 56]) + title-opacity-animation (interpolate scroll-y [(* threshold 0.66) threshold] [0 1])] + [rn/view {:style (style/container-view view-height)} + [rn/touchable-opacity + {:active-opacity 1 + :on-press #(rf/dispatch [:navigate-back]) + :style (style/button-container {:left 20})} + [quo/icon :i/arrow-left {:size 20 :color (colors/theme-colors colors/black colors/white)}]] + [rn/touchable-opacity + {:active-opacity 1 + :style (style/button-container {:right 20})} + [quo/icon :i/options {:size 20 :color (colors/theme-colors colors/black colors/white)}]] + [reanimated/blur-view + {:blurAmount 32 + :blurType :light + :overlayColor (if platform/ios? colors/white-opa-70 :transparent) + :style (style/blur-view opacity-animation)} + [reanimated/view {:style (style/header-comp translate-animation title-opacity-animation)} + [header-comp]]] + [reanimated/flat-list + {:data [nil] + :render-fn main-comp + :key-fn str + :header (reagent/as-element (header parameters (:top insets) scroll-y)) + ;; TODO: https://github.com/status-im/status-mobile/issues/14924 + :scroll-event-throttle 8 + :on-scroll (fn [event] (scroll-handler event initial-y scroll-y))}]]))]))]) diff --git a/src/quo2/components/separator.cljs b/src/quo2/components/separator.cljs index a7520c09eb..65158c2076 100644 --- a/src/quo2/components/separator.cljs +++ b/src/quo2/components/separator.cljs @@ -12,4 +12,4 @@ quo2.colors/neutral-10 quo2.colors/neutral-80) :align-self :stretch - :height 1})}]) \ No newline at end of file + :height 1})}]) diff --git a/src/react_native/reanimated.cljs b/src/react_native/reanimated.cljs index 93f4f70975..4f8d6b750b 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-fast-image" :as FastImage] ["@react-native-community/blur" :as blur] ["react-native-reanimated" :default reanimated :refer (useSharedValue useAnimatedStyle @@ -15,6 +16,7 @@ SlideOutUp LinearTransition)] [reagent.core :as reagent] + [react-native.flat-list :as rn-flat-list] [utils.collection])) ;; Animations @@ -27,9 +29,15 @@ (def view (reagent/adapt-react-class (.-View reanimated))) (def image (reagent/adapt-react-class (.-Image reanimated))) +(def reanimated-flat-list (reagent/adapt-react-class (.-FlatList ^js rn))) +(defn flat-list + [props] + [reanimated-flat-list (rn-flat-list/base-list-props props)]) + (def touchable-opacity (create-animated-component (.-TouchableOpacity ^js rn))) (def linear-gradient (create-animated-component LinearGradient)) +(def fast-image (create-animated-component FastImage)) (def blur-view (create-animated-component (.-BlurView blur))) ;; Hooks diff --git a/src/status_im2/contexts/quo_preview/animated_header_list/animated_header_list.cljs b/src/status_im2/contexts/quo_preview/animated_header_list/animated_header_list.cljs new file mode 100644 index 0000000000..ba24eed700 --- /dev/null +++ b/src/status_im2/contexts/quo_preview/animated_header_list/animated_header_list.cljs @@ -0,0 +1,88 @@ +(ns status-im2.contexts.quo-preview.animated-header-list.animated-header-list + (:require + [quo2.core :as quo] + [quo2.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.fast-image :as fast-image] + [react-native.reanimated :as reanimated] + [quo2.components.animated-header-flatlist.view :as animated-header-list])) + +(def data [0 1 2 3 4 5 6 7 8 9 10]) + +(defn child + [] + [rn/view + {:style {:height 100 + :background-color colors/primary-50-opa-40 + :margin 10 + :justify-content :center + :padding-left 10}} + [quo/text "This is some message!!!!!"]]) + + +(defn main-comp + [] + [rn/flat-list + {:data data + :render-fn child + :key-fn (fn [item] (str item)) + :header + [rn/view + {:style {:height 70 + :padding-top 8 + :padding-horizontal 20}} + [quo/text {:size :paragraph-2} + "Some random description • Developer • Designer • Olympic gold winner • President • Super Hero"]]}]) + +(defn display-picture-comp + [animation] + [:f> + (fn [] + [reanimated/fast-image + {:style (reanimated/apply-animations-to-style + {:width animation + :height animation} + {:border-radius 72}) + :source + {:uri + "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"}}])]) + + + +(defn header-comp + [] + [rn/view + {:style {:flex-direction :row + :justify-content :center + :align-items :center}} + [fast-image/fast-image + {:source {:uri + "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"} + :style {:width 32 + :height 32 + :border-radius 16 + :margin-right 8}}] + [quo/text {:weight :semi-bold} "Alecia Keys"]]) + +(defn title-comp + [] + [quo/text + {:weight :semi-bold + :size :heading-1 + :style {:margin-top 56 + :margin-left 20}} "Alicia Keys"]) + +(def theme-color (colors/theme-alpha "#5BCC95" 0.2 0.2)) + +(def parameters + {:theme-color theme-color + :cover-uri + "https://hips.hearstapps.com/hmg-prod.s3.amazonaws.com/images/kitten-playing-with-toy-mouse-royalty-free-image-590055188-1542816918.jpg?crop=1.00xw:0.758xh;0,0.132xh&resize=480:*" + :display-picture-comp display-picture-comp + :header-comp header-comp + :title-comp title-comp + :main-comp main-comp}) + +(defn mock-screen + [] + [animated-header-list/animated-header-list parameters]) diff --git a/src/status_im2/contexts/quo_preview/main.cljs b/src/status_im2/contexts/quo_preview/main.cljs index 9acbe30b76..6bf052b118 100644 --- a/src/status_im2/contexts/quo_preview/main.cljs +++ b/src/status_im2/contexts/quo_preview/main.cljs @@ -68,12 +68,16 @@ [status-im2.contexts.quo-preview.wallet.lowest-price :as lowest-price] [status-im2.contexts.quo-preview.wallet.network-amount :as network-amount] [status-im2.contexts.quo-preview.wallet.network-breakdown :as network-breakdown] - [status-im2.contexts.quo-preview.wallet.token-overview :as token-overview])) + [status-im2.contexts.quo-preview.wallet.token-overview :as token-overview] + [status-im2.contexts.quo-preview.animated-header-list.animated-header-list :as animated-header-list])) (def screens-categories {:foundations [{:name :shadows :insets {:top false} :component shadows/preview-shadows}] + :animated-list [{:name :animated-header-list + :options {:topBar {:visible false}} + :component animated-header-list/mock-screen}] :avatar [{:name :group-avatar :insets {:top false} :component group-avatar/preview-group-avatar}