Search code examples
androidandroid-tablayoutandroid-appbarlayout

Glitch when removing TabLayout from AppBarLayout with removeView()


When a item is clicked in recyclerview i call this

appbar.removeView(tabs)

This video shows what happens

It seems to remove the TabLayout entirely without animation, then add it back, then use animation to remove it. I have slowed down the transition to show what its doing.

it also happens when adding them back like this

if (tabs.parent != null) {
        (tabs.parent as ViewGroup).removeView(tabs)
    }
    appbar.addView(tabs)
    appbar.setExpanded(true, true)

Here is my layout

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/coordinator_main_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:theme="@style/Theme.AppCompat.Light.NoActionBar">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:animateLayoutChanges="true"
        app:elevation="0dp"
        android:theme="@style/ThemeOverlay.AppCompat.ActionBar">

        <android.support.v7.widget.Toolbar
            xmlns:android="http://schemas.android.com/apk/res/android"
            style="@style/MyToolbar"
            android:id="@+id/toolbar"
            android:layout_height="wrap_content"
            android:layout_width="match_parent"
            app:layout_scrollFlags="scroll|enterAlways|snap"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

        <android.support.design.widget.TabLayout
            android:id="@+id/tabs"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:tabTextAppearance="@style/TabText"
            android:layout_marginRight="@dimen/small_spacing"
            android:layout_marginLeft="@dimen/small_spacing"
            app:tabMode="fixed"
            app:tabGravity="fill"
            app:tabIndicatorHeight="2dp"
            app:layout_scrollFlags="enterAlways"/>

    </android.support.design.widget.AppBarLayout>


    <FrameLayout
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/appbar"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" >

        <***.***.CustomViewPager
            android:id="@+id/viewpager"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </FrameLayout>

</android.support.design.widget.CoordinatorLayout>

Solution

  • So what is going on?

    Everything seems to work OK except there is that flash you mention. I took Chris Banes' Cheese Square app and made a few modifications to duplicate what you are seeing. Clicking the FAB removes and adds the tabs.

    Here is a video of what I came up with.

    enter image description here

    Here is a screen capture of the problem when the TabLayout is removed. As you can see, the RecyclerView is overlaying the appbar.

    enter image description here

    Here is a screen capture of the problem when the TabLayout is added back in. Here you can see that RecyclerView is shifted down to its position after the view is added.

    enter image description here

    The layout transition of the disappearing TabLayout is being done with LayoutTransition. To do an effective transition, LayoutTransition must determine what the final layout looks like so a set of animations can be built. This necessitates laying out what the screen will look like after the transition. It appears that the final layout is being displayed briefly before the animations run. (My guess.) This could be a race condition or it could be related to this caveat:

    This class, and the associated XML flag for containers, animateLayoutChanges="true", provides a simple utility meant for automating changes in straightforward situations. Using LayoutTransition at multiple levels of a nested view hierarchy may not work due to the interrelationship of the various levels of layout. Also, a container that is being scrolled at the same time as items are being added or removed is probably not a good candidate for this utility, because the before/after locations calculated by LayoutTransition may not match the actual locations when the animations finish due to the container being scrolled as the animations are running. You can work around that particular issue by disabling the 'changing' animations by setting the CHANGE_APPEARING and CHANGE_DISAPPEARING animations to null, and setting the startDelay of the other animations appropriately.

    Kudos and reputation to someone who can figure out exactly what is going on. (This issue may be the artifacts mentioned in the following cryptic comment in the code for LayoutTransition:)

    /**
     * Controls whether changing animations automatically animate the parent hierarchy as well.
     * This behavior prevents artifacts when wrap_content layouts snap to the end state as the
     * transition begins, causing visual glitches and clipping.
     * Default value is true.
     */
    private boolean mAnimateParentHierarchy = true;
    

    How to fix it?

    I suggest that you abandon LayoutTransitions altogether (android:animateLayoutChanges="false") and proceed with TransitionManager.

    We'll make use of the convenience method TransitionManager#beginDelayedTransition:

    beginDelayedTransition

    void beginDelayedTransition (ViewGroup sceneRoot, Transition transition)

    Convenience method to animate to a new scene defined by all changes within the given scene root between calling this method and the next rendering frame. Calling this method causes TransitionManager to capture current values in the scene root and then post a request to run a transition on the next frame. At that time, the new values in the scene root will be captured and changes will be animated. There is no need to create a Scene; it is implied by changes which take place between calling this method and the next frame when the transition begins.

    Here is my code for removing and adding the tabs. Make sure to set animateLayoutChanges="false".

    fab.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (mTabsAdded) {
                // Get rid of the indicator due to an on-screen artifact.
                tabs.setSelectedTabIndicatorHeight(0);
                TransitionSet set = new TransitionSet()
                    .addTransition(new Fade(OUT))
                    .addTransition(new ChangeBounds());
                TransitionManager.beginDelayedTransition(layout, set);
                mAppBar.removeView(tabs);
            } else {
                // Add tab indicator back in.
                int indicatorHeight = (int) TypedValue.applyDimension(
                    TypedValue.COMPLEX_UNIT_DIP, 2,
                    getResources().getDisplayMetrics());
                tabs.setSelectedTabIndicatorHeight(indicatorHeight);
                TransitionSet set = new TransitionSet()
                    .addTransition(new Fade(IN))
                    .addTransition(new ChangeBounds());
                TransitionManager.beginDelayedTransition(layout, set);
                mAppBar.addView(tabs);
            }
            mTabsAdded = !mTabsAdded;
        }
    });
    

    enter image description here