diff --git a/src/status_im/ui/components/bottom_sheet/styles.cljs b/src/status_im/ui/components/bottom_sheet/styles.cljs new file mode 100644 index 0000000000..a046f48a55 --- /dev/null +++ b/src/status_im/ui/components/bottom_sheet/styles.cljs @@ -0,0 +1,56 @@ +(ns status-im.ui.components.bottom-sheet.styles + (:require [status-im.ui.components.colors :as colors] + [status-im.utils.platform :as platform])) + +(def border-radius 16) +(def bottom-padding (if platform/iphone-x? 34 8)) +(def bottom-view-height 1000) + +(def container + {:position :absolute + :left 0 + :top 0 + :right 0 + :bottom 0 + :flex 1 + :justify-content :flex-end}) + +(defn shadow [opacity-value] + {:flex 1 + :position :absolute + :left 0 + :top 0 + :right 0 + :bottom 0 + :opacity opacity-value + :background-color colors/black-transparent-40}) + +(defn content-container + [content-height bottom-value] + {:background-color colors/white + :border-top-left-radius border-radius + :border-top-right-radius border-radius + :height (+ content-height border-radius bottom-view-height) + :bottom (- bottom-view-height) + :align-self :stretch + :transform [{:translateY bottom-value}] + :justify-content :flex-start + :align-items :center + :padding-bottom bottom-padding}) + +(def content-header + {:height border-radius + :align-self :stretch + :justify-content :center + :align-items :center}) + +(def handle + {:width 31 + :height 4 + :background-color colors/gray-transparent-40 + :border-radius 2}) + +(def bottom-view + {:background-color colors/white + :height bottom-view-height + :align-self :stretch}) diff --git a/src/status_im/ui/components/bottom_sheet/view.cljs b/src/status_im/ui/components/bottom_sheet/view.cljs new file mode 100644 index 0000000000..218237f0fa --- /dev/null +++ b/src/status_im/ui/components/bottom_sheet/view.cljs @@ -0,0 +1,146 @@ +(ns status-im.ui.components.bottom-sheet.view + (:require [status-im.ui.components.react :as react] + [status-im.ui.components.animation :as animation] + [status-im.ui.components.bottom-sheet.styles :as styles] + [reagent.core :as reagent])) + +(def initial-animation-duration 300) +(def release-animation-duration 150) +(def cancellation-animation-duration 100) +(def swipe-opacity-range 100) +(def cancellation-height 180) +(def min-opacity 0.05) +(def min-velocity 0.1) + +(defn- animate + [{:keys [opacity new-opacity-value + bottom new-bottom-value + duration callback]}] + (animation/start + (animation/parallel + [(animation/timing opacity + {:toValue new-opacity-value + :duration duration + :useNativeDriver true}) + (animation/timing bottom + {:toValue new-bottom-value + :duration duration + :useNativeDriver true})]) + (when (fn? callback) callback))) + +(defn animate-sign-panel + [opacity-value bottom-value] + (animate {:bottom bottom-value + :new-bottom-value 0 + :opacity opacity-value + :new-opacity-value 1 + :duration initial-animation-duration})) + +(defn- on-move + [{:keys [height bottom-value opacity-value]}] + (fn [_ state] + (let [dy (.-dy state)] + (cond (pos? dy) + (let [opacity (max min-opacity (- 1 (/ dy (- height swipe-opacity-range))))] + (animation/set-value bottom-value dy) + (animation/set-value opacity-value opacity)) + (neg? dy) + (animation/set-value bottom-value (/ dy 2)))))) + +(defn cancelled? [height dy vy] + (or + (<= min-velocity vy) + (> cancellation-height (- height dy)))) + +(defn- cancel + ([opts] (cancel opts nil)) + ([{:keys [height bottom-value show-sheet? opacity-value]} callback] + (animate {:bottom bottom-value + :new-bottom-value height + :opacity opacity-value + :new-opacity-value 0 + :duration cancellation-animation-duration + :callback #(do (reset! show-sheet? false) + (animation/set-value bottom-value height) + (animation/set-value opacity-value 0) + (when (fn? callback) (callback)))}))) + +(defn- on-release + [{:keys [height bottom-value opacity-value on-cancel] :as opts}] + (fn [_ state] + (let [{:strs [dy vy]} (js->clj state)] + (if (cancelled? height dy vy) + (cancel opts on-cancel) + (animate {:bottom bottom-value + :new-bottom-value 0 + :opacity opacity-value + :new-opacity-value 1 + :duration release-animation-duration}))))) + +(defn swipe-pan-responder [opts] + (.create + react/pan-responder + (clj->js + {:onMoveShouldSetPanResponder (fn [_ state] + (or (< 10 (js/Math.abs (.-dx state))) + (< 5 (js/Math.abs (.-dy state))))) + :onPanResponderMove (on-move opts) + :onPanResponderRelease (on-release opts) + :onPanResponderTerminate (on-release opts)}))) + +(defn pan-handlers [pan-responder] + (js->clj (.-panHandlers pan-responder))) + +(defn- bottom-sheet-view + [{:keys [opacity-value bottom-value]}] + (reagent.core/create-class + {:component-did-mount + #(animate-sign-panel opacity-value bottom-value) + :reagent-render + (fn [{:keys [opacity-value bottom-value + height content on-cancel] + :as opts}] + [react/view + (merge + (pan-handlers (swipe-pan-responder opts)) + {:style styles/container}) + [react/touchable-highlight + {:on-press #(cancel opts on-cancel) + :style styles/container} + + [react/animated-view (styles/shadow opacity-value)]] + [react/animated-view + {:style (styles/content-container height bottom-value)} + [react/view styles/content-header + [react/view styles/handle]] + content + [react/view {:style styles/bottom-view}]]])})) + +(defn bottom-sheet + [{:keys [show? content-height on-cancel]} _] + {:pre [(fn? on-cancel) (pos? content-height)]} + (let [show-sheet? (reagent/atom show?) + total-content-height (+ content-height styles/border-radius + styles/bottom-padding) + bottom-value (animation/create-value total-content-height) + opacity-value (animation/create-value 0) + opts {:height total-content-height + :bottom-value bottom-value + :opacity-value opacity-value + :show-sheet? show-sheet? + :on-cancel on-cancel}] + (reagent.core/create-class + {:component-will-update + (fn [this [_ new-args]] + (let [old-args (second (.-argv (.-props this))) + old-show? (:show? old-args) + new-show? (:show? new-args)] + (cond (and (not old-show?) new-show?) + (reset! show-sheet? true) + + (and old-show? (not new-show?) (true? @show-sheet?)) + (cancel opts)))) + :reagent-render + (fn [_ content] + (when @show-sheet? + [bottom-sheet-view (assoc opts :content content)]))}))) diff --git a/src/status_im/ui/components/colors.cljs b/src/status_im/ui/components/colors.cljs index 2f6b929fbe..572a21113c 100644 --- a/src/status_im/ui/components/colors.cljs +++ b/src/status_im/ui/components/colors.cljs @@ -22,10 +22,12 @@ ;; BLACK (def black "#000000") ;; Used as the default text color (def black-transparent (alpha black 0.1)) ;; Used as background color for rounded button on dark background and as background color for containers like "Backup seed phrase" +(def black-transparent-40 (alpha black 0.4)) (def gray-light black-transparent) ;; Used as divider color ;; DARK GREY (def gray "#939ba1") ;; Dark grey, used as a background for a light foreground and as section header and secondary text color +(def gray-transparent-40 (alpha gray 0.4)) ;; LIGHT GREY (def gray-lighter "#eef2f5") ;; Light Grey, used as a background or shadow