Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,9 @@ export type ScrollViewPropsIOS = Readonly<{
* scrolls under the system UI. Across it, every value except `'never'` pads
* the content container by the insets, so content is laid out clear of the
* system UI, such as the rail on iPhone Duo or camera cutout in landscape,
* while the container's background still extends edge to edge.
* while the container's background still extends edge to edge. On every
* edge, the `contentContainerStyle` padding counts toward the inset, so
* content sits at the larger of the two.
*
* @default `'never'`
* @platform ios
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,19 @@ NS_ASSUME_NONNULL_BEGIN
@interface RCTScrollContentViewComponentView : RCTViewComponentView

/*
* The safe area the content is laid out clear of, as padding on top of the content container's own. The enclosing
* The safe area the content is laid out clear of, as a floor on the content container's own padding. The enclosing
* scroll view sets the pair of insets across its scrollable axis, and zero while its `contentInsetAdjustmentBehavior`
* is `never`.
*/
@property (nonatomic, assign) UIEdgeInsets safeAreaPadding;
@property (nonatomic, readonly) UIEdgeInsets safeAreaPadding;

/*
* The content inset UIKit adds for the safe area along the scrollable axis, which the content container's own padding
* on those edges counts toward. The enclosing scroll view sets the pair of insets along its scrollable axis.
*/
@property (nonatomic, readonly) UIEdgeInsets contentInsetAdjustment;

- (void)setSafeAreaPadding:(UIEdgeInsets)safeAreaPadding contentInsetAdjustment:(UIEdgeInsets)contentInsetAdjustment;

@end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,32 @@ - (instancetype)initWithFrame:(CGRect)frame
return self;
}

- (void)setSafeAreaPadding:(UIEdgeInsets)safeAreaPadding
- (void)setSafeAreaPadding:(UIEdgeInsets)safeAreaPadding contentInsetAdjustment:(UIEdgeInsets)contentInsetAdjustment
{
_safeAreaPadding = safeAreaPadding;
_contentInsetAdjustment = contentInsetAdjustment;
[self _updateStateIfNecessary];
}

static EdgeInsets RCTRoundedEdgeInsets(UIEdgeInsets insets)
{
return {
.left = RCTRoundPixelValue(insets.left),
.top = RCTRoundPixelValue(insets.top),
.right = RCTRoundPixelValue(insets.right),
.bottom = RCTRoundPixelValue(insets.bottom)};
}

- (void)_updateStateIfNecessary
{
if (!_state) {
return;
}

UIEdgeInsets insets = _safeAreaPadding;
insets.left = RCTRoundPixelValue(insets.left);
insets.top = RCTRoundPixelValue(insets.top);
insets.right = RCTRoundPixelValue(insets.right);
insets.bottom = RCTRoundPixelValue(insets.bottom);

RCTI18nUtil *i18nUtil = [RCTI18nUtil sharedInstance];
ScrollContentViewState newData;
newData.safeAreaPadding = RCTEdgeInsetsFromUIEdgeInsets(insets);
newData.safeAreaPadding = RCTRoundedEdgeInsets(_safeAreaPadding);
newData.contentInsetAdjustment = RCTRoundedEdgeInsets(_contentInsetAdjustment);
newData.swapLeftAndRightInRTL = i18nUtil.isRTL && i18nUtil.doLeftAndRightSwapInRTL;
if (newData == _lastStateData) {
return;
Expand Down Expand Up @@ -90,6 +95,7 @@ - (void)prepareForRecycle
_state.reset();
_lastStateData = {};
_safeAreaPadding = UIEdgeInsetsZero;
_contentInsetAdjustment = UIEdgeInsetsZero;
}

@end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -888,10 +888,16 @@ - (void)layoutSubviews
[self _updateContentViewSafeAreaPadding];
}

- (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView
{
[self _updateContentViewSafeAreaPadding];
}

/*
* Hands the content container the safe area to pad its content by: the pair of insets across the scrollable axis,
* unless `contentInsetAdjustmentBehavior` is `never`. Along the scrollable axis UIKit turns the safe area into content
* insets instead, so content scrolls under the system UI.
* Hands the content container the safe area to lay its content out clear of, unless `contentInsetAdjustmentBehavior`
* is `never`: the pair of insets across the scrollable axis, which it pads by, and along the axis the content inset
* UIKit adds for the safe area, which its own padding counts toward. Content still scrolls under the system UI along
* the axis.
*/
- (void)_updateContentViewSafeAreaPadding
{
Expand All @@ -900,17 +906,24 @@ - (void)_updateContentViewSafeAreaPadding
}
const auto &props = static_cast<const ScrollViewProps &>(*_props);
UIEdgeInsets padding = UIEdgeInsetsZero;
UIEdgeInsets contentInset = UIEdgeInsetsZero;
if (props.contentInsetAdjustmentBehavior != ContentInsetAdjustmentBehavior::Never) {
UIEdgeInsets insets = self.safeAreaInsets;
UIEdgeInsets adjusted = _scrollView.adjustedContentInset;
UIEdgeInsets current = _scrollView.contentInset;
if (props.horizontal) {
padding.top = insets.top;
padding.bottom = insets.bottom;
contentInset.left = adjusted.left - current.left;
contentInset.right = adjusted.right - current.right;
} else {
padding.left = insets.left;
padding.right = insets.right;
contentInset.top = adjusted.top - current.top;
contentInset.bottom = adjusted.bottom - current.bottom;
}
}
((RCTScrollContentViewComponentView *)_contentView).safeAreaPadding = padding;
[(RCTScrollContentViewComponentView *)_contentView setSafeAreaPadding:padding contentInsetAdjustment:contentInset];
}

- (void)_handleScrollEndIfNeeded
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include "ScrollContentViewShadowNode.h"

#include <algorithm>
#include <array>

namespace facebook::react {
Expand Down Expand Up @@ -78,16 +79,21 @@ StyleLength resolvedPadding(
}

/*
* The safe area is a floor on the padding, as it is on UIKit's layout
* margins: the edge takes the larger of its style padding and the inset.
* Where UIKit already applies the inset as a content inset, the edge keeps
* what its padding exceeds that by, which lands the content on the same floor.
* A percentage padding keeps the edge as the style set it, with no inset
* added, since Yoga holds one length per edge and a percentage has no point
* value to add to.
* applied, since Yoga holds one length per edge and a percentage has no point
* value to compare.
*/
StyleLength padded(StyleLength value, Float inset) {
if (inset == 0 || value.isPercent()) {
StyleLength padded(StyleLength value, Float inset, Float contentInset) {
if ((inset == 0 && contentInset == 0) || value.isPercent()) {
return value;
}
auto base = value.isPoints() ? value.value().unwrap() : 0.0f;
return StyleLength::points(base + inset);
auto base = value.isPoints() ? Float{value.value().unwrap()} : Float{0};
return StyleLength::points(
std::max(std::max(base, inset) - contentInset, Float{0}));
}

} // namespace
Expand Down Expand Up @@ -131,22 +137,31 @@ void ScrollContentViewShadowNode::applySafeAreaPadding() {
const ScrollContentViewState& state,
LayoutDirection layoutDirection) {
const auto& insets = state.safeAreaPadding;
const auto& contentInsets = state.contentInsetAdjustment;
auto swap = state.swapLeftAndRightInRTL;
if (insets.top != 0 || insets.bottom != 0) {
if (insets.top != 0 || insets.bottom != 0 || contentInsets.top != 0 ||
contentInsets.bottom != 0) {
auto top = padded(
resolvedPadding(style, Edge::Top, layoutDirection, swap), insets.top);
resolvedPadding(style, Edge::Top, layoutDirection, swap),
insets.top,
contentInsets.top);
auto bottom = padded(
resolvedPadding(style, Edge::Bottom, layoutDirection, swap),
insets.bottom);
insets.bottom,
contentInsets.bottom);
style.setPadding(Edge::Top, top);
style.setPadding(Edge::Bottom, bottom);
}
if (insets.left != 0 || insets.right != 0) {
if (insets.left != 0 || insets.right != 0 || contentInsets.left != 0 ||
contentInsets.right != 0) {
auto left = padded(
resolvedPadding(style, Edge::Left, layoutDirection, swap), insets.left);
resolvedPadding(style, Edge::Left, layoutDirection, swap),
insets.left,
contentInsets.left);
auto right = padded(
resolvedPadding(style, Edge::Right, layoutDirection, swap),
insets.right);
insets.right,
contentInsets.right);
// The logical edges take precedence over every other padding edge along
// the row. The physical edges are cleared, since at layout they are copied
// over the logical ones when left and right swap in RTL.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ extern const char ScrollContentViewComponentName[];

/*
* `ShadowNode` for the <ScrollContentView> component, the content container of
* a <ScrollView>. It pads its content by the safe area in its state, on top of
* the padding its style sets.
* a <ScrollView>. It lays its content out clear of the safe area in its state
* by at least the padding its style sets: across the scrollable axis as
* padding, and along it as what the padding exceeds UIKit's content inset by.
*/
class ScrollContentViewShadowNode final : public ConcreteViewShadowNode<
ScrollContentViewComponentName,
Expand All @@ -46,10 +47,11 @@ class ScrollContentViewShadowNode final : public ConcreteViewShadowNode<
void applySafeAreaPadding();

/*
* Pads `style` by `state.safeAreaPadding`, on top of the padding `style`
* resolves to on each edge, following the precedence Yoga gives the padding
* edges for `layoutDirection`. An edge with a percentage padding keeps it
* and gets no inset.
* Raises the padding `style` resolves to on each edge to at least
* `state.safeAreaPadding`, less `state.contentInsetAdjustment` where UIKit
* already applies the safe area as a content inset, following the precedence
* Yoga gives the padding edges for `layoutDirection`. An edge with a
* percentage padding keeps it and gets no inset.
*/
static void padForSafeArea(yoga::Style &style, const ScrollContentViewState &state, LayoutDirection layoutDirection);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,19 @@ namespace facebook::react {
class ScrollContentViewState final {
public:
/*
* The safe area the content is laid out clear of, as padding on top of the
* padding the style sets. The enclosing scroll view sets the pair of edges
* across its scrollable axis.
* The safe area the content is laid out clear of, as a floor on the padding
* the style sets. The enclosing scroll view sets the pair of edges across
* its scrollable axis.
*/
EdgeInsets safeAreaPadding{};

/*
* The content inset UIKit adds for the safe area along the scrollable axis,
* which the padding the style sets on those edges counts toward. The
* enclosing scroll view sets the pair of edges along its scrollable axis.
*/
EdgeInsets contentInsetAdjustment{};

/*
* Whether left and right swap in RTL, which decides the padding edges the
* safe area adds to.
Expand All @@ -48,6 +55,11 @@ class ScrollContentViewState final {
(Float)data["top"].getDouble(),
(Float)data["right"].getDouble(),
(Float)data["bottom"].getDouble()}),
contentInsetAdjustment(
{(Float)data["contentInsetLeft"].getDouble(),
(Float)data["contentInsetTop"].getDouble(),
(Float)data["contentInsetRight"].getDouble(),
(Float)data["contentInsetBottom"].getDouble()}),
swapLeftAndRightInRTL(data["swapLeftAndRightInRTL"].getBool())
{
}
Expand All @@ -56,6 +68,8 @@ class ScrollContentViewState final {
{
return folly::dynamic::object("left", safeAreaPadding.left)("top", safeAreaPadding.top)(
"right", safeAreaPadding.right)("bottom", safeAreaPadding.bottom)(
"contentInsetLeft", contentInsetAdjustment.left)("contentInsetTop", contentInsetAdjustment.top)(
"contentInsetRight", contentInsetAdjustment.right)("contentInsetBottom", contentInsetAdjustment.bottom)(
"swapLeftAndRightInRTL", swapLeftAndRightInRTL);
}
#endif
Expand Down
Loading
Loading