Lightbox opacity swipe animation, and margins (#16510)

* feat: lightbox animations
This commit is contained in:
Omar Basem 2023-07-11 13:41:34 +04:00 committed by GitHub
parent f5948fa016
commit 7392b9bf50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 146 additions and 96 deletions

View File

@ -58,7 +58,7 @@
:start {:x 0 :y 1}
:end {:x 0 :y 0}
:style (style/gradient-container insets animations derived)}
[text-sheet/view messages animations state]
[text-sheet/view messages animations state props]
[rn/flat-list
{:ref #(reset! (:small-list-ref props) %)
:key-fn :message-id

View File

@ -40,12 +40,13 @@
(defn expand-sheet
[{:keys [derived-value overlay-opacity saved-top]}
expanded-height max-height overlay-z-index expanded?]
(reanimated/animate derived-value expanded-height)
(reanimated/animate overlay-opacity (/ expanded-height max-height))
(reanimated/set-shared-value saved-top (- expanded-height))
(reset! overlay-z-index 1)
(reset! expanded? true))
expanded-height max-height overlay-z-index expanded? text-sheet-lock?]
(when-not @text-sheet-lock?
(reanimated/animate derived-value expanded-height)
(reanimated/animate overlay-opacity (/ expanded-height max-height))
(reanimated/set-shared-value saved-top (- expanded-height))
(reset! overlay-z-index 1)
(reset! expanded? true)))
(defn on-scroll
[e expanded? dragging? {:keys [gradient-opacity]}]

View File

@ -20,7 +20,7 @@
[rn/view {:style style/bar}]]))
(defn text-sheet
[messages overlay-opacity overlay-z-index]
[messages overlay-opacity overlay-z-index text-sheet-lock?]
(let [text-height (reagent/atom 0)
expanded? (reagent/atom false)
dragging? (atom false)]
@ -49,7 +49,12 @@
[reanimated/touchable-opacity
{:active-opacity 1
:on-press
#(utils/expand-sheet animations expanded-height max-height overlay-z-index expanded?)
#(utils/expand-sheet animations
expanded-height
max-height
overlay-z-index
expanded?
text-sheet-lock?)
:style (style/sheet-container derived)}
[bar @text-height]
[reanimated/linear-gradient
@ -74,5 +79,5 @@
:on-layout #(utils/on-layout % text-height)}]]]]))))
(defn view
[messages {:keys [overlay-opacity]} {:keys [overlay-z-index]}]
[:f> text-sheet messages overlay-opacity overlay-z-index])
[messages {:keys [overlay-opacity]} {:keys [overlay-z-index]} {:keys [text-sheet-lock?]}]
[:f> text-sheet messages overlay-opacity overlay-z-index text-sheet-lock?])

View File

@ -1,6 +1,7 @@
(ns status-im2.contexts.chat.lightbox.utils
(:require
[clojure.string :as string]
[oops.core :as oops]
[quo2.foundations.colors :as colors]
[react-native.core :as rn]
[react-native.gesture :as gesture]
@ -11,7 +12,6 @@
[status-im2.contexts.chat.lightbox.animations :as anim]
[status-im2.contexts.chat.lightbox.top-view :as top-view]
[utils.re-frame :as rf]
[oops.core :refer [oget]]
[status-im2.contexts.chat.lightbox.constants :as constants]
[utils.worklets.lightbox :as worklet]))
@ -98,6 +98,20 @@
(when (and enabled? (not= result orientation/landscape-right))
(handle-orientation result props state animations))))))))
(defn on-scroll
[e item-width {:keys [images-opacity]} landscape?]
(let [total-item-width (+ item-width constants/separator-width)
progress (/ (if landscape?
(oops/oget e "nativeEvent.contentOffset.y")
(oops/oget e "nativeEvent.contentOffset.x"))
total-item-width)
index-initial (max (Math/floor progress) 0)
index-final (inc index-initial)
decimal-part (- progress index-initial)]
(anim/set-val (nth images-opacity index-initial) (- 1 decimal-part))
(when (< index-final (count images-opacity))
(anim/set-val (nth images-opacity index-final) decimal-part))))
(defn drag-gesture
[{:keys [pan-x pan-y background-color opacity layout]} x? set-full-height?]
(->
@ -105,14 +119,15 @@
(gesture/enabled true)
(gesture/max-pointers 1)
(gesture/on-start #(reset! set-full-height? false))
(gesture/on-update (fn [e]
(let [translation (if x? (oget e "translationX") (oget e "translationY"))
progress (Math/abs (/ translation constants/drag-threshold))]
(anim/set-val (if x? pan-x pan-y) translation)
(anim/set-val opacity (- 1 progress))
(anim/set-val layout (* progress -20)))))
(gesture/on-update
(fn [e]
(let [translation (if x? (oops/oget e "translationX") (oops/oget e "translationY"))
progress (Math/abs (/ translation constants/drag-threshold))]
(anim/set-val (if x? pan-x pan-y) translation)
(anim/set-val opacity (- 1 progress))
(anim/set-val layout (* progress -20)))))
(gesture/on-end (fn [e]
(if (> (Math/abs (if x? (oget e "translationX") (oget e "translationY")))
(if (> (Math/abs (if x? (oops/oget e "translationX") (oops/oget e "translationY")))
constants/drag-threshold)
(do
(anim/animate background-color "rgba(0,0,0,0)")
@ -129,6 +144,7 @@
{:flat-list-ref (atom nil)
:small-list-ref (atom nil)
:scroll-index-lock? (atom true)
:text-sheet-lock? (atom false)
:timers (atom {})})
(defn init-state
@ -142,20 +158,29 @@
:set-full-height? (reagent/atom false)
:overlay-z-index (reagent/atom 0)})
(defn initialize-opacity
[size selected-index]
(mapv #(if (= % selected-index)
(anim/use-val 1)
(anim/use-val 0))
(range size)))
(defn init-animations
[]
{:background-color (anim/use-val colors/neutral-100-opa-0)
:border (anim/use-val (if platform/ios? 0 16))
:opacity (anim/use-val 0)
:overlay-opacity (anim/use-val 0)
:rotate (anim/use-val "0deg")
: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 (rn/get-window)))
:top-view-bg (anim/use-val colors/neutral-100-opa-0)
:pan-y (anim/use-val 0)
:pan-x (anim/use-val 0)})
[size index]
{:background-color (anim/use-val colors/neutral-100-opa-0)
:border (anim/use-val (if platform/ios? 0 16))
:full-screen-scale (anim/use-val 1)
:opacity (anim/use-val 0)
:overlay-opacity (anim/use-val 0)
:images-opacity (initialize-opacity size index)
:rotate (anim/use-val "0deg")
: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 (rn/get-window)))
:top-view-bg (anim/use-val colors/neutral-100-opa-0)
:pan-y (anim/use-val 0)
:pan-x (anim/use-val 0)})
(defn init-derived-animations
[{:keys [layout]}]

View File

@ -1,6 +1,7 @@
(ns status-im2.contexts.chat.lightbox.view
(:require
[clojure.string :as string]
[oops.core :as oops]
[quo2.foundations.colors :as colors]
[react-native.core :as rn]
[react-native.orientation :as orientation]
@ -14,7 +15,6 @@
[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]
[oops.core :refer [oget]]
[status-im2.contexts.chat.lightbox.utils :as utils]
[status-im2.contexts.chat.lightbox.constants :as constants]))
@ -28,12 +28,12 @@
(defn on-viewable-items-changed
[e {:keys [scroll-index-lock? small-list-ref]} {:keys [scroll-index]}]
(when-not @scroll-index-lock?
(let [changed (-> e (oget :changed) first)
index (oget changed :index)]
(let [changed (-> e (oops/oget :changed) first)
index (oops/oget changed :index)]
(reset! scroll-index index)
(when @small-list-ref
(.scrollToIndex ^js @small-list-ref #js {:animated true :index index}))
(rf/dispatch [:chat.ui/update-shared-element-id (:message-id (oget changed :item))]))))
(rf/dispatch [:chat.ui/update-shared-element-id (:message-id (oops/oget changed :item))]))))
(defn image
[message index _ {:keys [screen-width screen-height] :as args}]
@ -79,19 +79,23 @@
[gesture/flat-list
{:ref #(reset! (:flat-list-ref props) %)
:key-fn :message-id
:on-scroll #(utils/on-scroll % item-width animations landscape?)
:scroll-event-throttle 8
:style {:width (+ screen-width constants/separator-width)}
:data @data
:render-fn image
:render-data {:opacity-value (:opacity animations)
:border-value (:border animations)
:transparent? transparent?
:set-full-height? set-full-height?
:screen-height screen-height
:screen-width screen-width
:window-height window-height
:window-width window-width
:props props
:curr-orientation curr-orientation}
:render-data {:opacity-value (:opacity animations)
:border-value (:border animations)
:full-screen-scale (:full-screen-scale animations)
:images-opacity (:images-opacity animations)
:transparent? transparent?
:set-full-height? set-full-height?
:screen-height screen-height
:screen-width screen-width
:window-height window-height
:window-width window-width
:props props
:curr-orientation curr-orientation}
:horizontal horizontal?
:inverted inverted?
:paging-enabled true
@ -113,7 +117,7 @@
handle-items-changed (fn [e]
(on-viewable-items-changed e props state))]
(fn []
(let [animations (utils/init-animations)
(let [animations (utils/init-animations (count messages) index)
derived (utils/init-derived-animations animations)]
(anim/animate (:background-color animations) colors/neutral-100)
(reset! (:data state) messages)

View File

@ -15,3 +15,5 @@
(def ^:const default-duration 300)
(def ^:const default-dimension 1000)
(def ^:const margin 8)

View File

@ -6,6 +6,7 @@
(defn container
[{:keys [width height]}
{:keys [pan-x pan-y pinch-x pinch-y scale]}
full-screen-scale
set-full-height?
portrait?]
(reanimated/apply-animations-to-style
@ -13,7 +14,8 @@
{:translateY pan-y}
{:translateX pinch-x}
{:translateY pinch-y}
{:scale scale}]}
{:scale scale}
{:scale full-screen-scale}]}
{:justify-content :center
:align-items :center
:width (if (or platform/ios? portrait?) width "100%")
@ -22,10 +24,12 @@
(defn image
[{:keys [image-width image-height]}
{:keys [rotate rotate-scale]}
border-radius]
{:keys [border-value images-opacity]}
index]
(reanimated/apply-animations-to-style
{:transform [{:rotate rotate}
{:scale rotate-scale}]
:border-radius border-radius}
:opacity (nth images-opacity index)
:border-radius border-value}
{:width image-width
:height image-height}))

View File

@ -1,31 +1,32 @@
(ns status-im2.contexts.chat.lightbox.zoomable-image.utils
(:require
[clojure.string :as string]
[react-native.core :as rn]
[react-native.navigation :as navigation]
[react-native.orientation :as orientation]
[react-native.platform :as platform]
[react-native.reanimated :as reanimated]
[reagent.core :as reagent]
[status-im2.contexts.chat.lightbox.zoomable-image.constants :as c]
[status-im2.contexts.chat.lightbox.zoomable-image.constants :as constants]
[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)))
(let [duration (if exit? 100 constants/default-duration)]
(anim/animate pinch-x constants/init-offset duration)
(anim/set-val pinch-x-start constants/init-offset)
(anim/animate pan-x constants/init-offset duration)
(anim/set-val pan-x-start constants/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)))
(let [duration (if exit? 100 constants/default-duration)]
(anim/animate pinch-y constants/init-offset duration)
(anim/set-val pinch-y-start constants/init-offset)
(anim/animate pan-y constants/init-offset duration)
(anim/set-val pan-y-start constants/init-offset)))
(defn reset-values
[exit? animations {:keys [focal-x focal-y]}]
@ -40,9 +41,9 @@
{:keys [x-threshold-scale y-threshold-scale]}
{:keys [scale saved-scale] :as animations}
{:keys [pan-x-enabled? pan-y-enabled?] :as state}]
(when (= value c/min-scale)
(when (= value constants/min-scale)
(reset-values exit? animations state))
(anim/animate scale value (if exit? 100 c/default-duration))
(anim/animate scale value (if exit? 100 constants/default-duration))
(anim/set-val saved-scale value)
(reset! pan-x-enabled? (> value x-threshold-scale))
(reset! pan-y-enabled? (> value y-threshold-scale)))
@ -66,8 +67,8 @@
(anim/animate rotate-scale landscape-scale-val))
(= curr-orientation orientation/portrait)
(do
(anim/animate rotate c/init-rotation)
(anim/animate rotate-scale c/min-scale)))
(anim/animate rotate constants/init-rotation)
(anim/animate rotate-scale constants/min-scale)))
(cond
(= curr-orientation orientation/landscape-left)
(do
@ -79,8 +80,8 @@
(anim/set-val rotate-scale landscape-scale-val))
(= curr-orientation orientation/portrait)
(do
(anim/set-val rotate c/init-rotation)
(anim/set-val rotate-scale c/min-scale))))
(anim/set-val rotate constants/init-rotation)
(anim/set-val rotate-scale constants/min-scale))))
(center-x animations false)
(center-y animations false)
(reset! pan-x-enabled? (> (anim/get-val scale) x-threshold-scale))
@ -92,9 +93,9 @@
[exit-lightbox-signal index scale rescale set-full-height?]
(when (= exit-lightbox-signal index)
(reset! set-full-height? false)
(if (> scale c/min-scale)
(if (> scale constants/min-scale)
(do
(rescale c/min-scale true)
(rescale constants/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)))
@ -102,13 +103,16 @@
(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)))
(when (and (= zoom-out-signal index) (> scale constants/min-scale))
(rescale constants/min-scale true)))
(defn toggle-opacity
[index {:keys [opacity-value border-value transparent? props]} portrait?]
(let [{:keys [small-list-ref timers]} props
opacity (reanimated/get-shared-value opacity-value)]
[index {:keys [opacity-value border-value full-screen-scale transparent? props]} portrait?]
(let [{:keys [small-list-ref timers text-sheet-lock?]} props
opacity (reanimated/get-shared-value opacity-value)
scale-value (+ 1
(/ constants/margin
(:width (rn/get-window))))]
(if (= opacity 1)
(do
(js/clearTimeout (:show-0 @timers))
@ -119,7 +123,9 @@
(js/setTimeout #(navigation/merge-options "lightbox" {:statusBar {:visible false}})
(if platform/ios? 75 0)))
(anim/animate opacity-value 0)
(swap! timers assoc :hide-1 (js/setTimeout #(reset! transparent? (not @transparent?)) 400)))
(swap! timers assoc :hide-1 (js/setTimeout #(reset! transparent? (not @transparent?)) 400))
(reset! text-sheet-lock? true)
(js/setTimeout #(reset! text-sheet-lock? false) 300))
(do
(js/clearTimeout (:hide-0 @timers))
(js/clearTimeout (:hide-1 @timers))
@ -135,7 +141,8 @@
:show-2
(js/setTimeout #(navigation/merge-options "lightbox" {:statusBar {:visible true}})
(if platform/ios? 150 50))))))
(anim/animate border-value (if (= opacity 1) 0 16))))
(anim/animate border-value (if (= opacity 1) 0 16))
(anim/animate full-screen-scale (if (= opacity 1) scale-value 1))))
;;; Dimensions
(defn get-dimensions
@ -150,11 +157,12 @@
landscape-image-width (* pixels-width (/ window-width pixels-height))
width (if landscape? landscape-image-width portrait-image-width)
height (if landscape? screen-height portrait-image-height)
container-width (if platform/ios? window-width width)
container-height (if (and platform/ios? landscape?) landscape-image-width height)]
container-width (- (if platform/ios? window-width width) constants/margin)
container-height (- (if (and platform/ios? landscape?) landscape-image-width height)
constants/margin)]
;; width and height used in style prop
{:image-width (if platform/ios? portrait-image-width width)
:image-height (if platform/ios? portrait-image-height height)
{:image-width (- (if platform/ios? portrait-image-width width) constants/margin)
:image-height (- (if platform/ios? portrait-image-height height) constants/margin)
;; container width and height, also used in animations calculations
:width container-width
:height container-height
@ -169,7 +177,7 @@
;;; MATH
(defn get-max-offset
[size screen-size scale]
(/ (- (* size (min scale c/max-scale))
(/ (- (* size (min scale constants/max-scale))
screen-size)
2))
@ -181,8 +189,8 @@
(defn get-double-tap-offset
[size screen-size focal]
(let [center (/ size 2)
target-point (* (- center focal) c/double-tap-scale)
max-offset (get-max-offset size screen-size c/double-tap-scale)
target-point (* (- center focal) constants/double-tap-scale)
max-offset (get-max-offset size screen-size constants/double-tap-scale)
translate-val (min (Math/abs target-point) max-offset)]
(if (neg? target-point) (- translate-val) translate-val)))
@ -201,20 +209,20 @@
;;; INITIALIZATIONS
(defn init-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)
{:scale (anim/use-val constants/min-scale)
:saved-scale (anim/use-val constants/min-scale)
:pan-x-start (anim/use-val constants/init-offset)
:pan-x (anim/use-val constants/init-offset)
:pan-y-start (anim/use-val constants/init-offset)
:pan-y (anim/use-val constants/init-offset)
:pinch-x-start (anim/use-val constants/init-offset)
:pinch-x (anim/use-val constants/init-offset)
:pinch-y-start (anim/use-val constants/init-offset)
:pinch-y (anim/use-val constants/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)})
:rotate (anim/use-val constants/init-rotation)
:rotate-scale (anim/use-val constants/min-scale)})
(defn init-state
[]

View File

@ -221,12 +221,13 @@
[reanimated/view
{:style (style/container dimensions
animations
(:full-screen-scale render-data)
@set-full-height?
(= curr-orientation orientation/portrait))}
[reanimated/fast-image
{:source {:uri (http/replace-port (:image content) (rf/sub [:mediaserver/port]))}
:native-ID (when focused? :shared-element)
:style (style/image dimensions animations (:border-value render-data))}]]]))
:style (style/image dimensions animations render-data index)}]]]))
(defn zoomable-image
[]