#16111 - Toast animation & dismiss gesture

* Enable toast animations and refactor

* Add gesture-handler-root-HOC to detect toast's gestures on Android

* Add comment about flex 0 style
This commit is contained in:
Ulises Manuel Cárdenas 2023-07-04 16:25:08 -06:00 committed by GitHub
parent 2b4b357c32
commit ca0915c940
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 113 additions and 83 deletions

View File

@ -21,8 +21,7 @@
(defn toast-undo-action (defn toast-undo-action
[duration on-press override-theme] [duration on-press override-theme]
[toast-action-container [toast-action-container {:on-press on-press :accessibility-label :toast-undo-action}
{:on-press on-press :accessibility-label :toast-undo-action}
[rn/view {:style {:margin-right 5}} [rn/view {:style {:margin-right 5}}
[count-down-circle/circle-timer {:duration duration}]] [count-down-circle/circle-timer {:duration duration}]]
[text/text [text/text
@ -31,17 +30,17 @@
(defn- toast-container (defn- toast-container
[{:keys [left title text right container-style override-theme]}] [{:keys [left title text right container-style override-theme]}]
[rn/view [rn/view {:style (merge style/box-container container-style)}
{:style (merge style/box-container container-style)}
[blur/view [blur/view
{:style style/blur-container {:style style/blur-container
:blur-amount 13 :blur-amount 13
:blur-radius 10 :blur-radius 10
:blur-type :transparent :blur-type :transparent
:overlay-color :transparent}] :overlay-color :transparent}]
[rn/view
{:style (style/content-container override-theme)} [rn/view {:style (style/content-container override-theme)}
[rn/view {:style style/left-side-container} left] [rn/view {:style style/left-side-container}
left]
[rn/view {:style style/right-side-container} [rn/view {:style style/right-side-container}
(when title (when title
[text/text [text/text
@ -57,7 +56,7 @@
:style (style/text override-theme) :style (style/text override-theme)
:accessibility-label :toast-content} :accessibility-label :toast-content}
text])] text])]
(when right right)]]) right]])
(defn toast (defn toast
[{:keys [icon icon-color title text action undo-duration undo-on-press container-style [{:keys [icon icon-color title text action undo-duration undo-on-press container-style

View File

@ -0,0 +1,57 @@
(ns status-im2.common.toasts.animation
(:require [react-native.gesture :as gesture]
[react-native.reanimated :as reanimated]))
(def slide-out-up-animation
(-> ^js reanimated/slide-out-up-animation
(.springify)
(.damping 20)
(.stiffness 12)))
(def slide-in-up-animation
(-> ^js reanimated/slide-in-up-animation
(.springify)
(.damping 20)
(.stiffness 150)))
(def linear-transition
(-> ^js reanimated/linear-transition
.springify
(.damping 20)
(.stiffness 170)))
(defn- reset-translate-y
([translate-y]
(reset-translate-y translate-y 0))
([translate-y spring-value]
(reanimated/animate-shared-value-with-spring
translate-y
spring-value
{:mass 1 :damping 20 :stiffness 300})))
(defn- dismiss
[translate-y set-dismissed-locally close-toast]
(reset-translate-y translate-y -500)
(set-dismissed-locally)
(close-toast))
(defn on-update-gesture
[translate-y set-dismissed-locally close-toast]
(fn [^js evt]
(let [evt-translation-y (.-translationY evt)
pan-down? (> evt-translation-y 100)
pan-up? (< evt-translation-y -30)]
(cond
pan-down? (reset-translate-y translate-y)
pan-up? (dismiss translate-y set-dismissed-locally close-toast)
:else (reanimated/set-shared-value translate-y evt-translation-y)))))
(defn pan-gesture
[{:keys [clear-timer create-timer translate-y close-toast set-dismissed-locally
dismissed-locally?]}]
(-> (gesture/gesture-pan)
(gesture/on-start clear-timer)
(gesture/on-update (on-update-gesture translate-y set-dismissed-locally close-toast))
(gesture/on-end #(when-not dismissed-locally?
(reanimated/set-shared-value translate-y 0)
(create-timer)))))

View File

@ -1,13 +1,12 @@
(ns status-im2.common.toasts.style) (ns status-im2.common.toasts.style)
(def outmost-transparent-container (def outmost-transparent-container
{:elevation 2 {:elevation 2
:pointer-events :box-none :pointer-events :box-none
:padding-top 52 :padding-top 52
:flex-direction :column :flex-direction :column
:justify-content :center :justify-content :center
:align-items :center :align-items :center})
:background-color :transparent})
(def each-toast-container (def each-toast-container
{:width "100%" {:width "100%"

View File

@ -1,89 +1,56 @@
(ns status-im2.common.toasts.view (ns status-im2.common.toasts.view
(:require [quo2.core :as quo] (:require [quo2.core :as quo]
[react-native.background-timer :as background-timer]
[react-native.core :as rn] [react-native.core :as rn]
[react-native.gesture :as gesture] [react-native.gesture :as gesture]
[react-native.reanimated :as reanimated] [react-native.reanimated :as reanimated]
[reagent.core :as reagent] [reagent.core :as reagent]
[react-native.background-timer :as background-timer] [status-im2.common.toasts.animation :as animation]
[status-im2.common.toasts.style :as style] [status-im2.common.toasts.style :as style]
[utils.re-frame :as rf])) [utils.re-frame :as rf]))
(defn toast (defn toast
[id] [toast-id]
(let [{:keys [type] :as toast-opts} (rf/sub [:toasts/toast id])] (let [{:keys [type] :as toast-opts} (rf/sub [:toasts/toast toast-id])]
(if (= type :notification) (if (= type :notification)
[quo/notification toast-opts] [quo/notification toast-opts]
[quo/toast toast-opts]))) [quo/toast toast-opts])))
(defn reset-translate-y
[translate-y]
(reanimated/animate-shared-value-with-spring translate-y
0
{:mass 1
:damping 20
:stiffness 300}))
(defn dismiss
[translate-y dismissed-locally? close!]
(reanimated/animate-shared-value-with-spring
translate-y
-500
{:mass 1 :damping 20 :stiffness 300})
(reset! dismissed-locally? true)
(close!))
(defn f-container (defn f-container
[id] [toast-id]
(let [dismissed-locally? (reagent/atom false) (let [dismissed-locally? (reagent/atom false)
close! #(rf/dispatch [:toasts/close id]) set-dismissed-locally #(reset! dismissed-locally? true)
timer (reagent/atom nil) close-toast #(rf/dispatch [:toasts/close toast-id])
clear-timer #(background-timer/clear-timeout @timer)] timer (reagent/atom nil)
clear-timer #(background-timer/clear-timeout @timer)]
(fn [] (fn []
(let [duration (or (rf/sub [:toasts/toast-cursor id :duration]) 3000) (let [duration (or (rf/sub [:toasts/toast-cursor toast-id :duration]) 3000)
on-dismissed (or (rf/sub [:toasts/toast-cursor id :on-dismissed]) identity) on-dismissed (or (rf/sub [:toasts/toast-cursor toast-id :on-dismissed]) identity)
create-timer (fn [] create-timer #(reset! timer (background-timer/set-timeout close-toast duration))
(reset! timer (background-timer/set-timeout close! duration))) translate-y (reanimated/use-shared-value 0)
translate-y (reanimated/use-shared-value 0) pan-gesture (animation/pan-gesture {:clear-timer clear-timer
pan :create-timer create-timer
(-> :translate-y translate-y
(gesture/gesture-pan) :close-toast close-toast
(gesture/on-start clear-timer) :set-dismissed-locally set-dismissed-locally
(gesture/on-update :dismissed-locally? @dismissed-locally?})]
(fn [^js evt]
(let [evt-translation-y (.-translationY evt)
pan-down? (> evt-translation-y 100)
pan-up? (< evt-translation-y -30)]
(cond
pan-down? (reset-translate-y translate-y)
pan-up? (dismiss translate-y dismissed-locally? close!)
:else
(reanimated/set-shared-value translate-y
evt-translation-y)))))
(gesture/on-end (fn [_]
(when-not @dismissed-locally?
(reanimated/set-shared-value translate-y 0)
(create-timer)))))]
;; create auto dismiss timer, clear timer when unmount or duration changed ;; create auto dismiss timer, clear timer when unmount or duration changed
(rn/use-effect (fn [] (create-timer) clear-timer) [duration]) (rn/use-effect (fn [] (create-timer) clear-timer) [duration])
(rn/use-unmount #(on-dismissed id)) (rn/use-unmount #(on-dismissed toast-id))
[gesture/gesture-detector {:gesture pan}
[gesture/gesture-detector {:gesture pan-gesture}
[reanimated/view [reanimated/view
{;; TODO: this will enable layout animation at runtime and causing flicker on android {:entering animation/slide-in-up-animation
;; we need to resolve this and re-enable layout animation :exiting animation/slide-out-up-animation
;; issue at https://github.com/status-im/status-mobile/issues/14752 :layout animation/linear-transition
;; :entering slide-in-up-animation :style (reanimated/apply-animations-to-style
;; :exiting slide-out-up-animation {:transform [{:translateY translate-y}]}
;; :layout reanimated/linear-transition style/each-toast-container)}
:style (reanimated/apply-animations-to-style [toast toast-id]]]))))
{:transform [{:translateY translate-y}]}
style/each-toast-container)}
[toast id]]]))))
(defn toasts (defn toasts
[] []
(let [toasts-ordered (:ordered (rf/sub [:toasts]))] (->> (rf/sub [:toasts])
[into :ordered
[rn/view (into [rn/view {:style style/outmost-transparent-container}]
{:style style/outmost-transparent-container}] (map #(with-meta [:f> f-container %] {:key %})))))
(doall
(map (fn [id] ^{:key id} [:f> f-container id]) toasts-ordered))]))

View File

@ -212,7 +212,15 @@
opts)}}))) opts)}})))
;; toast ;; toast
(navigation/register-component "toasts" (fn [] views/toasts) js/undefined) (navigation/register-component "toasts"
; `:flex 0` is the same as `flex: 0 0 auto` in CSS.
; We need this to override the HOC default layout which is
; flex 1. If we don't override this property, this HOC
; will catch all touches/gestures while the toast is shown,
; preventing the user doing any action in the app
#(gesture/gesture-handler-root-hoc views/toasts
#js {:flex 0})
(fn [] views/toasts))
(re-frame/reg-fx :show-toasts (re-frame/reg-fx :show-toasts
(fn [] (fn []