feat: improve lightbox transition (#15315)

* feat: improve lightbox transition
This commit is contained in:
Omar Basem 2023-03-14 08:24:14 +04:00 committed by GitHub
parent 0fe6fea7e4
commit 64dde1c9d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 297 additions and 283 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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