Skip to content

Commit 010e330

Browse files
RSNarafacebook-github-bot
authored andcommitted
Refactor ScrollResponder Mixin removal
Summary: In D13307775, ScrollView was changed into a `React.Component` subclass. The solution needed a few touchups, so I added them in this diff. Reviewed By: TheSavior Differential Revision: D13404191 fbshipit-source-id: cba2ddab1fb92a2cbb91b59ac9ae5b5d51d91eb8
1 parent 221e2fe commit 010e330

File tree

2 files changed

+124
-75
lines changed

2 files changed

+124
-75
lines changed

Libraries/Components/ScrollResponder.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,13 @@ import type EmitterSubscription from 'EmitterSubscription';
108108

109109
const IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16;
110110

111-
type State = {
111+
export type State = {|
112112
isTouching: boolean,
113113
lastMomentumScrollBeginTime: number,
114114
lastMomentumScrollEndTime: number,
115115
observedScrollSinceBecomingResponder: boolean,
116116
becameResponderWhileAnimating: boolean,
117-
};
117+
|};
118118

119119
const ScrollResponderMixin = {
120120
_subscriptionKeyboardWillShow: (null: ?EmitterSubscription),
@@ -614,6 +614,7 @@ const ScrollResponderMixin = {
614614
'keyboardWillShow',
615615
this.scrollResponderKeyboardWillShow,
616616
);
617+
617618
this._subscriptionKeyboardWillHide = Keyboard.addListener(
618619
'keyboardWillHide',
619620
this.scrollResponderKeyboardWillHide,

Libraries/Components/ScrollView/ScrollView.js

+121-73
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import type {ViewProps} from 'ViewPropTypes';
3535
import type {PointProp} from 'PointPropType';
3636

3737
import type {ColorValue} from 'StyleSheetTypes';
38+
import type {State as ScrollResponderState} from 'ScrollResponder';
3839

3940
let AndroidScrollView;
4041
let AndroidHorizontalScrollContentView;
@@ -523,8 +524,23 @@ export type Props = $ReadOnly<{|
523524

524525
type State = {|
525526
layoutHeight: ?number,
527+
...ScrollResponderState,
526528
|};
527529

530+
function createScrollResponder(
531+
node: React.ElementRef<typeof ScrollView>,
532+
): typeof ScrollResponder.Mixin {
533+
const scrollResponder = {...ScrollResponder.Mixin};
534+
535+
for (const key in scrollResponder) {
536+
if (typeof scrollResponder[key] === 'function') {
537+
scrollResponder[key] = scrollResponder[key].bind(node);
538+
}
539+
}
540+
541+
return scrollResponder;
542+
}
543+
528544
/**
529545
* Component that wraps platform ScrollView while providing
530546
* integration with touch locking "responder" system.
@@ -561,17 +577,52 @@ type State = {|
561577
* supports out of the box.
562578
*/
563579
class ScrollView extends React.Component<Props, State> {
564-
_createScrollResponder = () => {
565-
const scrollResponder = {...ScrollResponder.Mixin};
580+
/**
581+
* Part 1: Removing ScrollResponder.Mixin:
582+
*
583+
* 1. Mixin methods should be flow typed. That's why we create a
584+
* copy of ScrollResponder.Mixin and attach it to this._scrollResponder.
585+
* Otherwise, we'd have to manually declare each method on the component
586+
* class and assign it a flow type.
587+
* 2. Mixin methods can call component methods, and access the component's
588+
* props and state. So, we need to bind all mixin methods to the
589+
* component instance.
590+
* 3. Continued...
591+
*/
592+
_scrollResponder: typeof ScrollResponder.Mixin = createScrollResponder(this);
593+
594+
constructor(...args) {
595+
super(...args);
596+
597+
/**
598+
* Part 2: Removing ScrollResponder.Mixin
599+
*
600+
* 3. Mixin methods access other mixin methods via dynamic dispatch using
601+
* this. Since mixin methods are bound to the component instance, we need
602+
* to copy all mixin methods to the component instance.
603+
*/
566604
for (const key in ScrollResponder.Mixin) {
567-
if (typeof scrollResponder[key] === 'function') {
568-
scrollResponder[key] = scrollResponder[key].bind(this);
569-
(this: any)[key] = scrollResponder[key].bind(this);
605+
if (
606+
typeof ScrollResponder.Mixin[key] === 'function' &&
607+
key.startsWith('scrollResponder')
608+
) {
609+
(this: any)[key] = ScrollResponder.Mixin[key].bind(this);
570610
}
571611
}
572-
return scrollResponder;
573-
};
574-
_scrollResponder = this._createScrollResponder();
612+
613+
/**
614+
* Part 3: Removing ScrollResponder.Mixin
615+
*
616+
* 4. Mixins can initialize properties and use properties on the component
617+
* instance.
618+
*/
619+
Object.keys(ScrollResponder.Mixin)
620+
.filter(key => typeof ScrollResponder.Mixin[key] !== 'function')
621+
.forEach(key => {
622+
(this: any)[key] = ScrollResponder.Mixin[key];
623+
});
624+
}
625+
575626
_scrollAnimatedValue: AnimatedImplementation.Value = new AnimatedImplementation.Value(
576627
0,
577628
);
@@ -581,6 +632,7 @@ class ScrollView extends React.Component<Props, State> {
581632

582633
state = {
583634
layoutHeight: null,
635+
...ScrollResponder.Mixin.scrollResponderMixinGetInitialState(),
584636
};
585637

586638
UNSAFE_componentWillMount() {
@@ -610,27 +662,27 @@ class ScrollView extends React.Component<Props, State> {
610662
}
611663
}
612664

613-
setNativeProps = (props: Object) => {
665+
setNativeProps(props: Object) {
614666
this._scrollViewRef && this._scrollViewRef.setNativeProps(props);
615-
};
667+
}
616668

617669
/**
618670
* Returns a reference to the underlying scroll responder, which supports
619671
* operations like `scrollTo`. All ScrollView-like components should
620672
* implement this method so that they can be composed while providing access
621673
* to the underlying scroll responder's methods.
622674
*/
623-
getScrollResponder = (): ScrollView => {
675+
getScrollResponder(): ScrollView {
624676
return this;
625-
};
677+
}
626678

627-
getScrollableNode = (): any => {
679+
getScrollableNode(): any {
628680
return ReactNative.findNodeHandle(this._scrollViewRef);
629-
};
681+
}
630682

631-
getInnerViewNode = (): any => {
683+
getInnerViewNode(): any {
632684
return ReactNative.findNodeHandle(this._innerViewRef);
633-
};
685+
}
634686

635687
/**
636688
* Scrolls to a given x, y offset, either immediately or with a smooth animation.
@@ -643,11 +695,11 @@ class ScrollView extends React.Component<Props, State> {
643695
* the function also accepts separate arguments as an alternative to the options object.
644696
* This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED.
645697
*/
646-
scrollTo = (
698+
scrollTo(
647699
y?: number | {x?: number, y?: number, animated?: boolean},
648700
x?: number,
649701
animated?: boolean,
650-
) => {
702+
) {
651703
if (typeof y === 'number') {
652704
console.warn(
653705
'`scrollTo(y, x, animated)` is deprecated. Use `scrollTo({x: 5, y: 5, ' +
@@ -661,7 +713,7 @@ class ScrollView extends React.Component<Props, State> {
661713
y: y || 0,
662714
animated: animated !== false,
663715
});
664-
};
716+
}
665717

666718
/**
667719
* If this is a vertical ScrollView scrolls to the bottom.
@@ -671,40 +723,40 @@ class ScrollView extends React.Component<Props, State> {
671723
* `scrollToEnd({animated: false})` for immediate scrolling.
672724
* If no options are passed, `animated` defaults to true.
673725
*/
674-
scrollToEnd = (options?: {animated?: boolean}) => {
726+
scrollToEnd(options?: {animated?: boolean}) {
675727
// Default to true
676728
const animated = (options && options.animated) !== false;
677729
this._scrollResponder.scrollResponderScrollToEnd({
678730
animated: animated,
679731
});
680-
};
732+
}
681733

682734
/**
683735
* Deprecated, use `scrollTo` instead.
684736
*/
685-
scrollWithoutAnimationTo = (y: number = 0, x: number = 0) => {
737+
scrollWithoutAnimationTo(y: number = 0, x: number = 0) {
686738
console.warn(
687739
'`scrollWithoutAnimationTo` is deprecated. Use `scrollTo` instead',
688740
);
689741
this.scrollTo({x, y, animated: false});
690-
};
742+
}
691743

692744
/**
693745
* Displays the scroll indicators momentarily.
694746
*
695747
* @platform ios
696748
*/
697-
flashScrollIndicators = () => {
749+
flashScrollIndicators() {
698750
this._scrollResponder.scrollResponderFlashScrollIndicators();
699-
};
751+
}
700752

701-
_getKeyForIndex = (index, childArray) => {
753+
_getKeyForIndex(index, childArray) {
702754
// $FlowFixMe Invalid prop usage
703755
const child = childArray[index];
704756
return child && child.key;
705-
};
757+
}
706758

707-
_updateAnimatedNodeAttachment = () => {
759+
_updateAnimatedNodeAttachment() {
708760
if (this._scrollAnimatedValueAttachment) {
709761
this._scrollAnimatedValueAttachment.detach();
710762
}
@@ -718,18 +770,19 @@ class ScrollView extends React.Component<Props, State> {
718770
[{nativeEvent: {contentOffset: {y: this._scrollAnimatedValue}}}],
719771
);
720772
}
721-
};
773+
}
722774

723-
_setStickyHeaderRef = (key, ref) => {
775+
_setStickyHeaderRef(key, ref) {
724776
if (ref) {
725777
this._stickyHeaderRefs.set(key, ref);
726778
} else {
727779
this._stickyHeaderRefs.delete(key);
728780
}
729-
};
781+
}
730782

731-
_onStickyHeaderLayout = (index, event, key) => {
732-
if (!this.props.stickyHeaderIndices) {
783+
_onStickyHeaderLayout(index, event, key) {
784+
const {stickyHeaderIndices} = this.props;
785+
if (!stickyHeaderIndices) {
733786
return;
734787
}
735788
const childArray = React.Children.toArray(this.props.children);
@@ -741,19 +794,15 @@ class ScrollView extends React.Component<Props, State> {
741794
const layoutY = event.nativeEvent.layout.y;
742795
this._headerLayoutYs.set(key, layoutY);
743796

744-
// $FlowFixMe
745-
const indexOfIndex = this.props.stickyHeaderIndices.indexOf(index);
746-
const previousHeaderIndex = this.props.stickyHeaderIndices[
747-
// $FlowFixMe
748-
indexOfIndex - 1
749-
];
797+
const indexOfIndex = stickyHeaderIndices.indexOf(index);
798+
const previousHeaderIndex = stickyHeaderIndices[indexOfIndex - 1];
750799
if (previousHeaderIndex != null) {
751800
const previousHeader = this._stickyHeaderRefs.get(
752801
this._getKeyForIndex(previousHeaderIndex, childArray),
753802
);
754803
previousHeader && previousHeader.setNextHeaderY(layoutY);
755804
}
756-
};
805+
}
757806

758807
_handleScroll = (e: Object) => {
759808
if (__DEV__) {
@@ -774,7 +823,7 @@ class ScrollView extends React.Component<Props, State> {
774823
if (Platform.OS === 'android') {
775824
if (
776825
this.props.keyboardDismissMode === 'on-drag' &&
777-
this._scrollResponder.isTouching
826+
this.state.isTouching
778827
) {
779828
dismissKeyboard();
780829
}
@@ -858,40 +907,39 @@ class ScrollView extends React.Component<Props, State> {
858907
}
859908

860909
const {stickyHeaderIndices} = this.props;
910+
let children = this.props.children;
911+
912+
if (stickyHeaderIndices != null && stickyHeaderIndices.length > 0) {
913+
const childArray = React.Children.toArray(this.props.children);
914+
915+
children = childArray.map((child, index) => {
916+
const indexOfIndex = child ? stickyHeaderIndices.indexOf(index) : -1;
917+
if (indexOfIndex > -1) {
918+
const key = child.key;
919+
const nextIndex = stickyHeaderIndices[indexOfIndex + 1];
920+
return (
921+
<ScrollViewStickyHeader
922+
key={key}
923+
ref={ref => this._setStickyHeaderRef(key, ref)}
924+
nextHeaderLayoutY={this._headerLayoutYs.get(
925+
this._getKeyForIndex(nextIndex, childArray),
926+
)}
927+
onLayout={event => this._onStickyHeaderLayout(index, event, key)}
928+
scrollAnimatedValue={this._scrollAnimatedValue}
929+
inverted={this.props.invertStickyHeaders}
930+
scrollViewHeight={this.state.layoutHeight}>
931+
{child}
932+
</ScrollViewStickyHeader>
933+
);
934+
} else {
935+
return child;
936+
}
937+
});
938+
}
939+
861940
const hasStickyHeaders =
862941
stickyHeaderIndices && stickyHeaderIndices.length > 0;
863-
const childArray =
864-
hasStickyHeaders && React.Children.toArray(this.props.children);
865-
const children = hasStickyHeaders
866-
? // $FlowFixMe Invalid prop usage
867-
childArray.map((child, index) => {
868-
// $FlowFixMe
869-
const indexOfIndex = child ? stickyHeaderIndices.indexOf(index) : -1;
870-
if (indexOfIndex > -1) {
871-
const key = child.key;
872-
// $FlowFixMe
873-
const nextIndex = stickyHeaderIndices[indexOfIndex + 1];
874-
return (
875-
<ScrollViewStickyHeader
876-
key={key}
877-
ref={ref => this._setStickyHeaderRef(key, ref)}
878-
nextHeaderLayoutY={this._headerLayoutYs.get(
879-
this._getKeyForIndex(nextIndex, childArray),
880-
)}
881-
onLayout={event =>
882-
this._onStickyHeaderLayout(index, event, key)
883-
}
884-
scrollAnimatedValue={this._scrollAnimatedValue}
885-
inverted={this.props.invertStickyHeaders}
886-
scrollViewHeight={this.state.layoutHeight}>
887-
{child}
888-
</ScrollViewStickyHeader>
889-
);
890-
} else {
891-
return child;
892-
}
893-
})
894-
: this.props.children;
942+
895943
const contentContainer = (
896944
<ScrollContentContainerViewClass
897945
{...contentSizeChangeProps}

0 commit comments

Comments
 (0)