/*
* 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">
* <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">
* </androidx.fragment.app.FragmentContainerView>
* </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);
}
}
}