From 64dde1c9d15e60fe5b043dcbdb1e5e80f37ed050 Mon Sep 17 00:00:00 2001 From: Omar Basem Date: Tue, 14 Mar 2023 08:24:14 +0400 Subject: [PATCH] feat: improve lightbox transition (#15315) * feat: improve lightbox transition --- src/react_native/reanimated.cljs | 8 +- src/react_native/safe_area.cljs | 10 +- .../contexts/chat/lightbox/animations.cljs | 21 ++ .../contexts/chat/lightbox/bottom_view.cljs | 6 +- .../contexts/chat/lightbox/common.cljs | 14 - .../contexts/chat/lightbox/style.cljs | 10 +- .../contexts/chat/lightbox/top_view.cljs | 41 +-- .../contexts/chat/lightbox/view.cljs | 62 ++-- .../chat/lightbox/zoomable_image/style.cljs | 5 +- .../chat/lightbox/zoomable_image/utils.cljs | 102 ++++-- .../chat/lightbox/zoomable_image/view.cljs | 299 ++++++------------ src/status_im2/navigation/screens.cljs | 2 + 12 files changed, 297 insertions(+), 283 deletions(-) create mode 100644 src/status_im2/contexts/chat/lightbox/animations.cljs delete mode 100644 src/status_im2/contexts/chat/lightbox/common.cljs diff --git a/src/react_native/reanimated.cljs b/src/react_native/reanimated.cljs index 7d33b627c7..2347030089 100644 --- a/src/react_native/reanimated.cljs +++ b/src/react_native/reanimated.cljs @@ -32,7 +32,11 @@ (def view (reagent/adapt-react-class (.-View reanimated))) (def scroll-view (reagent/adapt-react-class (.-ScrollView reanimated))) (def image (reagent/adapt-react-class (.-Image reanimated))) -(def reanimated-flat-list (create-animated-component (.-FlatList ^js rn))) + +;; TODO: This one should use FlatList from Reanimated. +;; Trying to use Flatlist from RA causes test to fail: "The first argument must be a component. Instead +;; received: object" +(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)]) @@ -61,6 +65,8 @@ (def in-out (.-inOut ^js Easing)) +(defn default-easing [] (in-out (.-quad ^js Easing))) + (def easings {:linear (bezier 0 0 1 1) :easing1 (bezier 0.25 0.1 0.25 1) diff --git a/src/react_native/safe_area.cljs b/src/react_native/safe_area.cljs index 33096f9eb7..02b41f31ab 100644 --- a/src/react_native/safe_area.cljs +++ b/src/react_native/safe_area.cljs @@ -1,6 +1,6 @@ (ns react-native.safe-area (:require ["react-native-safe-area-context" :as safe-area-context :refer - (SafeAreaProvider SafeAreaInsetsContext)] + (SafeAreaProvider SafeAreaInsetsContext useSafeAreaInsets)] [reagent.core :as reagent])) (def ^:private consumer-raw (reagent/adapt-react-class (.-Consumer ^js SafeAreaInsetsContext))) @@ -13,3 +13,11 @@ (fn [^js insets] (reagent/as-element [component (js->clj insets :keywordize-keys true)]))]) + +(defn use-safe-area + [] + (let [insets ^js (useSafeAreaInsets)] + {:top (.-top insets) + :bottom (.-bottom insets) + :left (.-left insets) + :right (.-right insets)})) diff --git a/src/status_im2/contexts/chat/lightbox/animations.cljs b/src/status_im2/contexts/chat/lightbox/animations.cljs new file mode 100644 index 0000000000..dd7a8a0e85 --- /dev/null +++ b/src/status_im2/contexts/chat/lightbox/animations.cljs @@ -0,0 +1,21 @@ +(ns status-im2.contexts.chat.lightbox.animations + (:require [react-native.reanimated :as reanimated])) + +;; TODO: Abstract Reanimated methods in a better way, issue: +;; https://github.com/status-im/status-mobile/issues/15176 +(def get-val reanimated/get-shared-value) + +(def set-val reanimated/set-shared-value) + +(def use-val reanimated/use-shared-value) + +(defn animate + ([animation value] + (animate animation value 300)) + ([animation value duration] + (set-val animation + (reanimated/with-timing value + (clj->js {:duration duration + :easing (reanimated/default-easing)}))))) + +(def animate-decay reanimated/animate-shared-value-with-decay) diff --git a/src/status_im2/contexts/chat/lightbox/bottom_view.cljs b/src/status_im2/contexts/chat/lightbox/bottom_view.cljs index bad63521dd..228fb4e43b 100644 --- a/src/status_im2/contexts/chat/lightbox/bottom_view.cljs +++ b/src/status_im2/contexts/chat/lightbox/bottom_view.cljs @@ -5,7 +5,7 @@ [react-native.reanimated :as reanimated] [status-im2.contexts.chat.lightbox.style :as style] [utils.re-frame :as rf] - [status-im2.contexts.chat.lightbox.common :as common])) + [status-im2.contexts.chat.lightbox.animations :as anim])) (def small-image-size 40) @@ -25,10 +25,10 @@ [:f> (fn [] (let [size (if (= @scroll-index index) focused-image-size small-image-size) - size-value (common/use-val size) + size-value (anim/use-val size) {:keys [scroll-index-lock? small-list-ref flat-list-ref]} atoms] - (common/set-val-timing size-value size) + (anim/animate size-value size) [rn/touchable-opacity {:active-opacity 1 :on-press (fn [] diff --git a/src/status_im2/contexts/chat/lightbox/common.cljs b/src/status_im2/contexts/chat/lightbox/common.cljs deleted file mode 100644 index cf6283ad50..0000000000 --- a/src/status_im2/contexts/chat/lightbox/common.cljs +++ /dev/null @@ -1,14 +0,0 @@ -(ns status-im2.contexts.chat.lightbox.common - (:require [react-native.reanimated :as reanimated])) - -(def top-view-height 56) - -;; TODO: Abstract Reanimated methods in a better way, issue: -;; https://github.com/status-im/status-mobile/issues/15176 -(defn set-val-timing - [animation value] - (reanimated/set-shared-value animation (reanimated/with-timing value))) - -(defn use-val - [value] - (reanimated/use-shared-value value)) diff --git a/src/status_im2/contexts/chat/lightbox/style.cljs b/src/status_im2/contexts/chat/lightbox/style.cljs index 99749793ec..7f0634b073 100644 --- a/src/status_im2/contexts/chat/lightbox/style.cljs +++ b/src/status_im2/contexts/chat/lightbox/style.cljs @@ -1,12 +1,7 @@ (ns status-im2.contexts.chat.lightbox.style (:require [quo2.foundations.colors :as colors] [react-native.platform :as platform] - [react-native.reanimated :as reanimated] - [status-im2.contexts.chat.lightbox.common :as common])) - -;;;; MAIN-VIEW -(def container-view - {:background-color :black}) + [react-native.reanimated :as reanimated])) ;;;; TOP-VIEW (defn top-view-container @@ -27,7 +22,8 @@ {:position :absolute :padding-horizontal 20 :top (if platform/ios? top-inset 0) - :height common/top-view-height + ;; height defined in top_view.cljs, but can't import due to circular dependency + :height 56 :z-index 4 :flex-direction :row :justify-content :space-between diff --git a/src/status_im2/contexts/chat/lightbox/top_view.cljs b/src/status_im2/contexts/chat/lightbox/top_view.cljs index 25219f0354..0019ac5471 100644 --- a/src/status_im2/contexts/chat/lightbox/top_view.cljs +++ b/src/status_im2/contexts/chat/lightbox/top_view.cljs @@ -6,37 +6,39 @@ [react-native.orientation :as orientation] [react-native.platform :as platform] [react-native.reanimated :as reanimated] - [status-im2.contexts.chat.lightbox.common :as common] + [status-im2.contexts.chat.lightbox.animations :as anim] [status-im2.contexts.chat.lightbox.style :as style] [utils.datetime :as datetime] [utils.re-frame :as rf])) +(def ^:const top-view-height 56) + (defn animate-rotation [result screen-width screen-height insets-atom {:keys [rotate top-view-y top-view-x top-view-width top-view-bg]}] - (let [top-x (+ (/ common/top-view-height 2) (:top insets-atom))] + (let [top-x (+ (/ top-view-height 2) (:top insets-atom))] (cond (= result orientation/landscape-left) (do - (common/set-val-timing rotate "90deg") - (common/set-val-timing top-view-y 60) - (common/set-val-timing top-view-x (- (/ screen-height 2) top-x)) - (common/set-val-timing top-view-width screen-height) - (common/set-val-timing top-view-bg colors/neutral-100-opa-70)) + (anim/animate rotate "90deg") + (anim/animate top-view-y 60) + (anim/animate top-view-x (- (/ screen-height 2) top-x)) + (anim/animate top-view-width screen-height) + (anim/animate top-view-bg colors/neutral-100-opa-70)) (= result orientation/landscape-right) (do - (common/set-val-timing rotate "-90deg") - (common/set-val-timing top-view-y (- (- screen-width) 4)) - (common/set-val-timing top-view-x (+ (/ screen-height -2) top-x)) - (common/set-val-timing top-view-width screen-height) - (common/set-val-timing top-view-bg colors/neutral-100-opa-70)) + (anim/animate rotate "-90deg") + (anim/animate top-view-y (- (- screen-width) 4)) + (anim/animate top-view-x (+ (/ screen-height -2) top-x)) + (anim/animate top-view-width screen-height) + (anim/animate top-view-bg colors/neutral-100-opa-70)) (= result orientation/portrait) (do - (common/set-val-timing rotate "0deg") - (common/set-val-timing top-view-y 0) - (common/set-val-timing top-view-x 0) - (common/set-val-timing top-view-width screen-width) - (common/set-val-timing top-view-bg colors/neutral-100-opa-0))))) + (anim/animate rotate "0deg") + (anim/animate top-view-y 0) + (anim/animate top-view-x 0) + (anim/animate top-view-width screen-width) + (anim/animate top-view-bg colors/neutral-100-opa-0))))) (defn top-view [{:keys [from timestamp]} insets index animations landscape? screen-width] @@ -51,7 +53,10 @@ :align-items :center}} [rn/touchable-opacity {:on-press (fn [] - (common/set-val-timing (:opacity animations) 0) + (when platform/ios? + (anim/animate (:background-color animations) + (reanimated/with-timing "rgba(0,0,0,0)"))) + (anim/animate (:opacity animations) 0) (rf/dispatch (if platform/ios? [:chat.ui/exit-lightbox-signal @index] [:navigate-back]))) diff --git a/src/status_im2/contexts/chat/lightbox/view.cljs b/src/status_im2/contexts/chat/lightbox/view.cljs index 7833233e3c..c7685dc094 100644 --- a/src/status_im2/contexts/chat/lightbox/view.cljs +++ b/src/status_im2/contexts/chat/lightbox/view.cljs @@ -6,11 +6,10 @@ [react-native.orientation :as orientation] [react-native.platform :as platform] [react-native.reanimated :as reanimated] - [status-im2.contexts.chat.lightbox.common :as common] + [status-im2.contexts.chat.lightbox.animations :as anim] [utils.re-frame :as rf] [react-native.safe-area :as safe-area] [reagent.core :as reagent] - [status-im2.contexts.chat.lightbox.style :as style] [status-im2.contexts.chat.lightbox.zoomable-image.view :as zoomable-image] [status-im2.contexts.chat.lightbox.top-view :as top-view] [status-im2.contexts.chat.lightbox.bottom-view :as bottom-view] @@ -23,15 +22,15 @@ (let [opacity (reanimated/get-shared-value opacity-value)] (if (= opacity 1) (do - (common/set-val-timing opacity-value 0) + (anim/animate opacity-value 0) (js/setTimeout #(reset! transparent? (not @transparent?)) 400)) (do (reset! transparent? (not @transparent?)) - (reanimated/animate-shared-value-with-delay-default-easing opacity-value 1 300 50) + (js/setTimeout #(anim/animate opacity-value 1) 50) (js/setTimeout #(when @small-list-ref (.scrollToIndex ^js @small-list-ref #js {:animated false :index index})) 100))) - (common/set-val-timing border-value (if (= opacity 1) 0 12)))) + (anim/animate border-value (if (= opacity 1) 0 12)))) (defn handle-orientation [result index window animations {:keys [flat-list-ref insets-atom]}] @@ -87,6 +86,14 @@ #(toggle-opacity opacity-value border-value transparent? index atoms)] [rn/view {:style {:width seperator-width}}]])]) +;; using `safe-area/consumer` in this component in iOS causes unnecessary re-renders and weird behaviour +;; using `use-safe-area` on Android crashes the app with error `rendered fewer hooks than expected` +(defn container-view + [children] + (if platform/ios? + [:f> children] + [safe-area/consumer children])) + (defn lightbox [] [:f> @@ -103,18 +110,21 @@ scroll-index (reagent/atom index) transparent? (reagent/atom false) window (rf/sub [:dimensions/window]) - animations {:border (common/use-val (if platform/ios? 0 12)) - :opacity (common/use-val 0) - :rotate (common/use-val "0deg") - :top-layout (common/use-val -10) - :bottom-layout (common/use-val 10) - :top-view-y (common/use-val 0) - :top-view-x (common/use-val 0) - :top-view-width (common/use-val (:width window)) - :top-view-bg (common/use-val colors/neutral-100-opa-0)} + animations {:background-color (anim/use-val "rgba(0,0,0,0)") + :border (anim/use-val (if platform/ios? 0 12)) + :opacity (anim/use-val 0) + :rotate (anim/use-val "0deg") + :top-layout (anim/use-val -10) + :bottom-layout (anim/use-val 10) + :top-view-y (anim/use-val 0) + :top-view-x (anim/use-val 0) + :top-view-width (anim/use-val (:width window)) + :top-view-bg (anim/use-val colors/neutral-100-opa-0)} callback (fn [e] - (on-viewable-items-changed e scroll-index atoms))] + (on-viewable-items-changed e scroll-index atoms)) + insets-ios (when platform/ios? (safe-area/use-safe-area))] + (anim/animate (:background-color animations) "rgba(0,0,0,1)") (reset! data messages) (orientation/use-device-orientation-change (fn [result] @@ -131,19 +141,20 @@ (.scrollToIndex ^js @(:flat-list-ref atoms) #js {:animated false :index index})) (js/setTimeout (fn [] - (common/set-val-timing (:opacity animations) 1) - (common/set-val-timing (:top-layout animations) 0) - (common/set-val-timing (:bottom-layout animations) 0) - (common/set-val-timing (:border animations) 12)) + (anim/animate (:opacity animations) 1) + (anim/animate (:top-layout animations) 0) + (anim/animate (:bottom-layout animations) 0) + (anim/animate (:border animations) 12)) (if platform/ios? 250 100)) (js/setTimeout #(reset! (:scroll-index-lock? atoms) false) 300) (fn [] (rf/dispatch [:chat.ui/zoom-out-signal nil]) (when platform/android? (rf/dispatch [:chat.ui/lightbox-scale 1]))))) - [safe-area/consumer - (fn [insets] - (let [curr-orientation (or (rf/sub [:lightbox/orientation]) orientation/portrait) + [container-view + (fn [insets-android] + (let [insets (if platform/ios? insets-ios insets-android) + curr-orientation (or (rf/sub [:lightbox/orientation]) orientation/portrait) landscape? (string/includes? curr-orientation orientation/landscape) horizontal? (or platform/android? (not landscape?)) inverted? (and platform/ios? (= curr-orientation orientation/landscape-right)) @@ -155,7 +166,12 @@ (:width window)) item-width (if (and landscape? platform/ios?) screen-height screen-width)] (reset! (:insets-atom atoms) insets) - [rn/view {:style style/container-view} + [reanimated/view + {:style (if platform/ios? + (reanimated/apply-animations-to-style {:background-color (:background-color + animations)} + {}) + {:background-color :black})} (when-not @transparent? [top-view/top-view (first messages) insets scroll-index animations landscape? screen-width]) diff --git a/src/status_im2/contexts/chat/lightbox/zoomable_image/style.cljs b/src/status_im2/contexts/chat/lightbox/zoomable_image/style.cljs index dee2bf8411..e1a7d3939a 100644 --- a/src/status_im2/contexts/chat/lightbox/zoomable_image/style.cljs +++ b/src/status_im2/contexts/chat/lightbox/zoomable_image/style.cljs @@ -6,7 +6,8 @@ (defn container [{:keys [width height]} {:keys [pan-x pan-y pinch-x pinch-y scale]} - set-full-height?] + set-full-height? + portrait?] (reanimated/apply-animations-to-style {:transform [{:translateX pan-x} {:translateY pan-y} @@ -15,7 +16,7 @@ {:scale scale}]} {:justify-content :center :align-items :center - :width (if platform/ios? width "100%") + :width (if (or platform/ios? portrait?) width "100%") :height (if set-full-height? "100%" height)})) (defn image diff --git a/src/status_im2/contexts/chat/lightbox/zoomable_image/utils.cljs b/src/status_im2/contexts/chat/lightbox/zoomable_image/utils.cljs index e272b2995c..e313aa8546 100644 --- a/src/status_im2/contexts/chat/lightbox/zoomable_image/utils.cljs +++ b/src/status_im2/contexts/chat/lightbox/zoomable_image/utils.cljs @@ -4,9 +4,92 @@ [react-native.orientation :as orientation] [react-native.platform :as platform] [status-im2.contexts.chat.lightbox.zoomable-image.constants :as c] + [status-im2.contexts.chat.lightbox.animations :as anim] [utils.re-frame :as rf])) +;;; Helpers +(defn center-x + [{:keys [pinch-x pinch-x-start pan-x pan-x-start]} exit?] + (let [duration (if exit? 100 c/default-duration)] + (anim/animate pinch-x c/init-offset duration) + (anim/set-val pinch-x-start c/init-offset) + (anim/animate pan-x c/init-offset duration) + (anim/set-val pan-x-start c/init-offset))) +(defn center-y + [{:keys [pinch-y pinch-y-start pan-y pan-y-start]} exit?] + (let [duration (if exit? 100 c/default-duration)] + (anim/animate pinch-y c/init-offset duration) + (anim/set-val pinch-y-start c/init-offset) + (anim/animate pan-y c/init-offset duration) + (anim/set-val pan-y-start c/init-offset))) + +(defn reset-values + [exit? animations {:keys [focal-x focal-y]}] + (center-x animations exit?) + (center-y animations exit?) + (reset! focal-x nil) + (reset! focal-y nil)) + +(defn rescale-image + [value + exit? + {:keys [x-threshold-scale y-threshold-scale]} + {:keys [scale saved-scale] :as animations} + {:keys [pan-x-enabled? pan-y-enabled?] :as props}] + (anim/animate scale value (if exit? 100 c/default-duration)) + (anim/set-val saved-scale value) + (when (= value c/min-scale) + (reset-values exit? animations props)) + (reset! pan-x-enabled? (> value x-threshold-scale)) + (reset! pan-y-enabled? (> value y-threshold-scale))) + +;;; Handlers +(defn handle-orientation-change + [curr-orientation + focused? + {:keys [landscape-scale-val x-threshold-scale y-threshold-scale]} + {:keys [rotate rotate-scale scale] :as animations} + {:keys [pan-x-enabled? pan-y-enabled?]}] + (let [duration (when focused? c/default-duration)] + (cond + (= curr-orientation orientation/landscape-left) + (do + (anim/animate rotate "90deg" duration) + (anim/animate rotate-scale landscape-scale-val duration)) + (= curr-orientation orientation/landscape-right) + (do + (anim/animate rotate "-90deg" duration) + (anim/animate rotate-scale landscape-scale-val duration)) + (= curr-orientation orientation/portrait) + (do + (anim/animate rotate c/init-rotation duration) + (anim/animate rotate-scale c/min-scale duration))) + (center-x animations false) + (center-y animations false) + (reset! pan-x-enabled? (> (anim/get-val scale) x-threshold-scale)) + (reset! pan-y-enabled? (> (anim/get-val scale) y-threshold-scale)))) + +;; On ios, when attempting to navigate back while zoomed in, the shared-element transition animation +;; doesn't execute properly, so we need to zoom out first +(defn handle-exit-lightbox-signal + [exit-lightbox-signal index scale rescale set-full-height?] + (when (= exit-lightbox-signal index) + (reset! set-full-height? false) + (if (> scale c/min-scale) + (do + (rescale c/min-scale true) + (js/setTimeout #(rf/dispatch [:navigate-back]) 70)) + (rf/dispatch [:navigate-back])) + (js/setTimeout #(rf/dispatch [:chat.ui/exit-lightbox-signal nil]) 500))) + +(defn handle-zoom-out-signal + "Zooms out when pressing on another photo from the small bottom list" + [zoom-out-signal index scale rescale] + (when (and (= zoom-out-signal index) (> scale c/min-scale)) + (rescale c/min-scale true))) + +;;; Dimensions (defn get-dimensions "Calculates all required dimensions. Dimensions calculations are different on iOS and Android because landscape mode is implemented differently.On Android, we just need to resize the content, and the OS takes care of the @@ -39,25 +122,6 @@ :y-threshold-scale (/ screen-height (min screen-height container-height)) :landscape-scale-val (/ portrait-image-width portrait-image-height)})) -(defn handle-exit-lightbox-signal - "On ios, when attempting to navigate back while zoomed in, the shared-element transition animation - doesn't execute properly, so we need to zoom out first" - [exit-lightbox-signal index scale rescale set-full-height?] - (when (= exit-lightbox-signal index) - (reset! set-full-height? false) - (if (> scale c/min-scale) - (do - (rescale c/min-scale true) - (js/setTimeout #(rf/dispatch [:navigate-back]) 70)) - (rf/dispatch [:navigate-back])) - (js/setTimeout #(rf/dispatch [:chat.ui/exit-lightbox-signal nil]) 500))) - -(defn handle-zoom-out-signal - "Zooms out when pressing on another photo from the small bottom list" - [zoom-out-signal index scale rescale] - (when (and (= zoom-out-signal index) (> scale c/min-scale)) - (rescale c/min-scale true))) - ;;; MATH (defn get-max-offset diff --git a/src/status_im2/contexts/chat/lightbox/zoomable_image/view.cljs b/src/status_im2/contexts/chat/lightbox/zoomable_image/view.cljs index 8f570058b4..c9ea4c1c81 100644 --- a/src/status_im2/contexts/chat/lightbox/zoomable_image/view.cljs +++ b/src/status_im2/contexts/chat/lightbox/zoomable_image/view.cljs @@ -8,102 +8,11 @@ [utils.re-frame :as rf] [oops.core :refer [oget]] [react-native.orientation :as orientation] + [status-im2.contexts.chat.lightbox.animations :as anim] [status-im2.contexts.chat.lightbox.zoomable-image.constants :as c] [status-im2.contexts.chat.lightbox.zoomable-image.style :as style] [status-im2.contexts.chat.lightbox.zoomable-image.utils :as utils])) -;;;; Some aliases for reanimated methods, as they are used 10s of times in this file -;; TODO: Abstract Reanimated methods in a better way, issue: -;; https://github.com/status-im/status-mobile/issues/15176 -(defn get-val - [animation] - (reanimated/get-shared-value animation)) - -(defn set-val - [animation value] - (reanimated/set-shared-value animation value)) - -(defn use-val - [value] - (reanimated/use-shared-value value)) - -(defn timing - ([value] - (timing value c/default-duration)) - ([value duration] - (if (= duration nil) - value - (reanimated/with-timing-duration value duration)))) - -(defn set-val-decay - [animation velocity bounds] - (reanimated/animate-shared-value-with-decay animation (* velocity c/velocity-factor) bounds)) - -;;;; Helpers -(defn center-x - [{:keys [pinch-x pinch-x-start pan-x pan-x-start]} exit?] - (let [duration (if exit? 100 c/default-duration)] - (set-val pinch-x (timing c/init-offset duration)) - (set-val pinch-x-start c/init-offset) - (set-val pan-x (timing c/init-offset duration)) - (set-val pan-x-start c/init-offset))) - -(defn center-y - [{:keys [pinch-y pinch-y-start pan-y pan-y-start]} exit?] - (let [duration (if exit? 100 c/default-duration)] - (set-val pinch-y (timing c/init-offset duration)) - (set-val pinch-y-start c/init-offset) - (set-val pan-y (timing c/init-offset duration)) - (set-val pan-y-start c/init-offset))) - -(defn reset-values - [exit? animations {:keys [focal-x focal-y]}] - (center-x animations exit?) - (center-y animations exit?) - (reset! focal-x nil) - (reset! focal-y nil)) - -(defn rescale-image - [value - exit? - {:keys [x-threshold-scale y-threshold-scale]} - {:keys [scale saved-scale] :as animations} - {:keys [pan-x-enabled? pan-y-enabled?] :as props}] - (set-val scale (timing value (if exit? 100 c/default-duration))) - (set-val saved-scale value) - (when (= value c/min-scale) - (reset-values exit? animations props)) - (reset! pan-x-enabled? (> value x-threshold-scale)) - (reset! pan-y-enabled? (> value y-threshold-scale)) - (when platform/android? - (rf/dispatch [:chat.ui/lightbox-scale value]))) - -(defn handle-orientation-change - [curr-orientation - focused? - {:keys [landscape-scale-val x-threshold-scale y-threshold-scale]} - {:keys [rotate rotate-scale scale] :as animations} - {:keys [pan-x-enabled? pan-y-enabled?]}] - (let [duration (when focused? c/default-duration)] - (cond - (= curr-orientation orientation/landscape-left) - (do - (set-val rotate (timing "90deg" duration)) - (set-val rotate-scale (timing landscape-scale-val duration))) - (= curr-orientation orientation/landscape-right) - (do - (set-val rotate (timing "-90deg" duration)) - (set-val rotate-scale (timing landscape-scale-val duration))) - (= curr-orientation orientation/portrait) - (do - (set-val rotate (timing c/init-rotation duration)) - (set-val rotate-scale (timing c/min-scale duration)))) - (center-x animations false) - (center-y animations false) - (reset! pan-x-enabled? (> (get-val scale) x-threshold-scale)) - (reset! pan-y-enabled? (> (get-val scale) y-threshold-scale)))) - -;;;; Gestures (defn tap-gesture [on-tap] (-> @@ -119,48 +28,46 @@ (gesture/number-of-taps 2) (gesture/on-start (fn [e] - (if (= (get-val scale) c/min-scale) + (if (= (anim/get-val scale) c/min-scale) (let [translate-x (utils/get-double-tap-offset width screen-width (oget e "x")) translate-y (utils/get-double-tap-offset height screen-height (oget e "y"))] (when (> c/double-tap-scale x-threshold-scale) - (set-val pan-x (timing translate-x)) - (set-val pan-x-start translate-x)) + (anim/animate pan-x translate-x) + (anim/set-val pan-x-start translate-x)) (when (> c/double-tap-scale y-threshold-scale) - (set-val pan-y (timing translate-y)) - (set-val pan-y-start translate-y)) + (anim/animate pan-y translate-y) + (anim/set-val pan-y-start translate-y)) (rescale c/double-tap-scale)) (rescale c/min-scale)))))) ;; not using on-finalize because on-finalize gets called always regardless the gesture executed or not (defn finalize-pinch [{:keys [width height screen-height screen-width x-threshold-scale y-threshold-scale]} - {:keys [saved-scale scale pinch-x pinch-y pinch-x-start pinch-y-start pan-y pan-y-start pan-x + {:keys [scale pinch-x pinch-y pinch-x-start pinch-y-start pan-y pan-y-start pan-x pan-x-start]} {:keys [pan-x-enabled? pan-y-enabled?]}] - (let [curr-offset-y (+ (get-val pan-y) (get-val pinch-y)) - max-offset-y (utils/get-max-offset height screen-height (get-val scale)) + (let [curr-offset-y (+ (anim/get-val pan-y) (anim/get-val pinch-y)) + max-offset-y (utils/get-max-offset height screen-height (anim/get-val scale)) max-offset-y (if (neg? curr-offset-y) (- max-offset-y) max-offset-y) - curr-offset-x (+ (get-val pan-x) (get-val pinch-x)) - max-offset-x (utils/get-max-offset width screen-width (get-val scale)) + curr-offset-x (+ (anim/get-val pan-x) (anim/get-val pinch-x)) + max-offset-x (utils/get-max-offset width screen-width (anim/get-val scale)) max-offset-x (if (neg? curr-offset-x) (- max-offset-x) max-offset-x)] - (when (and (> (get-val scale) y-threshold-scale) - (< (get-val scale) c/max-scale) + (when (and (> (anim/get-val scale) y-threshold-scale) + (< (anim/get-val scale) c/max-scale) (> (Math/abs curr-offset-y) (Math/abs max-offset-y))) - (set-val pinch-y (timing c/init-offset)) - (set-val pinch-y-start c/init-offset) - (set-val pan-y (timing max-offset-y)) - (set-val pan-y-start max-offset-y)) - (when (and (> (get-val scale) x-threshold-scale) - (< (get-val scale) c/max-scale) + (anim/animate pinch-y c/init-offset) + (anim/set-val pinch-y-start c/init-offset) + (anim/animate pan-y max-offset-y) + (anim/set-val pan-y-start max-offset-y)) + (when (and (> (anim/get-val scale) x-threshold-scale) + (< (anim/get-val scale) c/max-scale) (> (Math/abs curr-offset-x) (Math/abs max-offset-x))) - (set-val pinch-x (timing c/init-offset)) - (set-val pinch-x-start c/init-offset) - (set-val pan-x (timing max-offset-x)) - (set-val pan-x-start max-offset-x)) - (reset! pan-x-enabled? (> (get-val scale) x-threshold-scale)) - (reset! pan-y-enabled? (> (get-val scale) y-threshold-scale)) - (when platform/android? - (rf/dispatch [:chat.ui/lightbox-scale (get-val saved-scale)])))) + (anim/animate pinch-x c/init-offset) + (anim/set-val pinch-x-start c/init-offset) + (anim/animate pan-x max-offset-x) + (anim/set-val pan-x-start max-offset-x)) + (reset! pan-x-enabled? (> (anim/get-val scale) x-threshold-scale)) + (reset! pan-y-enabled? (> (anim/get-val scale) y-threshold-scale)))) (defn pinch-gesture [{:keys [width height screen-height screen-width x-threshold-scale y-threshold-scale] :as dimensions} @@ -179,40 +86,41 @@ (reset! focal-x (utils/get-focal (oget e "focalX") width screen-width)) (reset! focal-y (utils/get-focal (oget e "focalY") height screen-height))))) (gesture/on-update (fn [e] - (let [new-scale (* (oget e "scale") (get-val saved-scale)) - scale-diff (utils/get-scale-diff new-scale (get-val saved-scale)) + (let [new-scale (* (oget e "scale") (anim/get-val saved-scale)) + scale-diff (utils/get-scale-diff new-scale (anim/get-val saved-scale)) new-pinch-x (utils/get-pinch-position scale-diff screen-width @focal-x) new-pinch-y (utils/get-pinch-position scale-diff screen-height @focal-y)] - (when (and (>= new-scale c/max-scale) (= (get-val pinch-x-max) js/Infinity)) - (set-val pinch-x-max (get-val pinch-x)) - (set-val pinch-y-max (get-val pinch-y))) - (set-val pinch-x (+ new-pinch-x (get-val pinch-x-start))) - (set-val pinch-y (+ new-pinch-y (get-val pinch-y-start))) - (set-val scale new-scale)))) + (when (and (>= new-scale c/max-scale) + (= (anim/get-val pinch-x-max) js/Infinity)) + (anim/set-val pinch-x-max (anim/get-val pinch-x)) + (anim/set-val pinch-y-max (anim/get-val pinch-y))) + (anim/set-val pinch-x (+ new-pinch-x (anim/get-val pinch-x-start))) + (anim/set-val pinch-y (+ new-pinch-y (anim/get-val pinch-y-start))) + (anim/set-val scale new-scale)))) (gesture/on-end (fn [] (cond - (< (get-val scale) c/min-scale) + (< (anim/get-val scale) c/min-scale) (rescale c/min-scale) - (> (get-val scale) c/max-scale) + (> (anim/get-val scale) c/max-scale) (do - (set-val pinch-x (timing (get-val pinch-x-max))) - (set-val pinch-x-start (get-val pinch-x-max)) - (set-val pinch-x-max js/Infinity) - (set-val pinch-y (timing (get-val pinch-y-max))) - (set-val pinch-y-start (get-val pinch-y-max)) - (set-val pinch-y-max js/Infinity) - (set-val scale (timing c/max-scale)) - (set-val saved-scale c/max-scale)) + (anim/animate pinch-x (anim/get-val pinch-x-max)) + (anim/set-val pinch-x-start (anim/get-val pinch-x-max)) + (anim/set-val pinch-x-max js/Infinity) + (anim/animate pinch-y (anim/get-val pinch-y-max)) + (anim/set-val pinch-y-start (anim/get-val pinch-y-max)) + (anim/set-val pinch-y-max js/Infinity) + (anim/animate scale c/max-scale) + (anim/set-val saved-scale c/max-scale)) :else (do - (set-val saved-scale (get-val scale)) - (set-val pinch-x-start (get-val pinch-x)) - (set-val pinch-y-start (get-val pinch-y)) - (when (< (get-val scale) x-threshold-scale) - (center-x animations false)) - (when (< (get-val scale) y-threshold-scale) - (center-y animations false)))) + (anim/set-val saved-scale (anim/get-val scale)) + (anim/set-val pinch-x-start (anim/get-val pinch-x)) + (anim/set-val pinch-y-start (anim/get-val pinch-y)) + (when (< (anim/get-val scale) x-threshold-scale) + (utils/center-x animations false)) + (when (< (anim/get-val scale) y-threshold-scale) + (utils/center-y animations false)))) (finalize-pinch dimensions animations props))))) (defn pan-x-gesture @@ -225,27 +133,28 @@ (gesture/enabled @pan-x-enabled?) (gesture/average-touches false) (gesture/on-update (fn [e] - (set-val pan-x (+ (get-val pan-x-start) (oget e "translationX"))))) + (anim/set-val pan-x (+ (anim/get-val pan-x-start) (oget e "translationX"))))) (gesture/on-end (fn [e] - (let [curr-offset (+ (get-val pan-x) (get-val pinch-x-start)) - max-offset (utils/get-max-offset width screen-width (get-val scale)) - max-offset (if (neg? curr-offset) (- max-offset) max-offset)] + (let [curr-offset (+ (anim/get-val pan-x) (anim/get-val pinch-x-start)) + max-offset (utils/get-max-offset width screen-width (anim/get-val scale)) + max-offset (if (neg? curr-offset) (- max-offset) max-offset) + velocity (* (oget e "velocityX") c/velocity-factor)] (cond - (< (get-val scale) x-threshold-scale) + (< (anim/get-val scale) x-threshold-scale) (rescale c/min-scale) (> (Math/abs curr-offset) (Math/abs max-offset)) (do - (set-val pan-x (timing max-offset)) - (set-val pan-x-start max-offset) - (set-val pinch-x (timing c/init-offset)) - (set-val pinch-x-start c/init-offset)) + (anim/animate pan-x max-offset) + (anim/set-val pan-x-start max-offset) + (anim/animate pinch-x c/init-offset) + (anim/set-val pinch-x-start c/init-offset)) :else - (let [lower-bound (- (- (Math/abs max-offset)) (get-val pinch-x-start)) - upper-bound (- (Math/abs max-offset) (get-val pinch-x-start))] - (set-val pan-x-start (get-val pan-x)) - (set-val-decay pan-x (oget e "velocityX") [lower-bound upper-bound]) - (set-val-decay pan-x-start (oget e "velocityX") [lower-bound upper-bound])))))))) + (let [lower-bound (- (- (Math/abs max-offset)) (anim/get-val pinch-x-start)) + upper-bound (- (Math/abs max-offset) (anim/get-val pinch-x-start))] + (anim/set-val pan-x-start (anim/get-val pan-x)) + (anim/animate-decay pan-x velocity [lower-bound upper-bound]) + (anim/animate-decay pan-x-start velocity [lower-bound upper-bound])))))))) (defn pan-y-gesture @@ -258,30 +167,29 @@ (gesture/enabled @pan-y-enabled?) (gesture/average-touches false) (gesture/on-update (fn [e] - (set-val pan-y (+ (get-val pan-y-start) (oget e "translationY"))))) + (anim/set-val pan-y (+ (anim/get-val pan-y-start) (oget e "translationY"))))) (gesture/on-end (fn [e] - (let [curr-offset (+ (get-val pan-y) (get-val pinch-y-start)) - max-offset (utils/get-max-offset height screen-height (get-val scale)) - max-offset (if (neg? curr-offset) (- max-offset) max-offset)] + (let [curr-offset (+ (anim/get-val pan-y) (anim/get-val pinch-y-start)) + max-offset (utils/get-max-offset height screen-height (anim/get-val scale)) + max-offset (if (neg? curr-offset) (- max-offset) max-offset) + velocity (* (oget e "velocityY") c/velocity-factor)] (cond - (< (get-val scale) y-threshold-scale) + (< (anim/get-val scale) y-threshold-scale) (rescale c/min-scale) (> (Math/abs curr-offset) (Math/abs max-offset)) (do - (set-val pan-y (timing max-offset)) - (set-val pan-y-start max-offset) - (set-val pinch-y (timing c/init-offset)) - (set-val pinch-y-start c/init-offset)) + (anim/animate pan-y max-offset) + (anim/set-val pan-y-start max-offset) + (anim/animate pinch-y c/init-offset) + (anim/set-val pinch-y-start c/init-offset)) :else - (let [lower-bound (- (- (Math/abs max-offset)) (get-val pinch-y-start)) - upper-bound (- (Math/abs max-offset) (get-val pinch-y-start))] - (set-val pan-y-start (get-val pan-y)) - (set-val-decay pan-y (oget e "velocityY") [lower-bound upper-bound]) - (set-val-decay pan-y-start (oget e "velocityY") [lower-bound upper-bound])))))))) + (let [lower-bound (- (- (Math/abs max-offset)) (anim/get-val pinch-y-start)) + upper-bound (- (Math/abs max-offset) (anim/get-val pinch-y-start))] + (anim/set-val pan-y-start (anim/get-val pan-y)) + (anim/animate-decay pan-y velocity [lower-bound upper-bound]) + (anim/animate-decay pan-y-start velocity [lower-bound upper-bound])))))))) - -;;;; Finally, the component (defn zoomable-image [{:keys [image-width image-height content message-id]} index border-radius on-tap] (let [set-full-height? (reagent/atom false)] @@ -290,41 +198,40 @@ (let [shared-element-id (rf/sub [:shared-element-id]) exit-lightbox-signal (rf/sub [:lightbox/exit-signal]) zoom-out-signal (rf/sub [:lightbox/zoom-out-signal]) - initial-scale (if platform/ios? c/min-scale (rf/sub [:lightbox/scale])) - curr-orientation (or (rf/sub [:lightbox/orientation]) orientation/portrait) focused? (= shared-element-id message-id) + curr-orientation (or (rf/sub [:lightbox/orientation]) orientation/portrait) dimensions (utils/get-dimensions image-width image-height curr-orientation) - animations {:scale (use-val initial-scale) - :saved-scale (use-val initial-scale) - :pan-x-start (use-val c/init-offset) - :pan-x (use-val c/init-offset) - :pan-y-start (use-val c/init-offset) - :pan-y (use-val c/init-offset) - :pinch-x-start (use-val c/init-offset) - :pinch-x (use-val c/init-offset) - :pinch-y-start (use-val c/init-offset) - :pinch-y (use-val c/init-offset) - :pinch-x-max (use-val js/Infinity) - :pinch-y-max (use-val js/Infinity) - :rotate (use-val c/init-rotation) - :rotate-scale (use-val c/min-scale)} + animations {:scale (anim/use-val c/min-scale) + :saved-scale (anim/use-val c/min-scale) + :pan-x-start (anim/use-val c/init-offset) + :pan-x (anim/use-val c/init-offset) + :pan-y-start (anim/use-val c/init-offset) + :pan-y (anim/use-val c/init-offset) + :pinch-x-start (anim/use-val c/init-offset) + :pinch-x (anim/use-val c/init-offset) + :pinch-y-start (anim/use-val c/init-offset) + :pinch-y (anim/use-val c/init-offset) + :pinch-x-max (anim/use-val js/Infinity) + :pinch-y-max (anim/use-val js/Infinity) + :rotate (anim/use-val c/init-rotation) + :rotate-scale (anim/use-val c/min-scale)} props {:pan-x-enabled? (reagent/atom false) :pan-y-enabled? (reagent/atom false) :focal-x (reagent/atom nil) :focal-y (reagent/atom nil)} rescale (fn [value exit?] - (rescale-image value exit? dimensions animations props))] + (utils/rescale-image value exit? dimensions animations props))] (rn/use-effect-once (fn [] (js/setTimeout #(reset! set-full-height? true) 500) js/undefined)) (when platform/ios? - (handle-orientation-change curr-orientation focused? dimensions animations props) + (utils/handle-orientation-change curr-orientation focused? dimensions animations props) (utils/handle-exit-lightbox-signal exit-lightbox-signal index - (get-val (:scale animations)) + (anim/get-val (:scale animations)) rescale set-full-height?)) - (utils/handle-zoom-out-signal zoom-out-signal index (get-val (:scale animations)) rescale) + (utils/handle-zoom-out-signal zoom-out-signal index (anim/get-val (:scale animations)) rescale) [:f> (fn [] (let [tap (tap-gesture on-tap) @@ -337,9 +244,11 @@ (gesture/exclusive double-tap tap))] [gesture/gesture-detector {:gesture composed-gestures} [reanimated/view - {:style (style/container dimensions animations @set-full-height?)} + {:style (style/container dimensions + animations + @set-full-height? + (= curr-orientation orientation/portrait))} [reanimated/fast-image {:source {:uri (:image content)} :native-ID (when focused? :shared-element) :style (style/image dimensions animations border-radius)}]]]))]))])) - diff --git a/src/status_im2/navigation/screens.cljs b/src/status_im2/navigation/screens.cljs index 800e91d1a8..e65fae4870 100644 --- a/src/status_im2/navigation/screens.cljs +++ b/src/status_im2/navigation/screens.cljs @@ -65,6 +65,8 @@ :style :light :animate true} :navigationBar {:backgroundColor colors/black} + :layout {:componentBackgroundColor :transparent + :backgroundColor :transparent} :animations {:push {:sharedElementTransitions [{:fromId :shared-element :toId :shared-element :interpolation {:type :decelerate