diff --git a/common/cpp/react/renderer/components/rnscreens/FrameCorrectionModes.h b/common/cpp/react/renderer/components/rnscreens/FrameCorrectionModes.h new file mode 100644 index 0000000000..37fd90561a --- /dev/null +++ b/common/cpp/react/renderer/components/rnscreens/FrameCorrectionModes.h @@ -0,0 +1,51 @@ +#pragma once + +#include + +namespace rnscreens { + +/** + * Flags describing types of corrections to apply to Screen frame + * after layout process. + */ +class FrameCorrectionModes final { + public: + enum Mode : std::uint8_t { + /** + * No correction should be applied to layout metrics of Screen + */ + None = 0, + + /** + * Screen's frame height should be corrected + */ + FrameHeightCorrection = 1 << 0, + + /** + * Screen's frame origin should be corrected + */ + FrameOriginCorrection = 1 << 1, + }; + + inline void set(Mode mode) { + modes_ = Mode(modes_ | mode); + } + + inline void unset(Mode mode) { + modes_ = Mode(modes_ & ~mode); + } + + // Check whether current set of flags contains all flags set in argument. + inline bool check(Mode mode) const { + return Mode(modes_ & mode) == mode; + } + + inline Mode getAll() const { + return modes_; + } + + private: + Mode modes_{Mode::None}; +}; + +} // namespace rnscreens diff --git a/common/cpp/react/renderer/components/rnscreens/RNSScreenComponentDescriptor.h b/common/cpp/react/renderer/components/rnscreens/RNSScreenComponentDescriptor.h index 94e2a2f2e9..7977e5c9bb 100644 --- a/common/cpp/react/renderer/components/rnscreens/RNSScreenComponentDescriptor.h +++ b/common/cpp/react/renderer/components/rnscreens/RNSScreenComponentDescriptor.h @@ -1,13 +1,19 @@ #pragma once +#ifdef ANDROID +#include +#endif #include #include #include #include "RNSScreenShadowNode.h" +#include "utils/RectUtil.h" namespace facebook { namespace react { +using namespace rnscreens; + class RNSScreenComponentDescriptor final : public ConcreteComponentDescriptor { public: @@ -30,9 +36,35 @@ class RNSScreenComponentDescriptor final #ifdef ANDROID if (stateData.frameSize.width != 0 && stateData.frameSize.height != 0) { // When we receive dimensions from JVM side we can remove padding used for - // correction + // correction, and we can stop applying height correction for the frame. + // We want to leave top offset correction though intact. + // TODO: In future, when we have dynamic header height we might want to + // update Y offset correction here. + +#ifdef REACT_NATIVE_DEBUG + // We use the fact that height correction is disabled once we receive + // state from the native, so when we have incoming state & height + // correction is still enabled, we know this is the very first native + // state update. + if (screenShadowNode.getFrameCorrectionModes().check( + FrameCorrectionModes::Mode::FrameHeightCorrection) && + !checkFrameSizesEqualWithEps( + screenShadowNode.layoutMetrics_.frame.size, + stateData.frameSize)) { + LOG(ERROR) + << "[RNScreens] The first frame received from state update: " + << stateData.frameSize.width << "x" << stateData.frameSize.height + << " differs from the one expected: " + << screenShadowNode.layoutMetrics_.frame.size.width << "x" + << screenShadowNode.layoutMetrics_.frame.size.height + << ". This is most likely a react-native-screens library bug. Please report this at https://github.com/software-mansion/react-native-screens/issues"; + } +#endif screenShadowNode.setPadding({0, 0, 0, 0}); + screenShadowNode.getFrameCorrectionModes().unset( + FrameCorrectionModes::Mode::FrameHeightCorrection); + layoutableShadowNode.setSize( Size{stateData.frameSize.width, stateData.frameSize.height}); } diff --git a/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.cpp b/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.cpp index c21ec56da2..c92b1237f6 100644 --- a/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.cpp +++ b/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.cpp @@ -3,6 +3,9 @@ namespace facebook { namespace react { +namespace yoga = facebook::yoga; +using namespace rnscreens; + extern const char RNSScreenComponentName[] = "RNSScreen"; Point RNSScreenShadowNode::getContentOriginOffset( @@ -106,12 +109,53 @@ void RNSScreenShadowNode::appendChild(const ShadowNode::Shared &child) { .value_or(0.f); screenShadowNode.setPadding({0, 0, 0, headerHeight}); + screenShadowNode.setHeaderHeight(headerHeight); + screenShadowNode.getFrameCorrectionModes().set( + FrameCorrectionModes::Mode( + FrameCorrectionModes::Mode::FrameHeightCorrection | + FrameCorrectionModes::Mode::FrameOriginCorrection)); } } #endif // ANDROID } +void RNSScreenShadowNode::layout(facebook::react::LayoutContext layoutContext) { + YogaLayoutableShadowNode::layout(layoutContext); + #ifdef ANDROID + applyFrameCorrections(); +#endif // ANDROID +} + +#ifdef ANDROID +void RNSScreenShadowNode::applyFrameCorrections() { + ensureUnsealed(); + + // On the very first layout we want to correct both Y offset and frame size. + // On consecutive layouts we want to correct only Y offset, as the frame size + // is received from JVM side. This is done so if the Screen dimensions are + // read from ShadowTree (e.g by reanimated) they have chance of being + // accurate. On JVM side we do ignore this frame anyway, because + // ScreenStackViewManager.needsCustomLayoutForChildren() == true. + const auto &stateData = getStateData(); + const float lastKnownHeaderHeight = stateData.getLastKnownHeaderHeight(); + const auto &headerCorrectionModes = stateData.getHeaderCorrectionModes(); + layoutMetrics_.frame.origin.y += lastKnownHeaderHeight * + headerCorrectionModes.check( + FrameCorrectionModes::Mode::FrameOriginCorrection); + layoutMetrics_.frame.size.height -= lastKnownHeaderHeight * + headerCorrectionModes.check( + FrameCorrectionModes::Mode::FrameHeightCorrection); +} + +void RNSScreenShadowNode::setHeaderHeight(float headerHeight) { + getStateDataMutable().setHeaderHeight(headerHeight); +} + +FrameCorrectionModes &RNSScreenShadowNode::getFrameCorrectionModes() { + return getStateDataMutable().getFrameCorrectionModes(); +} + RNSScreenShadowNode::StateData &RNSScreenShadowNode::getStateDataMutable() { // We assume that this method is called to mutate the data, so we ensure // we're unsealed. diff --git a/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.h b/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.h index 98cc8f216d..3a4e723c4a 100644 --- a/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.h +++ b/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.h @@ -5,11 +5,14 @@ #include #include #include +#include "FrameCorrectionModes.h" #include "RNSScreenState.h" namespace facebook { namespace react { +using namespace rnscreens; + JSI_EXPORT extern const char RNSScreenComponentName[]; class JSI_EXPORT RNSScreenShadowNode final : public ConcreteViewShadowNode< @@ -27,12 +30,18 @@ class JSI_EXPORT RNSScreenShadowNode final : public ConcreteViewShadowNode< void appendChild(const ShadowNode::Shared& child) override; + void layout(LayoutContext layoutContext) override; + #pragma mark - Custom interface void setHeaderHeight(float headerHeight); + FrameCorrectionModes &getFrameCorrectionModes(); + private: #ifdef ANDROID + void applyFrameCorrections(); + StateData &getStateDataMutable(); #endif // ANDROID }; diff --git a/common/cpp/react/renderer/components/rnscreens/RNSScreenState.cpp b/common/cpp/react/renderer/components/rnscreens/RNSScreenState.cpp index 66e75b99ef..d5c9876303 100644 --- a/common/cpp/react/renderer/components/rnscreens/RNSScreenState.cpp +++ b/common/cpp/react/renderer/components/rnscreens/RNSScreenState.cpp @@ -3,6 +3,8 @@ namespace facebook { namespace react { +using namespace rnscreens; + #ifdef ANDROID folly::dynamic RNSScreenState::getDynamic() const { return folly::dynamic::object("frameWidth", frameSize.width)( @@ -10,6 +12,23 @@ folly::dynamic RNSScreenState::getDynamic() const { "contentOffsetY", contentOffset.y); } +void RNSScreenState::setHeaderHeight(float headerHeight) { + lastKnownHeaderHeight_ = headerHeight; +} + +float RNSScreenState::getLastKnownHeaderHeight() const noexcept { + return lastKnownHeaderHeight_; +} + +FrameCorrectionModes &RNSScreenState::getFrameCorrectionModes() noexcept { + return headerCorrectionModes_; +} + +const FrameCorrectionModes &RNSScreenState::getHeaderCorrectionModes() + const noexcept { + return headerCorrectionModes_; +} + #endif } // namespace react diff --git a/common/cpp/react/renderer/components/rnscreens/RNSScreenState.h b/common/cpp/react/renderer/components/rnscreens/RNSScreenState.h index 4016d817c1..c02b234596 100644 --- a/common/cpp/react/renderer/components/rnscreens/RNSScreenState.h +++ b/common/cpp/react/renderer/components/rnscreens/RNSScreenState.h @@ -9,9 +9,13 @@ #include #endif +#include "FrameCorrectionModes.h" + namespace facebook { namespace react { +using namespace rnscreens; + class JSI_EXPORT RNSScreenState final { public: using Shared = std::shared_ptr; @@ -27,7 +31,9 @@ class JSI_EXPORT RNSScreenState final { (Float)data["frameHeight"].getDouble()}), contentOffset(Point{ (Float)data["contentOffsetX"].getDouble(), - (Float)data["contentOffsetY"].getDouble()}){}; + (Float)data["contentOffsetY"].getDouble()}), + lastKnownHeaderHeight_{previousState.lastKnownHeaderHeight_}, + headerCorrectionModes_{previousState.headerCorrectionModes_} {}; #endif const Size frameSize{}; @@ -38,8 +44,25 @@ class JSI_EXPORT RNSScreenState final { MapBuffer getMapBuffer() const { return MapBufferBuilder::EMPTY(); }; + + void setHeaderHeight(float headerHeight); + + float getLastKnownHeaderHeight() const noexcept; + + FrameCorrectionModes &getFrameCorrectionModes() noexcept; + + const FrameCorrectionModes &getHeaderCorrectionModes() const noexcept; + #endif + private: +#ifdef ANDROID + // Header height as measured on dummy layout + float lastKnownHeaderHeight_{0.f}; + + FrameCorrectionModes headerCorrectionModes_{}; +#endif // ANDROID + #pragma mark - Getters }; diff --git a/common/cpp/react/renderer/components/rnscreens/utils/RectUtil.h b/common/cpp/react/renderer/components/rnscreens/utils/RectUtil.h new file mode 100644 index 0000000000..b7407172b5 --- /dev/null +++ b/common/cpp/react/renderer/components/rnscreens/utils/RectUtil.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include + +namespace rnscreens { + +namespace react = facebook::react; + +template + requires std::is_floating_point_v +inline constexpr bool equalWithRespectToEps(const T a, const T b, const T eps) { + return std::abs(a - b) <= eps; +} + +/** + * Compares given two frame sizes with respect to the epsilon. + * + * @param first first frame size + * @param second second frame size + * @param eps comparison precision, defaults to 0.01, which should ensure that + * precision of comparison is under 1px + * @return whether the frame dimensions are the same with respect to given + * epsilon + */ +inline constexpr bool checkFrameSizesEqualWithEps( + const react::Size &first, + const react::Size &second, + const react::Float eps = 0.01) { + return equalWithRespectToEps(first.width, second.width, eps) && + equalWithRespectToEps(first.height, second.height, eps); +} + +} // namespace rnscreens