FragmentContainerView.java

/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.fragment.app;

import android.animation.LayoutTransition;
import android.content.Context;
import android.graphics.Canvas;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import java.util.ArrayList;
/**
 * FragmentContainerView is a customized Layout designed specifically for Fragments. It extends
 * {@link FrameLayout}, so it can reliably handle Fragment Transactions, and it also has additional
 * features to coordinate with fragment behavior.
 *
 * <p>FragmentContainerView should be used as the container for Fragments, commonly set in the
 * xml layout of an activity, e.g.: <p>
 *
 * <pre class="prettyprint">
 * &lt;androidx.fragment.app.FragmentContainerView
 *        xmlns:android="http://schemas.android.com/apk/res/android"
 *        xmlns:app="http://schemas.android.com/apk/res-auto"
 *        android:id="@+id/fragment_container_view"
 *        android:layout_width="match_parent"
 *        android:layout_height="match_parent"&gt;
 * &lt;/androidx.fragment.app.FragmentContainerView&gt;
 * </pre>
 *
 * <p>FragmentContainerView should not be used as a replacement for other ViewGroups (FrameLayout,
 * LinearLayout, etc) outside of Fragment use cases.
 *
 * <p>FragmentContainerView will only allow views to returned by a Fragment's
 * {@link Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)}. Attempting to add any other
 * view will result in an {@link IllegalStateException}.
 *
 * <p>Layout animations and transitions are disabled for FragmentContainerView. Animations should be
 * done through {@link FragmentTransaction#setCustomAnimations(int, int, int, int)}. If
 * animateLayoutChanges is set to <code>true</code> or
 * {@link #setLayoutTransition(LayoutTransition)} is called directly an
 * {@link UnsupportedOperationException} will be thrown.
 *
 * <p>Fragments using exit animations are drawn before all others for FragmentContainerView. This
 * ensures that exiting Fragments do not appear on top of the view.
 */
public final class FragmentContainerView extends FrameLayout {

    private ArrayList<View> mDisappearingFragmentChildren;

    private ArrayList<View> mTransitioningFragmentViews;

    // Used to indicate whether the FragmentContainerView should override the default ViewGroup
    // drawing order.
    private boolean mDrawDisappearingViewsFirst = true;

    public FragmentContainerView(@NonNull Context context) {
        this(context, null);
    }

    public FragmentContainerView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FragmentContainerView(
            @NonNull Context context,
            @Nullable AttributeSet attrs,
            int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * When called, this method throws a {@link UnsupportedOperationException}. This can be called
     * either explicitly, or implicitly by setting animateLayoutChanges to <code>true</code>.
     *
     * <p>View animations and transitions are disabled for FragmentContainerView. Use
     * {@link FragmentTransaction#setCustomAnimations(int, int, int, int)} and
     * {@link FragmentTransaction#setTransition(int)}.
     *
     * @param transition The LayoutTransition object that will animated changes in layout. A value
     * of <code>null</code> means no transition will run on layout changes.
     * @attr ref android.R.styleable#ViewGroup_animateLayoutChanges
     */
    @Override
    public void setLayoutTransition(@Nullable LayoutTransition transition) {
        throw new UnsupportedOperationException(
                "FragmentContainerView does not support Layout Transitions or "
                        + "animateLayoutChanges=\"true\".");
    }

    /**
     * {@inheritDoc}
     *
     * <p>The sys ui flags must be set to enable extending the layout into the window insets.
     */
    @NonNull
    @RequiresApi(20)
    @Override
    public WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // Give child views fresh insets.
            child.dispatchApplyWindowInsets(new WindowInsets(insets));
        }
        return insets;
    }

    @Override
    protected void dispatchDraw(@NonNull Canvas canvas) {
        if (mDrawDisappearingViewsFirst && mDisappearingFragmentChildren != null) {
            for (int i = 0; i < mDisappearingFragmentChildren.size(); i++) {
                super.drawChild(canvas, mDisappearingFragmentChildren.get(i), getDrawingTime());
            }
        }
        super.dispatchDraw(canvas);
    }

    @Override
    protected boolean drawChild(@NonNull Canvas canvas, @NonNull View child, long drawingTime) {
        if (mDrawDisappearingViewsFirst && mDisappearingFragmentChildren != null
                && mDisappearingFragmentChildren.size() > 0) {
            // If the child is disappearing, we have already drawn it so skip.
            if (mDisappearingFragmentChildren.contains(child)) {
                return false;
            }
        }
        return super.drawChild(canvas, child, drawingTime);
    }

    @Override
    public void startViewTransition(@NonNull View view) {
        if (view.getParent() == this) {
            if (mTransitioningFragmentViews == null) {
                mTransitioningFragmentViews = new ArrayList<>();
            }
            mTransitioningFragmentViews.add(view);
        }
        super.startViewTransition(view);
    }

    @Override
    public void endViewTransition(@NonNull View view) {
        if (mTransitioningFragmentViews != null) {
            mTransitioningFragmentViews.remove(view);
            if (mDisappearingFragmentChildren != null
                    && mDisappearingFragmentChildren.remove(view)) {
                mDrawDisappearingViewsFirst = true;
            }
        }
        super.endViewTransition(view);
    }

    // Used to indicate the container should change the default drawing order.
    void setDrawDisappearingViewsLast(boolean drawDisappearingViewsFirst) {
        mDrawDisappearingViewsFirst = drawDisappearingViewsFirst;
    }

    /**
     * <p>FragmentContainerView will only allow views to returned by a Fragment's
     * {@link Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)}. Attempting to add any
     *  other view will result in an {@link IllegalStateException}.
     *
     * {@inheritDoc}
     */
    @Override
    public void addView(@NonNull View child, int index, @Nullable ViewGroup.LayoutParams params) {
        if (FragmentManager.getViewFragment(child) == null) {
            throw new IllegalStateException("Views added to a FragmentContainerView must be"
                    + " associated with a Fragment. View " + child + " is not associated with a"
                    + " Fragment.");
        }
        super.addView(child, index, params);
    }

    /**
     * <p>FragmentContainerView will only allow views to returned by a Fragment's
     * {@link Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)}. Attempting to add any
     *  other view will result in an {@link IllegalStateException}.
     *
     * {@inheritDoc}
     */
    @Override
    protected boolean addViewInLayout(@NonNull View child, int index,
            @Nullable ViewGroup.LayoutParams params, boolean preventRequestLayout) {
        if (FragmentManager.getViewFragment(child) == null) {
            throw new IllegalStateException("Views added to a FragmentContainerView must be"
                    + " associated with a Fragment. View " + child + " is not associated with a"
                    + " Fragment.");
        }
        return super.addViewInLayout(child, index, params, preventRequestLayout);
    }

    @Override
    public void removeViewAt(int index) {
        View view = getChildAt(index);
        addDisappearingFragmentView(view);
        super.removeViewAt(index);
    }

    @Override
    public void removeViewInLayout(@NonNull View view) {
        addDisappearingFragmentView(view);
        super.removeViewInLayout(view);
    }

    @Override
    public void removeView(@NonNull View view) {
        addDisappearingFragmentView(view);
        super.removeView(view);
    }

    @Override
    public void removeViews(int start, int count) {
        for (int i = start; i < start + count; i++) {
            final View view = getChildAt(i);
            addDisappearingFragmentView(view);
        }
        super.removeViews(start, count);
    }

    @Override
    public void removeViewsInLayout(int start, int count) {
        for (int i = start; i < start + count; i++) {
            final View view = getChildAt(i);
            addDisappearingFragmentView(view);
        }
        super.removeViewsInLayout(start, count);
    }

    @Override
    public void removeAllViewsInLayout() {
        for (int i = getChildCount() - 1; i >= 0; i--) {
            final View view = getChildAt(i);
            addDisappearingFragmentView(view);
        }
        super.removeAllViewsInLayout();
    }

    @Override
    protected void removeDetachedView(@NonNull View child, boolean animate) {
        if (animate) {
            addDisappearingFragmentView(child);
        }
        super.removeDetachedView(child, animate);
    }

    /**
     * This method adds a {@link View} to the list of disappearing views only if it meets the
     * proper conditions to be considered a disappearing view.
     *
     * @param v {@link View} that might be added to list of disappearing views
     */
    private void addDisappearingFragmentView(@NonNull View v) {
        if (v.getAnimation() != null || (mTransitioningFragmentViews != null
                && mTransitioningFragmentViews.contains(v))) {
            if (mDisappearingFragmentChildren == null) {
                mDisappearingFragmentChildren = new ArrayList<>();
            }
            mDisappearingFragmentChildren.add(v);
        }
    }



}