Implement collectible header with animations (#20024)

Co-authored-by: Ajay Sivan <ajayesivan@gmail.com>
This commit is contained in:
Ulises Manuel 2024-05-16 22:16:17 -06:00 committed by GitHub
parent b76395282d
commit e1408f2a5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 244 additions and 70 deletions

View File

@ -0,0 +1,25 @@
import {
useDerivedValue,
interpolate,
interpolateColor,
Extrapolation,
useAnimatedStyle,
} from 'react-native-reanimated';
const CLAMP_MIN = 0;
const CLAMP_MAX = 60;
const BLUR_MIN = 1;
const BLUR_MAX = 15;
export const useBlurAmount = (sharedValue) =>
useDerivedValue(() =>
parseInt(interpolate(sharedValue.value, [CLAMP_MIN, CLAMP_MAX], [BLUR_MIN, BLUR_MAX], Extrapolation.CLAMP)),
);
export function useLayerOpacity(sharedValue, from, to) {
return useAnimatedStyle(() => ({
flex: 1,
justifyContent: 'flex-end',
backgroundColor: interpolateColor(sharedValue.value, [0, 60], [from, to], 'RGB'),
}));
}

View File

@ -438,6 +438,7 @@
"../src/js/worklets/parallax.js" #js {} "../src/js/worklets/parallax.js" #js {}
"../src/js/worklets/profile_header.js" #js {} "../src/js/worklets/profile_header.js" #js {}
"../src/js/worklets/identifiers_highlighting.js" #js {} "../src/js/worklets/identifiers_highlighting.js" #js {}
"../src/js/worklets/header_animations.js" #js {}
"./fleets.js" default-fleets "./fleets.js" default-fleets
"../translations/ar.json" (js/JSON.parse (slurp "./translations/ar.json")) "../translations/ar.json" (js/JSON.parse (slurp "./translations/ar.json"))
"../translations/de.json" (js/JSON.parse (slurp "./translations/de.json")) "../translations/de.json" (js/JSON.parse (slurp "./translations/de.json"))

View File

@ -1,13 +1,10 @@
(ns quo.components.profile.expanded-collectible.style (ns quo.components.profile.expanded-collectible.style
(:require [quo.foundations.colors :as colors] (:require [quo.foundations.colors :as colors]))
[quo.foundations.shadows :as shadows]))
(defn container (def container
[theme]
(merge (shadows/get 2 theme)
{:align-items :center {:align-items :center
:justify-content :center :justify-content :center
:border-radius 16})) :border-radius 16})
(defn image (defn image
[square? aspect-ratio] [square? aspect-ratio]

View File

@ -34,7 +34,8 @@
label]]) label]])
(defn view-internal (defn view-internal
[{:keys [container-style square? on-press counter image-src native-ID supported-file?]}] [{:keys [container-style square? on-press counter image-src native-ID supported-file?
on-collectible-load]}]
(let [theme (quo.theme/use-theme) (let [theme (quo.theme/use-theme)
[image-size set-image-size] (rn/use-state {}) [image-size set-image-size] (rn/use-state {})
[image-error? set-image-error] (rn/use-state false)] [image-error? set-image-error] (rn/use-state false)]
@ -48,7 +49,7 @@
[rn/pressable [rn/pressable
{:on-press (when (and (not image-error?) supported-file?) on-press) {:on-press (when (and (not image-error?) supported-file?) on-press)
:accessibility-label :expanded-collectible :accessibility-label :expanded-collectible
:style (merge container-style (style/container theme))} :style (merge container-style style/container)}
(cond (cond
(not supported-file?) (not supported-file?)
[fallback-view [fallback-view
@ -68,7 +69,8 @@
{:style (style/image square? (:aspect-ratio image-size)) {:style (style/image square? (:aspect-ratio image-size))
:source image-src :source image-src
:native-ID native-ID :native-ID native-ID
:on-error #(set-image-error true)}] :on-error #(set-image-error true)
:on-load on-collectible-load}]
[counter-view counter]])])) [counter-view counter]])]))
(def ?schema (def ?schema
@ -82,7 +84,8 @@
[:native-ID {:optional true} [:maybe [:or string? keyword?]]] [:native-ID {:optional true} [:maybe [:or string? keyword?]]]
[:square? {:optional true} [:maybe boolean?]] [:square? {:optional true} [:maybe boolean?]]
[:counter {:optional true} [:maybe string?]] [:counter {:optional true} [:maybe string?]]
[:on-press {:optional true} [:maybe fn?]]]]] [:on-press {:optional true} [:maybe fn?]]
[:on-collectible-load {:optional true} [:maybe fn?]]]]]
:any]) :any])
(def view (schema/instrument #'view-internal ?schema)) (def view (schema/instrument #'view-internal ?schema))

View File

@ -108,7 +108,7 @@
children] children]
(let [theme (quo.theme/use-theme)] (let [theme (quo.theme/use-theme)]
[:<> [:<>
[:f> f-scroll-page-header [f-scroll-page-header
{:scroll-height @scroll-height {:scroll-height @scroll-height
:height height :height height
:sticky-header sticky-header :sticky-header sticky-header

View File

@ -1,17 +1,15 @@
(ns status-im.contexts.wallet.collectible.style) (ns status-im.contexts.wallet.collectible.style
(:require [quo.foundations.colors :as colors]
[react-native.core :as rn]
[react-native.platform :as platform]))
(def container (def container
{:margin-top 100 {:margin-bottom 34})
:margin-bottom 34})
(def preview-container (def preview-container
{:margin-horizontal 8 {:margin-horizontal 8
:margin-top 12}) :margin-top 12
:padding-top 100})
(def preview
{:width "100%"
:aspect-ratio 1
:border-radius 16})
(def header (def header
{:margin-horizontal 20 {:margin-horizontal 20
@ -43,3 +41,43 @@
(def opensea-button (def opensea-button
{:flex 1 {:flex 1
:margin-left 6}) :margin-left 6})
(def animated-header
{:position :absolute
:top 0
:left 0
:right 0
:height 100
:z-index 1
:overflow :hidden})
(defn scroll-view
[safe-area-top]
{:flex 1
:margin-top (when platform/ios? (- safe-area-top))})
(def gradient-layer
{:position :absolute
:top 0
:left 0
:right 0
:bottom 0
:flex 1
:align-items :center
:overflow :hidden})
(def image-background
{:height (:height (rn/get-window))
:aspect-ratio 1})
(def gradient
{:position :absolute
:top 0
:left 0
:right 0
:bottom 0})
(defn background-color
[theme]
{:flex 1
:background-color (colors/theme-colors colors/white colors/neutral-95 theme)})

View File

@ -1,21 +1,29 @@
(ns status-im.contexts.wallet.collectible.view (ns status-im.contexts.wallet.collectible.view
(:require (:require
[oops.core :as oops]
[quo.core :as quo] [quo.core :as quo]
[quo.foundations.colors :as colors] [quo.foundations.colors :as colors]
[quo.theme :as quo.theme] [quo.theme :as quo.theme]
[react-native.core :as rn] [react-native.core :as rn]
[react-native.linear-gradient :as linear-gradient]
[react-native.platform :as platform]
[react-native.reanimated :as reanimated]
[react-native.safe-area :as safe-area]
[reagent.core :as reagent] [reagent.core :as reagent]
[status-im.common.scroll-page.view :as scroll-page]
[status-im.contexts.wallet.collectible.options.view :as options-drawer] [status-im.contexts.wallet.collectible.options.view :as options-drawer]
[status-im.contexts.wallet.collectible.style :as style] [status-im.contexts.wallet.collectible.style :as style]
[status-im.contexts.wallet.collectible.tabs.view :as tabs] [status-im.contexts.wallet.collectible.tabs.view :as tabs]
[status-im.contexts.wallet.collectible.utils :as utils] [status-im.contexts.wallet.collectible.utils :as utils]
[utils.i18n :as i18n] [utils.i18n :as i18n]
[utils.re-frame :as rf])) [utils.re-frame :as rf]
[utils.worklets.header-animations :as header-animations]))
(defn header (defn header
[collectible-name collection-name collection-image-url] [collectible-name collection-name collection-image-url set-title-ref]
[rn/view {:style style/header} [rn/view
{:style style/header
:ref set-title-ref
:collapsable false}
[quo/text [quo/text
{:weight :semi-bold {:weight :semi-bold
:size :heading-1} :size :heading-1}
@ -64,13 +72,74 @@
:label (i18n/label :t/about) :label (i18n/label :t/about)
:accessibility-label :about-tab}]) :accessibility-label :about-tab}])
(defn view (def navigate-back #(rf/dispatch [:navigate-back]))
(defn animated-header
[{:keys [scroll-amount title-opacity page-nav-type picture title description theme]}]
(let [blur-amount (header-animations/use-blur-amount scroll-amount)
layer-opacity (header-animations/use-layer-opacity
scroll-amount
"transparent"
(colors/theme-colors colors/white-opa-50 colors/neutral-95-opa-70-blur theme))]
[rn/view {:style style/animated-header}
[reanimated/blur-view
{:style {:flex 1}
:blur-type :transparent
:overlay-color :transparent
:blur-amount blur-amount
:blur-radius blur-amount}
[reanimated/view {:style layer-opacity}
[quo/page-nav
{:type page-nav-type
:picture picture
:title title
:description description
:background :blur
:icon-name :i/close
:accessibility-label :back-button
:on-press navigate-back
:right-side [{:icon-name :i/options
:on-press #(rf/dispatch
[:show-bottom-sheet
{:content (fn []
[options-drawer/view
{:name title
:image picture}])
:theme theme}])}]
:center-opacity title-opacity}]]]]))
(defn on-scroll
[e scroll-amount title-opacity title-bottom-coord]
(let [scroll-y (oops/oget e "nativeEvent.contentOffset.y")
new-opacity (if (>= scroll-y @title-bottom-coord) 1 0)]
(reanimated/set-shared-value scroll-amount scroll-y)
(reanimated/set-shared-value title-opacity
(reanimated/with-timing new-opacity #js {:duration 300}))))
(defn- gradient-layer
[image-uri]
(let [theme (quo.theme/use-theme)]
[rn/view {:style style/gradient-layer}
[rn/image
{:style style/image-background
:source {:uri image-uri}
:blur-radius 14}]
[rn/view {:style style/gradient}
[linear-gradient/linear-gradient
{:style {:flex 1}
:colors (colors/theme-colors
[colors/white-opa-70 colors/white colors/white]
[colors/neutral-95-opa-70 colors/neutral-95 colors/neutral-95]
theme)
:locations [0 0.9 1]}]]]))
(defn collectible-details
[_] [_]
(let [selected-tab (reagent/atom :overview) (let [selected-tab (reagent/atom :overview)
on-tab-change #(reset! selected-tab %)] on-tab-change #(reset! selected-tab %)]
(fn [] (fn [{:keys [collectible set-title-bottom theme]}]
(let [theme (quo.theme/use-theme) (let [title-ref (rn/use-ref-atom nil)
collectible (rf/sub [:wallet/last-collectible-details]) set-title-ref (rn/use-callback #(reset! title-ref %))
animation-shared-element-id (rf/sub [:animation-shared-element-id]) animation-shared-element-id (rf/sub [:animation-shared-element-id])
collectible-owner (rf/sub [:wallet/last-collectible-details-owner]) collectible-owner (rf/sub [:wallet/last-collectible-details-owner])
{:keys [id {:keys [id
@ -95,25 +164,12 @@
:id token-id :id token-id
:header collectible-name :header collectible-name
:description collection-name} :description collection-name}
total-owned (utils/total-owned-collectible (:ownership collectible) total-owned (utils/total-owned-collectible
(:ownership collectible)
(:address collectible-owner))] (:address collectible-owner))]
(rn/use-unmount #(rf/dispatch [:wallet/clear-last-collectible-details]))
[scroll-page/scroll-page
{:navigate-back? true
:height 148
:page-nav-props {:type :title-description
:title collectible-name
:description collection-name
:right-side [{:icon-name :i/options
:on-press #(rf/dispatch
[:show-bottom-sheet
{:content (fn [] [options-drawer/view
{:name collectible-name
:image preview-uri}])
:theme theme}])}]
:picture preview-uri
:blur? true}}
[rn/view {:style style/container} [rn/view {:style style/container}
[rn/view
[gradient-layer preview-uri]
[quo/expanded-collectible [quo/expanded-collectible
{:image-src preview-uri {:image-src preview-uri
:container-style style/preview-container :container-style style/preview-container
@ -133,13 +189,22 @@
(fn [] (fn []
[options-drawer/view [options-drawer/view
{:name collectible-name {:name collectible-name
:image preview-uri}])}])}])))}] :image
[header collectible-name collection-name collection-image] preview-uri}])}])}])))
:on-collectible-load (fn []
;; We need to delay the measurement because the
;; navigation has an animation
(js/setTimeout
#(some-> @title-ref
(oops/ocall "measureInWindow" set-title-bottom))
300))}]]
[rn/view {:style (style/background-color theme)}
[header collectible-name collection-name collection-image set-title-ref]
[cta-buttons [cta-buttons
{:chain-id chain-id {:chain-id chain-id
:token-id token-id :token-id token-id
:contract-address contract-address :contract-address contract-address
:watch-only? false ;(:watch-only? collectible-owner) :watch-only? (:watch-only? collectible-owner)
:collectible collectible}] :collectible collectible}]
[quo/tabs [quo/tabs
{:size 32 {:size 32
@ -149,3 +214,41 @@
:on-change on-tab-change :on-change on-tab-change
:data tabs-data}] :data tabs-data}]
[tabs/view {:selected-tab @selected-tab}]]])))) [tabs/view {:selected-tab @selected-tab}]]]))))
(defn view
[_]
(let [{:keys [top]} (safe-area/get-insets)
theme (quo.theme/use-theme)
title-bottom-coord (rn/use-ref-atom 0)
set-title-bottom (rn/use-callback
(fn [_ y _ height]
(reset! title-bottom-coord
(+ y height -100 (if platform/ios? (- top) top)))))
scroll-amount (reanimated/use-shared-value 0)
title-opacity (reanimated/use-shared-value 0)
collectible (rf/sub [:wallet/last-collectible-details])
{:keys [preview-url
collection-data
collectible-data]} collectible
{preview-uri :uri} preview-url
{collectible-name :name} collectible-data
{collection-name :name} collection-data]
(rn/use-unmount #(rf/dispatch [:wallet/clear-last-collectible-details]))
[rn/view {:style (style/background-color theme)}
[animated-header
{:scroll-amount scroll-amount
:title-opacity title-opacity
:page-nav-type :title-description
:picture preview-uri
:title collectible-name
:description collection-name
:theme theme}]
[reanimated/scroll-view
{:style (style/scroll-view top)
:on-scroll #(on-scroll % scroll-amount title-opacity title-bottom-coord)}
[collectible-details
{:collectible collectible
:set-title-bottom set-title-bottom
:theme theme}]]]))

View File

@ -0,0 +1,7 @@
(ns utils.worklets.header-animations)
(def worklets (js/require "../src/js/worklets/header_animations.js"))
(def use-blur-amount (.-useBlurAmount worklets))
(def use-layer-opacity (.-useLayerOpacity worklets))