Animated Header Flatlist (#14925)

* feat: animated header list (quo2)
This commit is contained in:
Omar Basem 2023-02-01 10:40:52 +04:00 committed by GitHub
parent a38ef22ca7
commit fb9309f700
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 276 additions and 2 deletions

View File

@ -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}))

View File

@ -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))}]]))]))])

View File

@ -12,4 +12,4 @@
quo2.colors/neutral-10
quo2.colors/neutral-80)
:align-self :stretch
:height 1})}])
:height 1})}])

View File

@ -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

View File

@ -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])

View File

@ -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}