SVG component becomes transparent when goBack
hehex9 opened this issue · comments
Description
There is an obvious UI blinking that occurs when returning from a screen that uses svg (react-native-svg), svg views in the child screen will become transparent and then disappear with the screen. (check the video)
It happens in navigators created by native-stack (createNativeStackNavigator) @react-navigation/stack works just fine in this scenario.
Not sure if it's related to react-native-svg.
(only happens on Android)
Screenshots
Screenrecord-2021-01-11-15-37-14-455.mp4
Steps To Reproduce
check the snack below
Expected behavior
SVG View should be visible util the screen is completely disappear (pop to bottom / right)
Actual behavior
will becomes transparent when navigating back to its parent screen
Snack or minimal code example
https://snack.expo.io/@hehex/native-stack-svg-blink
Package versions
- React: 16.13.1
- React Native: 0.63.4
- React Native Screens: 2.16.1
I have same issue with custom headerCenter and headerLeft, if you add any Image component in screen or header the same thing happen, maybe native stack is very fast? or React Native bridge its slower? @wkozyra95
FIY: only happen in Android
android react-native-fast-image have the same problem when goback android ViewManager onDropViewInstance method callback immediately and view detach the tree (android native), although view detach the tree view can't invisible immediately until animation finished
/**
* This method tells the ViewGroup that the given View object, which should have this
* ViewGroup as its parent,
* should be kept around (re-displayed when the ViewGroup draws its children) even if it
* is removed from its parent. This allows animations, such as those used by
* {@link android.app.Fragment} and {@link android.animation.LayoutTransition} to animate
* the removal of views. A call to this method should always be accompanied by a later call
* to {@link #endViewTransition(View)}, such as after an animation on the View has finished,
* so that the View finally gets removed.
*
* @param view The View object to be kept visible even if it gets removed from its parent.
*/
public void startViewTransition(View view) {
if (view.mParent == this) {
if (mTransitioningViews == null) {
mTransitioningViews = new ArrayList<View>();
}
mTransitioningViews.add(view);
}
}
image will white requestManager.clear(view) immediately
public void onDropViewInstance(FastImageViewWithUrl view) {
if (requestManager != null) {
requestManager.clear(view);
}
super.onDropViewInstance(view);
}
i think SVG component is the same issue as fastimage
View detached Animation has not started SVG component redraw empty bitmap
@fangasvsass amazing! but the same happen with de header (react-native-screens's HeaderConfig), Do you found any solution?
@msvargas HeaderConfig view is not rnscreen child ,you can call startViewTransition with HeaderConfig views
add code
private void prepareOutTransition(ViewGroup screen) {
startTransitionRecursive(screen);
}
private void startTransitionRecursive(ViewGroup parent) {
for (int i = 0, size = parent.getChildCount(); i < size; i++) {
View child = parent.getChildAt(i);
parent.startViewTransition(child);
if (child instanceof ViewGroup) {
startTransitionRecursive((ViewGroup) child);
}
}
}
Complete code
screens version 2.16.1
node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.java
package com.swmansion.rnscreens;
import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import com.facebook.react.ReactApplication;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.views.text.ReactFontManager;
import java.util.ArrayList;
public class ScreenStackHeaderConfig extends ViewGroup {
private final ArrayList<ScreenStackHeaderSubview> mConfigSubviews = new ArrayList<>(3);
private String mTitle;
private int mTitleColor;
private String mTitleFontFamily;
private String mDirection;
private float mTitleFontSize;
private Integer mBackgroundColor;
private boolean mIsHidden;
private boolean mIsBackButtonHidden;
private boolean mIsShadowHidden;
private boolean mDestroyed;
private boolean mBackButtonInCustomView;
private boolean mIsTopInsetEnabled = true;
private boolean mIsTranslucent;
private int mTintColor;
private final Toolbar mToolbar;
private boolean mIsAttachedToWindow = false;
private int mDefaultStartInset;
private int mDefaultStartInsetWithNavigation;
private static class DebugMenuToolbar extends Toolbar {
public DebugMenuToolbar(Context context) {
super(context);
}
@Override
public boolean showOverflowMenu() {
((ReactApplication) getContext().getApplicationContext()).getReactNativeHost().getReactInstanceManager().showDevOptionsDialog();
return true;
}
}
private OnClickListener mBackClickListener = new OnClickListener() {
@Override
public void onClick(View view) {
ScreenStackFragment fragment = getScreenFragment();
if (fragment != null) {
ScreenStack stack = getScreenStack();
if (stack != null && stack.getRootScreen() == fragment.getScreen()) {
Fragment parentFragment = fragment.getParentFragment();
if (parentFragment instanceof ScreenStackFragment) {
((ScreenStackFragment) parentFragment).dismiss();
}
} else {
fragment.dismiss();
}
}
}
};
public ScreenStackHeaderConfig(Context context) {
super(context);
setVisibility(View.GONE);
mToolbar = BuildConfig.DEBUG ? new DebugMenuToolbar(context) : new Toolbar(context);
mDefaultStartInset = mToolbar.getContentInsetStart();
mDefaultStartInsetWithNavigation = mToolbar.getContentInsetStartWithNavigation();
// set primary color as background by default
TypedValue tv = new TypedValue();
if (context.getTheme().resolveAttribute(android.R.attr.colorPrimary, tv, true)) {
mToolbar.setBackgroundColor(tv.data);
}
mToolbar.setClipChildren(false);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// no-op
}
public void destroy() {
mDestroyed = true;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mIsAttachedToWindow = true;
onUpdate();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mIsAttachedToWindow = false;
}
private Screen getScreen() {
ViewParent screen = getParent();
if (screen instanceof Screen) {
return (Screen) screen;
}
return null;
}
private ScreenStack getScreenStack() {
Screen screen = getScreen();
if (screen != null) {
ScreenContainer container = screen.getContainer();
if (container instanceof ScreenStack) {
return (ScreenStack) container;
}
}
return null;
}
private ScreenStackFragment getScreenFragment() {
ViewParent screen = getParent();
if (screen instanceof Screen) {
Fragment fragment = ((Screen) screen).getFragment();
if (fragment instanceof ScreenStackFragment) {
return (ScreenStackFragment) fragment;
}
}
return null;
}
public void onUpdate() {
Screen parent = (Screen) getParent();
final ScreenStack stack = getScreenStack();
boolean isTop = stack == null ? true : stack.getTopScreen() == parent;
if (!mIsAttachedToWindow || !isTop || mDestroyed) {
return;
}
AppCompatActivity activity = (AppCompatActivity) getScreenFragment().getActivity();
if (activity == null) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && mDirection != null) {
if (mDirection.equals("rtl")) {
mToolbar.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
} else if (mDirection.equals("ltr")) {
mToolbar.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
}
}
if (mIsHidden) {
if (mToolbar.getParent() != null) {
getScreenFragment().removeToolbar();
}
return;
}
if (mToolbar.getParent() == null) {
getScreenFragment().setToolbar(mToolbar);
}
if (mIsTopInsetEnabled) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mToolbar.setPadding(0, getRootWindowInsets().getSystemWindowInsetTop(), 0, 0);
} else {
// Hacky fallback for old android. Before Marshmallow, the status bar height was always 25
mToolbar.setPadding(0, (int) (25 * getResources().getDisplayMetrics().density), 0, 0);
}
} else {
if (mToolbar.getPaddingTop() > 0) {
mToolbar.setPadding(0, 0, 0, 0);
}
}
activity.setSupportActionBar(mToolbar);
ActionBar actionBar = activity.getSupportActionBar();
// Reset toolbar insets. By default we set symmetric inset for start and end to match iOS
// implementation where both right and left icons are offset from the edge by default. We also
// reset startWithNavigation inset which corresponds to the distance between navigation icon and
// title. If title isn't set we clear that value few lines below to give more space to custom
// center-mounted views.
mToolbar.setContentInsetStartWithNavigation(mDefaultStartInsetWithNavigation);
mToolbar.setContentInsetsRelative(mDefaultStartInset, mDefaultStartInset);
// hide back button
actionBar.setDisplayHomeAsUpEnabled(getScreenFragment().canNavigateBack() ? !mIsBackButtonHidden : false);
// when setSupportActionBar is called a toolbar wrapper gets initialized that overwrites
// navigation click listener. The default behavior set in the wrapper is to call into
// menu options handlers, but we prefer the back handling logic to stay here instead.
mToolbar.setNavigationOnClickListener(mBackClickListener);
// shadow
getScreenFragment().setToolbarShadowHidden(mIsShadowHidden);
// translucent
getScreenFragment().setToolbarTranslucent(mIsTranslucent);
// title
actionBar.setTitle(mTitle);
if (TextUtils.isEmpty(mTitle)) {
// if title is empty we set start navigation inset to 0 to give more space to custom rendered
// views. When it is set to default it'd take up additional distance from the back button which
// would impact the position of custom header views rendered at the center.
mToolbar.setContentInsetStartWithNavigation(0);
}
TextView titleTextView = getTitleTextView();
if (mTitleColor != 0) {
mToolbar.setTitleTextColor(mTitleColor);
}
if (titleTextView != null) {
if (mTitleFontFamily != null) {
titleTextView.setTypeface(ReactFontManager.getInstance().getTypeface(
mTitleFontFamily, 0, getContext().getAssets()));
}
if (mTitleFontSize > 0) {
titleTextView.setTextSize(mTitleFontSize);
}
}
// background
if (mBackgroundColor != null) {
mToolbar.setBackgroundColor(mBackgroundColor);
}
// color
if (mTintColor != 0) {
Drawable navigationIcon = mToolbar.getNavigationIcon();
if (navigationIcon != null) {
navigationIcon.setColorFilter(mTintColor, PorterDuff.Mode.SRC_ATOP);
}
}
// subviews
for (int i = mToolbar.getChildCount() - 1; i >= 0; i--) {
if (mToolbar.getChildAt(i) instanceof ScreenStackHeaderSubview) {
mToolbar.removeViewAt(i);
}
}
for (int i = 0, size = mConfigSubviews.size(); i < size; i++) {
ScreenStackHeaderSubview view = mConfigSubviews.get(i);
ScreenStackHeaderSubview.Type type = view.getType();
if (type == ScreenStackHeaderSubview.Type.BACK) {
// we special case BACK button header config type as we don't add it as a view into toolbar
// but instead just copy the drawable from imageview that's added as a first child to it.
View firstChild = view.getChildAt(0);
if (!(firstChild instanceof ImageView)) {
throw new JSApplicationIllegalArgumentException("Back button header config view should have Image as first child");
}
actionBar.setHomeAsUpIndicator(((ImageView) firstChild).getDrawable());
continue;
}
Toolbar.LayoutParams params =
new Toolbar.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
switch (type) {
case LEFT:
// when there is a left item we need to disable navigation icon by default
// we also hide title as there is no other way to display left side items
if (!mBackButtonInCustomView) {
mToolbar.setNavigationIcon(null);
}
mToolbar.setTitle(null);
params.gravity = Gravity.START;
break;
case RIGHT:
params.gravity = Gravity.END;
break;
case CENTER:
params.width = LayoutParams.MATCH_PARENT;
params.gravity = Gravity.CENTER_HORIZONTAL;
mToolbar.setTitle(null);
break;
}
view.setLayoutParams(params);
mToolbar.addView(view);
prepareOutTransition(view);
}
}
private void prepareOutTransition(ViewGroup screen) {
startTransitionRecursive(screen);
}
private void startTransitionRecursive(ViewGroup parent) {
for (int i = 0, size = parent.getChildCount(); i < size; i++) {
View child = parent.getChildAt(i);
parent.startViewTransition(child);
if (child instanceof ViewGroup) {
startTransitionRecursive((ViewGroup) child);
}
}
}
private void maybeUpdate() {
if (getParent() != null && !mDestroyed) {
onUpdate();
}
}
public ScreenStackHeaderSubview getConfigSubview(int index) {
return mConfigSubviews.get(index);
}
public int getConfigSubviewsCount() {
return mConfigSubviews.size();
}
public void removeConfigSubview(int index) {
mConfigSubviews.remove(index);
maybeUpdate();
}
public void removeAllConfigSubviews() {
mConfigSubviews.clear();
maybeUpdate();
}
public void addConfigSubview(ScreenStackHeaderSubview child, int index) {
mConfigSubviews.add(index, child);
maybeUpdate();
}
private TextView getTitleTextView() {
for (int i = 0, size = mToolbar.getChildCount(); i < size; i++) {
View view = mToolbar.getChildAt(i);
if (view instanceof TextView) {
TextView tv = (TextView) view;
if (tv.getText().equals(mToolbar.getTitle())) {
return tv;
}
}
}
return null;
}
public void setTitle(String title) {
mTitle = title;
}
public void setTitleFontFamily(String titleFontFamily) {
mTitleFontFamily = titleFontFamily;
}
public void setTitleFontSize(float titleFontSize) {
mTitleFontSize = titleFontSize;
}
public void setTitleColor(int color) {
mTitleColor = color;
}
public void setTintColor(int color) {
mTintColor = color;
}
public void setTopInsetEnabled(boolean topInsetEnabled) { mIsTopInsetEnabled = topInsetEnabled; }
public void setBackgroundColor(Integer color) {
mBackgroundColor = color;
}
public void setHideShadow(boolean hideShadow) {
mIsShadowHidden = hideShadow;
}
public void setHideBackButton(boolean hideBackButton) {
mIsBackButtonHidden = hideBackButton;
}
public void setHidden(boolean hidden) {
mIsHidden = hidden;
}
public void setTranslucent(boolean translucent) {
mIsTranslucent = translucent;
}
public void setBackButtonInCustomView(boolean backButtonInCustomView) { mBackButtonInCustomView = backButtonInCustomView; }
public void setDirection(String direction) {
mDirection = direction;
}
}
Got the same problem with react-native-svg and on Android. When you trigger to go back, the svgs disappear while the transitions happens.
@msvargas HeaderConfig view is not rnscreen child ,you can call startViewTransition with HeaderConfig views
add code
private void prepareOutTransition(ViewGroup screen) { startTransitionRecursive(screen); } private void startTransitionRecursive(ViewGroup parent) { for (int i = 0, size = parent.getChildCount(); i < size; i++) { View child = parent.getChildAt(i); parent.startViewTransition(child); if (child instanceof ViewGroup) { startTransitionRecursive((ViewGroup) child); } } }
Complete code
screens version 2.16.1
node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.javapackage com.swmansion.rnscreens; import android.content.Context; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Build; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.ImageView; import android.widget.TextView; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; import com.facebook.react.ReactApplication; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.views.text.ReactFontManager; import java.util.ArrayList; public class ScreenStackHeaderConfig extends ViewGroup { private final ArrayList<ScreenStackHeaderSubview> mConfigSubviews = new ArrayList<>(3); private String mTitle; private int mTitleColor; private String mTitleFontFamily; private String mDirection; private float mTitleFontSize; private Integer mBackgroundColor; private boolean mIsHidden; private boolean mIsBackButtonHidden; private boolean mIsShadowHidden; private boolean mDestroyed; private boolean mBackButtonInCustomView; private boolean mIsTopInsetEnabled = true; private boolean mIsTranslucent; private int mTintColor; private final Toolbar mToolbar; private boolean mIsAttachedToWindow = false; private int mDefaultStartInset; private int mDefaultStartInsetWithNavigation; private static class DebugMenuToolbar extends Toolbar { public DebugMenuToolbar(Context context) { super(context); } @Override public boolean showOverflowMenu() { ((ReactApplication) getContext().getApplicationContext()).getReactNativeHost().getReactInstanceManager().showDevOptionsDialog(); return true; } } private OnClickListener mBackClickListener = new OnClickListener() { @Override public void onClick(View view) { ScreenStackFragment fragment = getScreenFragment(); if (fragment != null) { ScreenStack stack = getScreenStack(); if (stack != null && stack.getRootScreen() == fragment.getScreen()) { Fragment parentFragment = fragment.getParentFragment(); if (parentFragment instanceof ScreenStackFragment) { ((ScreenStackFragment) parentFragment).dismiss(); } } else { fragment.dismiss(); } } } }; public ScreenStackHeaderConfig(Context context) { super(context); setVisibility(View.GONE); mToolbar = BuildConfig.DEBUG ? new DebugMenuToolbar(context) : new Toolbar(context); mDefaultStartInset = mToolbar.getContentInsetStart(); mDefaultStartInsetWithNavigation = mToolbar.getContentInsetStartWithNavigation(); // set primary color as background by default TypedValue tv = new TypedValue(); if (context.getTheme().resolveAttribute(android.R.attr.colorPrimary, tv, true)) { mToolbar.setBackgroundColor(tv.data); } mToolbar.setClipChildren(false); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // no-op } public void destroy() { mDestroyed = true; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mIsAttachedToWindow = true; onUpdate(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mIsAttachedToWindow = false; } private Screen getScreen() { ViewParent screen = getParent(); if (screen instanceof Screen) { return (Screen) screen; } return null; } private ScreenStack getScreenStack() { Screen screen = getScreen(); if (screen != null) { ScreenContainer container = screen.getContainer(); if (container instanceof ScreenStack) { return (ScreenStack) container; } } return null; } private ScreenStackFragment getScreenFragment() { ViewParent screen = getParent(); if (screen instanceof Screen) { Fragment fragment = ((Screen) screen).getFragment(); if (fragment instanceof ScreenStackFragment) { return (ScreenStackFragment) fragment; } } return null; } public void onUpdate() { Screen parent = (Screen) getParent(); final ScreenStack stack = getScreenStack(); boolean isTop = stack == null ? true : stack.getTopScreen() == parent; if (!mIsAttachedToWindow || !isTop || mDestroyed) { return; } AppCompatActivity activity = (AppCompatActivity) getScreenFragment().getActivity(); if (activity == null) { return; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && mDirection != null) { if (mDirection.equals("rtl")) { mToolbar.setLayoutDirection(View.LAYOUT_DIRECTION_RTL); } else if (mDirection.equals("ltr")) { mToolbar.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); } } if (mIsHidden) { if (mToolbar.getParent() != null) { getScreenFragment().removeToolbar(); } return; } if (mToolbar.getParent() == null) { getScreenFragment().setToolbar(mToolbar); } if (mIsTopInsetEnabled) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mToolbar.setPadding(0, getRootWindowInsets().getSystemWindowInsetTop(), 0, 0); } else { // Hacky fallback for old android. Before Marshmallow, the status bar height was always 25 mToolbar.setPadding(0, (int) (25 * getResources().getDisplayMetrics().density), 0, 0); } } else { if (mToolbar.getPaddingTop() > 0) { mToolbar.setPadding(0, 0, 0, 0); } } activity.setSupportActionBar(mToolbar); ActionBar actionBar = activity.getSupportActionBar(); // Reset toolbar insets. By default we set symmetric inset for start and end to match iOS // implementation where both right and left icons are offset from the edge by default. We also // reset startWithNavigation inset which corresponds to the distance between navigation icon and // title. If title isn't set we clear that value few lines below to give more space to custom // center-mounted views. mToolbar.setContentInsetStartWithNavigation(mDefaultStartInsetWithNavigation); mToolbar.setContentInsetsRelative(mDefaultStartInset, mDefaultStartInset); // hide back button actionBar.setDisplayHomeAsUpEnabled(getScreenFragment().canNavigateBack() ? !mIsBackButtonHidden : false); // when setSupportActionBar is called a toolbar wrapper gets initialized that overwrites // navigation click listener. The default behavior set in the wrapper is to call into // menu options handlers, but we prefer the back handling logic to stay here instead. mToolbar.setNavigationOnClickListener(mBackClickListener); // shadow getScreenFragment().setToolbarShadowHidden(mIsShadowHidden); // translucent getScreenFragment().setToolbarTranslucent(mIsTranslucent); // title actionBar.setTitle(mTitle); if (TextUtils.isEmpty(mTitle)) { // if title is empty we set start navigation inset to 0 to give more space to custom rendered // views. When it is set to default it'd take up additional distance from the back button which // would impact the position of custom header views rendered at the center. mToolbar.setContentInsetStartWithNavigation(0); } TextView titleTextView = getTitleTextView(); if (mTitleColor != 0) { mToolbar.setTitleTextColor(mTitleColor); } if (titleTextView != null) { if (mTitleFontFamily != null) { titleTextView.setTypeface(ReactFontManager.getInstance().getTypeface( mTitleFontFamily, 0, getContext().getAssets())); } if (mTitleFontSize > 0) { titleTextView.setTextSize(mTitleFontSize); } } // background if (mBackgroundColor != null) { mToolbar.setBackgroundColor(mBackgroundColor); } // color if (mTintColor != 0) { Drawable navigationIcon = mToolbar.getNavigationIcon(); if (navigationIcon != null) { navigationIcon.setColorFilter(mTintColor, PorterDuff.Mode.SRC_ATOP); } } // subviews for (int i = mToolbar.getChildCount() - 1; i >= 0; i--) { if (mToolbar.getChildAt(i) instanceof ScreenStackHeaderSubview) { mToolbar.removeViewAt(i); } } for (int i = 0, size = mConfigSubviews.size(); i < size; i++) { ScreenStackHeaderSubview view = mConfigSubviews.get(i); ScreenStackHeaderSubview.Type type = view.getType(); if (type == ScreenStackHeaderSubview.Type.BACK) { // we special case BACK button header config type as we don't add it as a view into toolbar // but instead just copy the drawable from imageview that's added as a first child to it. View firstChild = view.getChildAt(0); if (!(firstChild instanceof ImageView)) { throw new JSApplicationIllegalArgumentException("Back button header config view should have Image as first child"); } actionBar.setHomeAsUpIndicator(((ImageView) firstChild).getDrawable()); continue; } Toolbar.LayoutParams params = new Toolbar.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); switch (type) { case LEFT: // when there is a left item we need to disable navigation icon by default // we also hide title as there is no other way to display left side items if (!mBackButtonInCustomView) { mToolbar.setNavigationIcon(null); } mToolbar.setTitle(null); params.gravity = Gravity.START; break; case RIGHT: params.gravity = Gravity.END; break; case CENTER: params.width = LayoutParams.MATCH_PARENT; params.gravity = Gravity.CENTER_HORIZONTAL; mToolbar.setTitle(null); break; } view.setLayoutParams(params); mToolbar.addView(view); prepareOutTransition(view); } } private void prepareOutTransition(ViewGroup screen) { startTransitionRecursive(screen); } private void startTransitionRecursive(ViewGroup parent) { for (int i = 0, size = parent.getChildCount(); i < size; i++) { View child = parent.getChildAt(i); parent.startViewTransition(child); if (child instanceof ViewGroup) { startTransitionRecursive((ViewGroup) child); } } } private void maybeUpdate() { if (getParent() != null && !mDestroyed) { onUpdate(); } } public ScreenStackHeaderSubview getConfigSubview(int index) { return mConfigSubviews.get(index); } public int getConfigSubviewsCount() { return mConfigSubviews.size(); } public void removeConfigSubview(int index) { mConfigSubviews.remove(index); maybeUpdate(); } public void removeAllConfigSubviews() { mConfigSubviews.clear(); maybeUpdate(); } public void addConfigSubview(ScreenStackHeaderSubview child, int index) { mConfigSubviews.add(index, child); maybeUpdate(); } private TextView getTitleTextView() { for (int i = 0, size = mToolbar.getChildCount(); i < size; i++) { View view = mToolbar.getChildAt(i); if (view instanceof TextView) { TextView tv = (TextView) view; if (tv.getText().equals(mToolbar.getTitle())) { return tv; } } } return null; } public void setTitle(String title) { mTitle = title; } public void setTitleFontFamily(String titleFontFamily) { mTitleFontFamily = titleFontFamily; } public void setTitleFontSize(float titleFontSize) { mTitleFontSize = titleFontSize; } public void setTitleColor(int color) { mTitleColor = color; } public void setTintColor(int color) { mTintColor = color; } public void setTopInsetEnabled(boolean topInsetEnabled) { mIsTopInsetEnabled = topInsetEnabled; } public void setBackgroundColor(Integer color) { mBackgroundColor = color; } public void setHideShadow(boolean hideShadow) { mIsShadowHidden = hideShadow; } public void setHideBackButton(boolean hideBackButton) { mIsBackButtonHidden = hideBackButton; } public void setHidden(boolean hidden) { mIsHidden = hidden; } public void setTranslucent(boolean translucent) { mIsTranslucent = translucent; } public void setBackButtonInCustomView(boolean backButtonInCustomView) { mBackButtonInCustomView = backButtonInCustomView; } public void setDirection(String direction) { mDirection = direction; } }
Thank you!! this fix the header, @WoLewicki please check this fix the same issue but in StackHeaderConfig with headerLeft config + Image component.
Short solution:
react-native-screens+2.16.1.patch
index 56ffaf2..32e741f 100644
--- a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.java
+++ b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.java
@@ -300,9 +301,20 @@ public class ScreenStackHeaderConfig extends ViewGroup {
view.setLayoutParams(params);
mToolbar.addView(view);
+
}
}
+ @Override
+ public View getChildAt(int index) {
+ return getConfigSubview(index);
+ }
+
+ @Override
+ public int getChildCount() {
+ return getConfigSubviewsCount();
+ }
+
private void maybeUpdate() {
if (getParent() != null && !mDestroyed) {
onUpdate();
Hi @fangasvsass with react-native-svg happen the same with, it calls onDropInstance, Do you found any solution for this in react-native-fast-image? thank you
SvgViewManager.java
@Override
public void onDropViewInstance(@Nonnull ReactViewGroup view) {
super.onDropViewInstance(view);
mTagToSvgView.remove(view.getId());
}
Can you submit a PR with the changes and a test case that shows it fixes the bug?
as @msvargas I'm having the same issue on fast-image. See comment with sample. Maybe a solution would be the to wait the animation interaction to finish so it can drop the view instances.
Can you submit a PR with the changes and a test case that shows it fixes the bug?
Sure, this only working when you have image components in HeaderConfig.
as @msvargas I'm having the same issue on fast-image. See comment with sample. Maybe a solution would be the to wait the animation interaction to finish so it can drop the view instances.
Thanks! I will check about it
Update:
If replace headerLeft with backButtonImage, the SVG working properly.
@WoLewicki Do you have any idea about it? thanks!
The backButtonImage
is treated differently than the other header options. You can see it here: https://github.com/software-mansion/react-native-screens/blob/master/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.java#L267. Probably the system knows how to correctly deal with the Drawable
.
Ok, but a minor change in StackHeaderConfig I fix the header when calling goBack in JS, however, thank you
I'm to be working on the issue, create Test773, and share with your another video with the issue in the header and screen. :)
Scenaries to reproduce issue:
- Go back with custom headerLeft (navigation.goBack())
- Go back with android hardware button (navigation.goBack())
- And with another button to goBack (navigation.goBack())
react-native-screens v2.17.1
TestsExample/src/Test773.js
import React from 'react';
import {Button, Text, View, StyleSheet, Image, Pressable} from 'react-native';
import {NavigationContainer, useNavigation} from '@react-navigation/native';
import {createNativeStackNavigator} from 'react-native-screens/native-stack';
import Svg, {Circle, Rect} from 'react-native-svg';
const backButtonImage = require('../assets/backButton.png');
const navigationRef = React.createRef();
const HeaderBackButton = () => (
<Pressable onPress={() => navigationRef.current.goBack()}>
<Image source={backButtonImage} />
</Pressable>
);
const Screen2 = () => {
const navigation = useNavigation();
return (
<View
style={{
backgroundColor: '#08141B',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
}}>
<View
style={[
StyleSheet.absoluteFill,
{alignItems: 'center', justifyContent: 'center'},
]}>
<Svg height="50%" width="50%" viewBox="0 0 100 100">
<Circle
cx="50"
cy="50"
r="45"
stroke="blue"
strokeWidth="2.5"
fill="green"
/>
<Rect
x="15"
y="15"
width="70"
height="70"
stroke="red"
strokeWidth="2"
fill="yellow"
/>
</Svg>
</View>
<Button title="navigation.goBack()" onPress={() => navigation.goBack()}>
<Text>Go back JS</Text>
</Button>
</View>
);
};
const Screen1 = () => {
const navigation = useNavigation();
return (
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
}}>
<Button
title="Go to Screen 2"
onPress={() => navigation.navigate('Screen2')}
/>
</View>
);
};
const Stack = createNativeStackNavigator();
const App = () => {
return (
<NavigationContainer ref={navigationRef}>
<Stack.Navigator
// mode="modal"
screenOptions={{
// backButtonImage,
headerLeft: HeaderBackButton,
gestureEnabled: true,
topInsetEnabled: false,
stackAnimation: 'slide_from_right',
}}>
<Stack.Screen
name="Screen1"
options={{headerShown: false}}
component={Screen1}
/>
<Stack.Screen name="Screen2" component={Screen2} />
</Stack.Navigator>
</NavigationContainer>
);
};
export default App;
@hehex9 @ferrannp After spending 3 days approx. I found the error and the solution, the error is that react-native-svg calls bitmap.recycle() and ignore when react-native-screen call startViewTransition.
I share the solution to help anybody
You can apply this patch:
react-native-svg+12.1.0
diff --git a/node_modules/react-native-svg/android/src/main/java/com/horcrux/svg/SvgView.java b/node_modules/react-native-svg/android/src/main/java/com/horcrux/svg/SvgView.java
index 5c792bd..d070e0e 100644
--- a/node_modules/react-native-svg/android/src/main/java/com/horcrux/svg/SvgView.java
+++ b/node_modules/react-native-svg/android/src/main/java/com/horcrux/svg/SvgView.java
@@ -66,6 +66,7 @@ public class SvgView extends ReactViewGroup implements ReactCompoundView, ReactC
}
private @Nullable Bitmap mBitmap;
+ private boolean mRemovalTransitionStarted = false;
public SvgView(ReactContext reactContext) {
super(reactContext);
@@ -90,12 +91,30 @@ public class SvgView extends ReactViewGroup implements ReactCompoundView, ReactC
((VirtualView) parent).getSvgView().invalidate();
return;
}
- if (mBitmap != null) {
- mBitmap.recycle();
+ // Additional: maybe its not necessary since Android 2.3.3 https://developer.android.com/topic/performance/graphics/manage-memory#recycle
+ if(!mRemovalTransitionStarted){
+ if (mBitmap != null) {
+ mBitmap.recycle();
+ }
+ mBitmap = null;
+ }
+ }
+
+ @Override
+ public void startViewTransition(View view) {
+ super.startViewTransition(view);
+ mRemovalTransitionStarted = true;
+ }
+
+ @Override
+ public void endViewTransition(View view) {
+ super.endViewTransition(view);
+ if (mRemovalTransitionStarted) {
+ mRemovalTransitionStarted = false;
}
- mBitmap = null;
}
+
@Override
protected void onDraw(Canvas canvas) {
if (getParent() instanceof VirtualView) {
Thanks
So it is rather an issue on the side of react-native-svg
, not react-native-screens
? And if so, is there anything we can do on our side, or can I close this issue?
I think the issue is on the react-native-svg side, Can you create a PR in react-native-svg to support o fix correct behavior with react-native-screens? because react-native-svg is used in a lot of apps
So it is rather an issue on the side of
react-native-svg
, notreact-native-screens
? And if so, is there anything we can do on our side, or can I close this issue?
I think you should create this PR @msvargas since you have a lot of insight to this. Therefore, you could submit a much more descriptive PR with the examples of how the change fixes the problems. You can link that PR here so it is easy to elaborate if needed.
I made a PR in react-native-fast-image
(DylanVann/react-native-fast-image#773) and react-native-svg
(software-mansion/react-native-svg#1542) and in this repo (#820) that should address all of the mentioned issues. Can all of you check if it resolves all the problems and do not introduce any new issues?
I made a PR in
react-native-fast-image
(DylanVann/react-native-fast-image#773) andreact-native-svg
(react-native-svg/react-native-svg#1542) and in this repo (#820) that should address all of the mentioned issues. Can all of you check if it resolves all the problems and do not introduce any new issues?
test the code not work and if use native stack only one screen attached Window pre screen images will call onDetachedFromWindow when goBack image becomes transparent
@fangasvsass I am sorry, but I don't understand. Could you be more descriptive?
sorry My English is bad ,open the A screen then open b Screen ,when open b Screen A screen's view will detachedFromWindow because A screen removed from android rootview
A screen's image will be cleared @WoLewicki
@fangasvsass can you post a reproduction where it is an issue? What is the problem with clearing the image after the screen is detached?
can confirm the PR for react-native-svg
works fine on my project
Ok so I will close this issue since there is nothing that can be done in react-native-screens
concerning these issues. Please comment in the PRs about problems regarding them. If there is something wrong, please comment here to reopen.
@hehex9 @ferrannp After spending 3 days approx. I found the error and the solution, the error is that react-native-svg calls bitmap.recycle() and ignore when react-native-screen call startViewTransition.
I share the solution to help anybody
You can apply this patch:
react-native-svg+12.1.0
diff --git a/node_modules/react-native-svg/android/src/main/java/com/horcrux/svg/SvgView.java b/node_modules/react-native-svg/android/src/main/java/com/horcrux/svg/SvgView.java index 5c792bd..d070e0e 100644 --- a/node_modules/react-native-svg/android/src/main/java/com/horcrux/svg/SvgView.java +++ b/node_modules/react-native-svg/android/src/main/java/com/horcrux/svg/SvgView.java @@ -66,6 +66,7 @@ public class SvgView extends ReactViewGroup implements ReactCompoundView, ReactC } private @Nullable Bitmap mBitmap; + private boolean mRemovalTransitionStarted = false; public SvgView(ReactContext reactContext) { super(reactContext); @@ -90,12 +91,30 @@ public class SvgView extends ReactViewGroup implements ReactCompoundView, ReactC ((VirtualView) parent).getSvgView().invalidate(); return; } - if (mBitmap != null) { - mBitmap.recycle(); + // Additional: maybe its not necessary since Android 2.3.3 https://developer.android.com/topic/performance/graphics/manage-memory#recycle + if(!mRemovalTransitionStarted){ + if (mBitmap != null) { + mBitmap.recycle(); + } + mBitmap = null; + } + } + + @Override + public void startViewTransition(View view) { + super.startViewTransition(view); + mRemovalTransitionStarted = true; + } + + @Override + public void endViewTransition(View view) { + super.endViewTransition(view); + if (mRemovalTransitionStarted) { + mRemovalTransitionStarted = false; } - mBitmap = null; } + @Override protected void onDraw(Canvas canvas) { if (getParent() instanceof VirtualView) {![]()
Thanks
It works !! Thanks
@hehex9 @ferrannp After spending 3 days approx. I found the error and the solution, the error is that react-native-svg calls bitmap.recycle() and ignore when react-native-screen call startViewTransition.
I share the solution to help anybody
You can apply this patch:
react-native-svg+12.1.0diff --git a/node_modules/react-native-svg/android/src/main/java/com/horcrux/svg/SvgView.java b/node_modules/react-native-svg/android/src/main/java/com/horcrux/svg/SvgView.java index 5c792bd..d070e0e 100644 --- a/node_modules/react-native-svg/android/src/main/java/com/horcrux/svg/SvgView.java +++ b/node_modules/react-native-svg/android/src/main/java/com/horcrux/svg/SvgView.java @@ -66,6 +66,7 @@ public class SvgView extends ReactViewGroup implements ReactCompoundView, ReactC } private @Nullable Bitmap mBitmap; + private boolean mRemovalTransitionStarted = false; public SvgView(ReactContext reactContext) { super(reactContext); @@ -90,12 +91,30 @@ public class SvgView extends ReactViewGroup implements ReactCompoundView, ReactC ((VirtualView) parent).getSvgView().invalidate(); return; } - if (mBitmap != null) { - mBitmap.recycle(); + // Additional: maybe its not necessary since Android 2.3.3 https://developer.android.com/topic/performance/graphics/manage-memory#recycle + if(!mRemovalTransitionStarted){ + if (mBitmap != null) { + mBitmap.recycle(); + } + mBitmap = null; + } + } + + @Override + public void startViewTransition(View view) { + super.startViewTransition(view); + mRemovalTransitionStarted = true; + } + + @Override + public void endViewTransition(View view) { + super.endViewTransition(view); + if (mRemovalTransitionStarted) { + mRemovalTransitionStarted = false; } - mBitmap = null; } + @Override protected void onDraw(Canvas canvas) { if (getParent() instanceof VirtualView) {Thanks
It works !! Thanks
+1 working on static and animated svgs.
can i get SvgView.java full code,thanks!!!
The same issue occurs with React Native Skia: when clicking away from the current screen on a bottom-tab
, the Skia
component becomes transparent for a split second. When returning to the screen, the Skia
component appears transparent again before becoming visible.
I can confirm that we are affected by an issue that seems familiar.
It might be the same issue.
We have a fix for it at Shopify/react-native-skia#1788.
However, I would like to understand the lifecycles methods I need to use on Android with react-native-screen to make sure the fix is clean enough. cc @kmagiera