| /* |
| * 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.content.res.TypedArray; |
| import android.graphics.Canvas; |
| import android.os.Build; |
| 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 androidx.fragment.R; |
| |
| 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 can also be used to add a Fragment by using the |
| * <code>android:name</code> attribute. FragmentContainerView will perform a one time operation |
| * that: |
| * |
| * <ul> |
| * <li>Creates a new instance of the Fragment</li> |
| * <li>Calls {@link Fragment#onInflate(Context, AttributeSet, Bundle)}</li> |
| * <li>Executes a FragmentTransaction to add the Fragment to the appropriate FragmentManager</li> |
| * </ul> |
| * |
| * <p> You can optionally include an <code>android:tag</code> which allows you to use |
| * {@link FragmentManager#findFragmentByTag(String)} to retrieve the added Fragment. |
| * |
| * <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" |
| * android:name="com.example.MyFragment" |
| * android:tag="my_tag"> |
| * </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 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 for APIs above 17. |
| * Otherwise, 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) { |
| super(context); |
| } |
| |
| /** |
| * Do not call this constructor directly. Doing so will result in an |
| * {@link UnsupportedOperationException}. |
| */ |
| public FragmentContainerView(@NonNull Context context, @Nullable AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| /** |
| * Do not call this constructor directly. Doing so will result in an |
| * {@link UnsupportedOperationException}. |
| */ |
| public FragmentContainerView( |
| @NonNull Context context, |
| @Nullable AttributeSet attrs, |
| int defStyleAttr) { |
| super(context, attrs, defStyleAttr); |
| if (attrs != null) { |
| String name = attrs.getClassAttribute(); |
| String attribute = "class"; |
| TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FragmentContainerView); |
| if (name == null) { |
| name = a.getString(R.styleable.FragmentContainerView_android_name); |
| attribute = "android:name"; |
| } |
| a.recycle(); |
| if (name != null && !isInEditMode()) { |
| throw new UnsupportedOperationException("FragmentContainerView must be within " |
| + "a FragmentActivity to use " + attribute + "=\"" + name + "\""); |
| } |
| } |
| } |
| |
| FragmentContainerView( |
| @NonNull Context context, |
| @NonNull AttributeSet attrs, |
| @NonNull FragmentManager fm) { |
| super(context, attrs); |
| |
| String name = attrs.getClassAttribute(); |
| TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FragmentContainerView); |
| if (name == null) { |
| name = a.getString(R.styleable.FragmentContainerView_android_name); |
| } |
| String tag = a.getString(R.styleable.FragmentContainerView_android_tag); |
| a.recycle(); |
| |
| int id = getId(); |
| Fragment existingFragment = fm.findFragmentById(id); |
| // If there is a name and there is no existing fragment, |
| // we should add an inflated Fragment to the view. |
| if (name != null && existingFragment == null) { |
| if (id <= 0) { |
| final String tagMessage = tag != null |
| ? " with tag " + tag |
| : ""; |
| throw new IllegalStateException("FragmentContainerView must have an android:id to " |
| + "add Fragment " + name + tagMessage); |
| } |
| Fragment containerFragment = |
| fm.getFragmentFactory().instantiate(context.getClassLoader(), name); |
| containerFragment.onInflate(context, attrs, null); |
| fm.beginTransaction() |
| .setReorderingAllowed(true) |
| .add(this, containerFragment, tag) |
| .commitNowAllowingStateLoss(); |
| } |
| } |
| |
| /** |
| * When called, this method throws a {@link UnsupportedOperationException} on APIs above 17. |
| * On APIs 17 and below, it calls {@link FrameLayout#setLayoutTransition(LayoutTransition)} |
| * This can be called either explicitly, or implicitly by setting animateLayoutChanges to |
| * <code>true</code>. |
| * |
| * <p>View animations and transitions are disabled for FragmentContainerView for APIs above 17. |
| * 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) { |
| if (Build.VERSION.SDK_INT < 18) { |
| // Transitions on APIs below 18 are using an empty LayoutTransition as a replacement |
| // for suppressLayout(true) and null LayoutTransition to then unsuppress it. If the |
| // API is below 18, we should allow FrameLayout to handle this call. |
| super.setLayoutTransition(transition); |
| return; |
| } |
| |
| 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 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 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 (mTransitioningFragmentViews != null && mTransitioningFragmentViews.contains(v)) { |
| if (mDisappearingFragmentChildren == null) { |
| mDisappearingFragmentChildren = new ArrayList<>(); |
| } |
| mDisappearingFragmentChildren.add(v); |
| } |
| } |
| } |