Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Android] Implement ScrollView sticky headers on Android #9957

Closed
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
1 change: 1 addition & 0 deletions Examples/UIExplorer/js/ListViewPagingExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ class ListViewPagingExample extends React.Component {
initialListSize={10}
pageSize={4}
scrollRenderAheadDistance={500}
stickySectionHeaders={true}
/>
);
}
Expand Down
4 changes: 3 additions & 1 deletion Examples/UIExplorer/js/UIExplorerExampleList.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class UIExplorerExampleList extends React.Component {
keyboardShouldPersistTaps={true}
automaticallyAdjustContentInsets={false}
keyboardDismissMode="on-drag"
stickySectionHeaders={true}
/>
</View>
);
Expand Down Expand Up @@ -148,7 +149,7 @@ class UIExplorerExampleList extends React.Component {

_renderRow(title: string, description: string, key: ?string, handler: ?Function): ?React.Element<any> {
return (
<View key={key || title}>
<View key={key || title} collapsable={false}>
<TouchableHighlight onPress={handler}>
<View style={styles.row}>
<Text style={styles.rowTitleText}>
Expand Down Expand Up @@ -185,6 +186,7 @@ const styles = StyleSheet.create({
padding: 5,
fontWeight: '500',
fontSize: 11,
backgroundColor: '#eeeeee',
},
row: {
backgroundColor: 'white',
Expand Down
5 changes: 4 additions & 1 deletion Libraries/Components/ScrollView/ScrollView.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,10 @@ const ScrollView = React.createClass({
* `stickyHeaderIndices={[0]}` will cause the first child to be fixed to the
* top of the scroll view. This property is not supported in conjunction
* with `horizontal={true}`.
* @platform ios
*
* **Note:**
* On Android if sticky headers are not working properly make sure the child
* views are not getting collapsed by adding collapsable={false} on each child.
*/
stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number),
style: StyleSheetPropType(ViewStylePropTypes),
Expand Down
19 changes: 17 additions & 2 deletions Libraries/CustomComponents/ListView/ListView.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
'use strict';

var ListViewDataSource = require('ListViewDataSource');
var Platform = require('Platform');
var React = require('React');
var ReactNative = require('ReactNative');
var RCTScrollViewManager = require('NativeModules').ScrollViewManager;
Expand Down Expand Up @@ -234,9 +235,17 @@ var ListView = React.createClass({
* `stickyHeaderIndices={[0]}` will cause the first child to be fixed to the
* top of the scroll view. This property is not supported in conjunction
* with `horizontal={true}`.
* @platform ios
*
* **Note:**
* On Android if sticky headers are not working properly make sure the child
* views are not getting collapsed by adding collapsable={false} on each child.
*/
stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number).isRequired,
/**
* If the sections headers should be sticky. Defaults to `true` on iOS and
* `false` on Android.
*/
stickySectionHeaders: PropTypes.bool.isRequired,
/**
* Flag indicating whether empty section headers should be rendered. In the future release
* empty section headers will be rendered by default, and the flag will be deprecated.
Expand Down Expand Up @@ -297,6 +306,7 @@ var ListView = React.createClass({
scrollRenderAheadDistance: DEFAULT_SCROLL_RENDER_AHEAD,
onEndReachedThreshold: DEFAULT_END_REACHED_THRESHOLD,
stickyHeaderIndices: [],
stickySectionHeaders: Platform.OS === 'ios',
};
},

Expand Down Expand Up @@ -464,9 +474,14 @@ var ListView = React.createClass({
if (props.removeClippedSubviews === undefined) {
props.removeClippedSubviews = true;
}

var stickyHeaderIndices = this.props.stickySectionHeaders ?
this.props.stickyHeaderIndices.concat(sectionHeaderIndices) :
this.props.stickyHeaderIndices;

Object.assign(props, {
onScroll: this._onScroll,
stickyHeaderIndices: this.props.stickyHeaderIndices.concat(sectionHeaderIndices),
stickyHeaderIndices,

// Do not pass these events downstream to ScrollView since they will be
// registered in ListView's own ScrollResponder.Mixin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import android.graphics.Color;
import android.os.Build;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;

import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.uimanager.annotations.ReactProp;
Expand Down Expand Up @@ -56,6 +56,8 @@ public void setTransform(T view, ReadableArray matrix) {
} else {
setTransformProperty(view, matrix);
}

updateClipping(view);
}

@ReactProp(name = PROP_OPACITY, defaultFloat = 1.f)
Expand Down Expand Up @@ -114,30 +116,40 @@ public void setImportantForAccessibility(T view, String importantForAccessibilit
@ReactProp(name = PROP_ROTATION)
public void setRotation(T view, float rotation) {
view.setRotation(rotation);

updateClipping(view);
}

@Deprecated
@ReactProp(name = PROP_SCALE_X, defaultFloat = 1f)
public void setScaleX(T view, float scaleX) {
view.setScaleX(scaleX);

updateClipping(view);
}

@Deprecated
@ReactProp(name = PROP_SCALE_Y, defaultFloat = 1f)
public void setScaleY(T view, float scaleY) {
view.setScaleY(scaleY);

updateClipping(view);
}

@Deprecated
@ReactProp(name = PROP_TRANSLATE_X, defaultFloat = 0f)
public void setTranslateX(T view, float translateX) {
view.setTranslationX(PixelUtil.toPixelFromDIP(translateX));

updateClipping(view);
}

@Deprecated
@ReactProp(name = PROP_TRANSLATE_Y, defaultFloat = 0f)
public void setTranslateY(T view, float translateY) {
view.setTranslationY(PixelUtil.toPixelFromDIP(translateY));

updateClipping(view);
}

@ReactProp(name = PROP_ACCESSIBILITY_LIVE_REGION)
Expand Down Expand Up @@ -176,4 +188,11 @@ private static void resetTransformProperty(View view) {
view.setScaleX(1);
view.setScaleY(1);
}

private static void updateClipping(View view) {
ViewParent parent = view.getParent();
if (parent instanceof ReactClippingViewGroup) {
((ReactClippingViewGroup) parent).updateClippingRect();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

package com.facebook.react.uimanager;

public interface DrawingOrderViewGroup {
/**
* Returns if the ViewGroup implements custom drawing order.
*/
boolean isDrawingOrderEnabled();

/**
* Returns which child to draw for the specified index.
*/
int getDrawingOrder(int i);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
package com.facebook.react.uimanager;

/**
* This interface should be implemented be native ViewGroup subclasses that can represent more
* This interface should be implemented by native ViewGroup subclasses that can represent more
* than a single react node. In that case, virtual and non-virtual (mapping to a View) elements
* can overlap, and TouchTargetHelper may incorrectly dispatch touch event to a wrong element
* because it priorities children over parents.
*/
public interface ReactCompoundViewGroup extends ReactCompoundView {
/**
* Returns true if react node responsible for the touch even is flattened into this ViewGroup.
* Returns true if react node responsible for the touch event is flattened into this ViewGroup.
* Use reactTagForTouch() to get its tag.
*/
boolean interceptsTouchEvent(float touchX, float touchY);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,12 @@ private static View findClosestReactAncestor(View view) {
*/
private static View findTouchTargetView(float[] eventCoords, ViewGroup viewGroup) {
int childrenCount = viewGroup.getChildCount();
final boolean useCustomOrder = (viewGroup instanceof DrawingOrderViewGroup) &&
((DrawingOrderViewGroup) viewGroup).isDrawingOrderEnabled();
for (int i = childrenCount - 1; i >= 0; i--) {
View child = viewGroup.getChildAt(i);
int childIndex = useCustomOrder ?
((DrawingOrderViewGroup) viewGroup).getDrawingOrder(i) : i;
View child = viewGroup.getChildAt(childIndex);
PointF childPoint = mTempPoint;
if (isTransformedTouchPointInView(eventCoords[0], eventCoords[1], viewGroup, child, childPoint)) {
// If it is contained within the child View, the childPoint value will contain the view
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -835,4 +835,8 @@ public int resolveRootTagFromReactTag(int reactTag) {

return rootTag;
}

public ViewManager getViewManager(String name) {
return mViewManagers.get(name);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.uimanager.DrawingOrderViewGroup;

/**
* Class responsible for animation layout changes, if a valid layout animation config has been
Expand Down Expand Up @@ -66,6 +67,12 @@ public void reset() {
}

public boolean shouldAnimateLayout(View viewToAnimate) {
if (viewToAnimate instanceof LayoutAnimationViewGroup) {
if (!((LayoutAnimationViewGroup) viewToAnimate).isLayoutAnimationEnabled()) {
return false;
}
}

// if view parent is null, skip animation: view have been clipped, we don't want animation to
// resume when view is re-attached to parent, which is the standard android animation behavior.
return mShouldAnimateLayout && viewToAnimate.getParent() != null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright 2004-present Facebook. All Rights Reserved.

package com.facebook.react.uimanager.layoutanimation;

public interface LayoutAnimationViewGroup {
boolean isLayoutAnimationEnabled();
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ android_library(
react_native_target('java/com/facebook/react/uimanager:uimanager'),
react_native_target('java/com/facebook/react/uimanager/annotations:annotations'),
react_native_target('java/com/facebook/react/views/view:view'),
react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'),
],
visibility = [
'PUBLIC',
Expand Down
Loading