diff --git a/Examples/UIExplorer/js/ListViewPagingExample.js b/Examples/UIExplorer/js/ListViewPagingExample.js index 4a50d7b25e0055..a3afe7ad4cd038 100644 --- a/Examples/UIExplorer/js/ListViewPagingExample.js +++ b/Examples/UIExplorer/js/ListViewPagingExample.js @@ -195,6 +195,7 @@ class ListViewPagingExample extends React.Component { initialListSize={10} pageSize={4} scrollRenderAheadDistance={500} + stickySectionHeaders={true} /> ); } diff --git a/Examples/UIExplorer/js/UIExplorerExampleList.js b/Examples/UIExplorer/js/UIExplorerExampleList.js index 47772a63f7f345..9d23d3234c2eb5 100644 --- a/Examples/UIExplorer/js/UIExplorerExampleList.js +++ b/Examples/UIExplorer/js/UIExplorerExampleList.js @@ -85,6 +85,7 @@ class UIExplorerExampleList extends React.Component { keyboardShouldPersistTaps={true} automaticallyAdjustContentInsets={false} keyboardDismissMode="on-drag" + stickySectionHeaders={true} /> ); @@ -148,7 +149,7 @@ class UIExplorerExampleList extends React.Component { _renderRow(title: string, description: string, key: ?string, handler: ?Function): ?React.Element { return ( - + @@ -185,6 +186,7 @@ const styles = StyleSheet.create({ padding: 5, fontWeight: '500', fontSize: 11, + backgroundColor: '#eeeeee', }, row: { backgroundColor: 'white', diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index eaaa2e86f94edd..4867eb0d552549 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -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), diff --git a/Libraries/CustomComponents/ListView/ListView.js b/Libraries/CustomComponents/ListView/ListView.js index 31512c3f77346f..dc3d7ca5b751ba 100644 --- a/Libraries/CustomComponents/ListView/ListView.js +++ b/Libraries/CustomComponents/ListView/ListView.js @@ -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; @@ -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. @@ -297,6 +306,7 @@ var ListView = React.createClass({ scrollRenderAheadDistance: DEFAULT_SCROLL_RENDER_AHEAD, onEndReachedThreshold: DEFAULT_END_REACHED_THRESHOLD, stickyHeaderIndices: [], + stickySectionHeaders: Platform.OS === 'ios', }; }, @@ -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 diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index 815bad0faddcc5..1282c57fea4e67 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -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; @@ -56,6 +56,8 @@ public void setTransform(T view, ReadableArray matrix) { } else { setTransformProperty(view, matrix); } + + updateClipping(view); } @ReactProp(name = PROP_OPACITY, defaultFloat = 1.f) @@ -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) @@ -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(); + } + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/DrawingOrderViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/DrawingOrderViewGroup.java new file mode 100644 index 00000000000000..ad6d04c38a5ef2 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/DrawingOrderViewGroup.java @@ -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); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactCompoundViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactCompoundViewGroup.java index 345dea9f9184bc..b8ac3348a2517c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactCompoundViewGroup.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactCompoundViewGroup.java @@ -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); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java index 6243edd19f6e1c..60157feffa7512 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java @@ -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 diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java index f176d07a1a4935..6c534b2f009776 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java @@ -835,4 +835,8 @@ public int resolveRootTagFromReactTag(int reactTag) { return rootTag; } + + public ViewManager getViewManager(String name) { + return mViewManagers.get(name); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.java index d5a08e6b264ac5..e2453c010a52bf 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.java @@ -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 @@ -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; diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationViewGroup.java new file mode 100644 index 00000000000000..ec07f9d22980f3 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationViewGroup.java @@ -0,0 +1,7 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.uimanager.layoutanimation; + +public interface LayoutAnimationViewGroup { + boolean isLayoutAnimationEnabled(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/BUCK index 4628dfc8332edb..f0b639ed258f80 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/BUCK @@ -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', diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java index 3c38d92dfabcad..a21a13d8a0a95f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java @@ -9,10 +9,6 @@ package com.facebook.react.views.scroll; -import javax.annotation.Nullable; - -import java.lang.reflect.Field; - import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Rect; @@ -25,13 +21,27 @@ import android.widget.OverScroller; import android.widget.ScrollView; +import com.facebook.common.logging.FLog; +import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; import com.facebook.react.common.ReactConstants; import com.facebook.react.uimanager.MeasureSpecAssertions; +import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.events.NativeGestureUtil; import com.facebook.react.uimanager.ReactClippingViewGroup; import com.facebook.react.uimanager.ReactClippingViewGroupHelper; -import com.facebook.infer.annotation.Assertions; +import com.facebook.react.uimanager.ViewGroupManager; +import com.facebook.react.views.view.ReactViewGroup; +import com.facebook.react.views.view.ReactViewManager; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.annotation.Nullable; /** * A simple subclass of ScrollView that doesn't dispatch measure and layout to its children and has @@ -44,6 +54,7 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou private static Field sScrollerField; private static boolean sTriedToGetScrollerField = false; + private static boolean sHasWarnedAboutStickyHeaders = false; private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper(); private final OverScroller mScroller; @@ -60,6 +71,20 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou private @Nullable Drawable mEndBackground; private int mEndFillColor = Color.TRANSPARENT; private View mContentView; + private @Nullable int[] mStickyHeaderIndices; + private @Nullable Set mStickyHeaderViews; + private @Nullable List mOrderedChildViews; + private ViewGroupManager mViewManager; + + private final ReactViewGroup.ChildDrawingOrderDelegate mContentDrawingOrderDelegate = + new ReactViewGroup.ChildDrawingOrderDelegate() { + @Override + public int getChildDrawingOrder(ReactViewGroup viewGroup, int drawingIndex) { + Assertions.assertNotNull(mOrderedChildViews); + + return viewGroup.indexOfChild(mOrderedChildViews.get(drawingIndex)); + } + }; public ReactScrollView(ReactContext context) { this(context, null); @@ -67,6 +92,10 @@ public ReactScrollView(ReactContext context) { public ReactScrollView(ReactContext context, @Nullable FpsListener fpsListener) { super(context); + + UIManagerModule uiManager = context.getNativeModule(UIManagerModule.class); + mViewManager = (ViewGroupManager) uiManager.getUIImplementation().getViewManager(ReactViewManager.REACT_CLASS); + mFpsListener = fpsListener; if (!sTriedToGetScrollerField) { @@ -145,6 +174,14 @@ protected void onAttachedToWindow() { if (mRemoveClippedSubviews) { updateClippingRect(); } + + View contentView = getChildAt(0); + if (contentView instanceof ReactViewGroup) { + ((ReactViewGroup) contentView).setChildDrawingOrderDelegate( + mStickyHeaderIndices != null ? mContentDrawingOrderDelegate : null); + } + + dockClosestSectionHeader(); } @Override @@ -152,6 +189,8 @@ protected void onScrollChanged(int x, int y, int oldX, int oldY) { super.onScrollChanged(x, y, oldX, oldY); if (mOnScrollDispatchHelper.onScrollChanged(x, y)) { + dockClosestSectionHeader(); + if (mRemoveClippedSubviews) { updateClippingRect(); } @@ -309,6 +348,13 @@ private int getMaxScrollY() { return Math.max(0, contentHeight - viewportHeight); } + @Override + protected void onDraw(Canvas canvas) { + updateOrderedChildViews(); + + super.onDraw(canvas); + } + @Override public void draw(Canvas canvas) { if (mEndFillColor != Color.TRANSPARENT) { @@ -328,6 +374,141 @@ public void setEndFillColor(int color) { } } + public void setStickyHeaderIndices(@Nullable ReadableArray indices) { + if (indices == null || indices.size() == 0) { + mStickyHeaderIndices = null; + } else { + int[] indicesArray = new int[indices.size()]; + for (int i = 0; i < indices.size(); i++) { + indicesArray[i] = indices.getInt(i); + } + + mStickyHeaderIndices = indicesArray; + } + } + + public void onAfterUpdateTransaction() { + View contentView = getChildAt(0); + if (contentView instanceof ReactViewGroup) { + ((ReactViewGroup) contentView).setChildDrawingOrderDelegate( + mStickyHeaderIndices != null ? mContentDrawingOrderDelegate : null); + } + + dockClosestSectionHeader(); + } + + private void dockClosestSectionHeader() { + if (mStickyHeaderIndices == null) { + return; + } + + if (mStickyHeaderViews == null) { + mStickyHeaderViews = new HashSet<>(); + } + mStickyHeaderViews.clear(); + + View previousHeader = null; + View currentHeader = null; + View nextHeader = null; + View firstChild = getChildAt(0); + + if (firstChild != null && !(firstChild instanceof ReactViewGroup)) { + if (!sHasWarnedAboutStickyHeaders) { + FLog.w( + ReactConstants.TAG, + "'stickyHeaderIndices' isn't a supported prop type for this UIImplementation (nodes). " + + "It'd be awesome if you could help us support it by sending a diff or PR :)"); + sHasWarnedAboutStickyHeaders = true; + } + return; + } + + ReactViewGroup contentView = (ReactViewGroup) getChildAt(0); + if (contentView == null) { + return; + } + + int scrollY = getScrollY(); + for (int idx : mStickyHeaderIndices) { + // If the subviews are out of sync with the sticky header indices don't + // do anything. + if (idx >= mViewManager.getChildCount(contentView)) { + break; + } + + View header = mViewManager.getChildAt(contentView, idx); + mStickyHeaderViews.add(header); + + // If nextHeader not yet found, search for docked headers. + if (nextHeader == null) { + int top = header.getTop(); + if (top > scrollY) { + nextHeader = header; + } else { + previousHeader = currentHeader; + currentHeader = header; + } + } + + header.setTranslationY(0); + if (header instanceof ReactViewGroup) { + ((ReactViewGroup) header).setLayoutAnimationEnabled(true); + } + } + + if (currentHeader == null) { + return; + } + + int currentHeaderTop = currentHeader.getTop(); + int currentHeaderHeight = currentHeader.getHeight(); + int yOffset = scrollY - currentHeaderTop; + + if (nextHeader != null) { + // The next header nudges the current header out of the way when it reaches + // the top of the screen. + int nextHeaderTop = nextHeader.getTop(); + int overlap = currentHeaderHeight - (nextHeaderTop - scrollY); + yOffset -= Math.max(0, overlap); + } + + currentHeader.setTranslationY(yOffset); + if (currentHeader instanceof ReactViewGroup) { + ((ReactViewGroup) currentHeader).setLayoutAnimationEnabled(false); + } + + if (previousHeader != null) { + // The previous header sits right above the currentHeader's initial position + // so it scrolls away nicely once the currentHeader has locked into place. + yOffset = currentHeaderTop - previousHeader.getTop() - previousHeader.getHeight(); + previousHeader.setTranslationY(yOffset); + } + } + + private void updateOrderedChildViews() { + if (mStickyHeaderIndices == null || mStickyHeaderViews == null) { + return; + } + + if (mOrderedChildViews == null) { + mOrderedChildViews = new ArrayList<>(); + } + mOrderedChildViews.clear(); + + ReactViewGroup contentView = (ReactViewGroup) getChildAt(0); + int childCount = contentView.getChildCount(); + int totalStickyHeaders = 0; + for (int i = 0; i < childCount; i++) { + View child = contentView.getChildAt(i); + if (mStickyHeaderViews.contains(child)) { + mOrderedChildViews.add(mOrderedChildViews.size(), child); + totalStickyHeaders++; + } else { + mOrderedChildViews.add(mOrderedChildViews.size() - totalStickyHeaders, child); + } + } + } + @Override protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { if (mScroller != null) { @@ -380,4 +561,3 @@ public void onLayoutChange(View v, int left, int top, int right, int bottom, int } } } - diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java index ba9173bccd403b..27d4a64863997e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java @@ -9,10 +9,6 @@ package com.facebook.react.views.scroll; -import javax.annotation.Nullable; - -import java.util.Map; - import android.graphics.Color; import com.facebook.react.bridge.ReadableArray; @@ -23,6 +19,10 @@ import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.ReactClippingViewGroupHelper; +import java.util.Map; + +import javax.annotation.Nullable; + /** * View manager for {@link ReactScrollView} components. * @@ -56,6 +56,13 @@ public ReactScrollView createViewInstance(ThemedReactContext context) { return new ReactScrollView(context, mFpsListener); } + @Override + protected void onAfterUpdateTransaction(ReactScrollView view) { + super.onAfterUpdateTransaction(view); + + view.onAfterUpdateTransaction(); + } + @ReactProp(name = "scrollEnabled", defaultBoolean = true) public void setScrollEnabled(ReactScrollView view, boolean value) { view.setScrollEnabled(value); @@ -107,6 +114,11 @@ public void setBottomFillColor(ReactScrollView view, int color) { view.setEndFillColor(color); } + @ReactProp(name = "stickyHeaderIndices") + public void setStickyHeaderIndices(ReactScrollView view, @Nullable ReadableArray indices) { + view.setStickyHeaderIndices(indices); + } + @Override public @Nullable Map getCommandsMap() { return ReactScrollViewCommandHelper.getCommandsMap(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java index f69abaf0252fda..09a01dad5ac52f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java @@ -13,7 +13,9 @@ import android.content.Context; import android.graphics.Color; +import android.graphics.Matrix; import android.graphics.Rect; +import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.view.animation.Animation; @@ -31,19 +33,25 @@ import com.facebook.react.uimanager.ReactClippingViewGroup; import com.facebook.react.uimanager.ReactClippingViewGroupHelper; import com.facebook.react.uimanager.ReactPointerEventsView; +import com.facebook.react.uimanager.layoutanimation.LayoutAnimationViewGroup; /** * Backing for a React View. Has support for borders, but since borders aren't common, lazy * initializes most of the storage needed for them. */ public class ReactViewGroup extends ViewGroup implements - ReactInterceptingViewGroup, ReactClippingViewGroup, ReactPointerEventsView, ReactHitSlopView { + ReactInterceptingViewGroup, ReactClippingViewGroup, ReactPointerEventsView, + ReactHitSlopView, DrawingOrderViewGroup, LayoutAnimationViewGroup { private static final int ARRAY_CAPACITY_INCREMENT = 12; private static final int DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT; private static final LayoutParams sDefaultLayoutParam = new ViewGroup.LayoutParams(0, 0); /* should only be used in {@link #updateClippingToRect} */ - private static final Rect sHelperRect = new Rect(); + private static final RectF sHelperRect = new RectF(); + + public interface ChildDrawingOrderDelegate { + int getChildDrawingOrder(ReactViewGroup view, int i); + } /** * This listener will be set for child views when removeClippedSubview property is enabled. When @@ -96,6 +104,8 @@ public void onLayoutChange( private @Nullable ReactViewBackgroundDrawable mReactBackgroundDrawable; private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener; private boolean mNeedsOffscreenAlphaCompositing = false; + private @Nullable ChildDrawingOrderDelegate mChildDrawingOrderDelegate; + private boolean mIsLayoutAnimationEnabled = true; public ReactViewGroup(Context context) { super(context); @@ -292,8 +302,15 @@ private void updateClippingToRect(Rect clippingRect) { private void updateSubviewClipStatus(Rect clippingRect, int idx, int clippedSoFar) { View child = Assertions.assertNotNull(mAllChildren)[idx]; sHelperRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom()); - boolean intersects = clippingRect - .intersects(sHelperRect.left, sHelperRect.top, sHelperRect.right, sHelperRect.bottom); + Matrix matrix = child.getMatrix(); + if (!matrix.isIdentity()) { + matrix.mapRect(sHelperRect); + } + boolean intersects = clippingRect.intersects( + (int) sHelperRect.left, + (int) sHelperRect.top, + (int) Math.ceil(sHelperRect.right), + (int) Math.ceil(sHelperRect.bottom)); boolean needUpdateClippingRecursive = false; // We never want to clip children that are being animated, as this can easily break layout : // when layout animation changes size and/or position of views contained inside a listview that @@ -338,8 +355,15 @@ private void updateSubviewClipStatus(View subview) { // do fast check whether intersect state changed sHelperRect.set(subview.getLeft(), subview.getTop(), subview.getRight(), subview.getBottom()); - boolean intersects = mClippingRect - .intersects(sHelperRect.left, sHelperRect.top, sHelperRect.right, sHelperRect.bottom); + Matrix matrix = subview.getMatrix(); + if (!matrix.isIdentity()) { + matrix.mapRect(sHelperRect); + } + boolean intersects = mClippingRect.intersects( + (int) sHelperRect.left, + (int) sHelperRect.top, + (int) Math.ceil(sHelperRect.right), + (int) Math.ceil(sHelperRect.bottom)); // If it was intersecting before, should be attached to the parent boolean oldIntersects = (subview.getParent() != null); @@ -532,4 +556,35 @@ public void setHitSlopRect(@Nullable Rect rect) { mHitSlopRect = rect; } + @Override + public int getDrawingOrder(int i) { + return getChildDrawingOrder(getChildCount(), i); + } + + @Override + public boolean isDrawingOrderEnabled() { + return isChildrenDrawingOrderEnabled(); + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + if (mChildDrawingOrderDelegate == null) { + return super.getChildDrawingOrder(childCount, i); + } else { + return mChildDrawingOrderDelegate.getChildDrawingOrder(this, i); + } + } + + public void setChildDrawingOrderDelegate(@Nullable ChildDrawingOrderDelegate delegate) { + setChildrenDrawingOrderEnabled(delegate != null); + mChildDrawingOrderDelegate = delegate; + } + + public boolean isLayoutAnimationEnabled() { + return mIsLayoutAnimationEnabled; + } + + public void setLayoutAnimationEnabled(boolean enabled) { + mIsLayoutAnimationEnabled = enabled; + } }