feat: improve lightbox transition (#15315)
* feat: improve lightbox transition
This commit is contained in:
parent
0fe6fea7e4
commit
64dde1c9d1
|
@ -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)
|
||||
|
|
|
@ -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)}))
|
||||
|
|
|
@ -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)
|
|
@ -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 []
|
||||
|
|
|
@ -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))
|
|
@ -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
|
||||
|
|
|
@ -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])))
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)}]]]))]))]))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue