software-mansion/react-native-screens

SVG component becomes transparent when goBack

hehex9 opened this issue Β· 33 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.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;
  }
}

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!

image

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:

  1. Go back with custom headerLeft (navigation.goBack())
  2. Go back with android hardware button (navigation.goBack())
  3. 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, not react-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.

@msvargas can confirm your patch works great on my device

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) and react-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.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

+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