| /* |
| * 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.util.AttributeSet |
| import android.view.View |
| import android.view.ViewGroup |
| import android.view.WindowInsets |
| import android.widget.FrameLayout |
| import androidx.annotation.RequiresApi |
| import androidx.core.content.withStyledAttributes |
| import androidx.core.view.ViewCompat |
| import androidx.core.view.WindowInsetsCompat |
| import androidx.fragment.R |
| |
| /** |
| * FragmentContainerView is a customized Layout designed specifically for Fragments. It extends |
| * [FrameLayout], so it can reliably handle Fragment Transactions, and it also has additional |
| * features to coordinate with fragment behavior. |
| * |
| * FragmentContainerView should be used as the container for Fragments, commonly set in the xml |
| * layout of an activity, e.g.: |
| * ``` |
| * <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> |
| * ``` |
| * |
| * FragmentContainerView can also be used to add a Fragment by using the `android:name` attribute. |
| * FragmentContainerView will perform a one time operation that: |
| * 1. Creates a new instance of the Fragment |
| * 2. Calls [Fragment.onInflate] |
| * 3. Executes a FragmentTransaction to add the Fragment to the appropriate FragmentManager |
| * |
| * You can optionally include an `android:tag` which allows you to use |
| * [FragmentManager.findFragmentByTag] to retrieve the added Fragment. |
| * |
| * ``` |
| * <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> |
| * ``` |
| * |
| * FragmentContainerView should not be used as a replacement for other ViewGroups (FrameLayout, |
| * LinearLayout, etc) outside of Fragment use cases. |
| * |
| * FragmentContainerView will only allow views returned by a Fragment's [Fragment.onCreateView]. |
| * Attempting to add any other view will result in an [IllegalStateException]. |
| * |
| * Layout animations and transitions are disabled for FragmentContainerView for APIs above 17. |
| * Otherwise, Animations should be done through [FragmentTransaction.setCustomAnimations]. If |
| * animateLayoutChanges is set to `true` or [setLayoutTransition] is called directly an |
| * [UnsupportedOperationException] will be thrown. |
| * |
| * 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 class FragmentContainerView : FrameLayout { |
| private val disappearingFragmentChildren: MutableList<View> = mutableListOf() |
| private val transitioningFragmentViews: MutableList<View> = mutableListOf() |
| private var applyWindowInsetsListener: OnApplyWindowInsetsListener? = null |
| |
| // Used to indicate whether the FragmentContainerView should override the default ViewGroup |
| // drawing order. |
| private var drawDisappearingViewsFirst = true |
| |
| public constructor(context: Context) : super(context) |
| |
| /** |
| * Do not call this constructor directly. Doing so will result in an |
| * [UnsupportedOperationException]. |
| */ |
| @JvmOverloads |
| public constructor( |
| context: Context, |
| attrs: AttributeSet?, |
| defStyleAttr: Int = 0 |
| ) : super(context, attrs, defStyleAttr) { |
| if (attrs != null) { |
| var name = attrs.classAttribute |
| var attribute = "class" |
| context.withStyledAttributes(attrs, R.styleable.FragmentContainerView) { |
| if (name == null) { |
| name = getString(R.styleable.FragmentContainerView_android_name) |
| attribute = "android:name" |
| } |
| } |
| if (name != null && !isInEditMode) { |
| throw UnsupportedOperationException( |
| "FragmentContainerView must be within a FragmentActivity to use $attribute" + |
| "=\"$name\"" |
| ) |
| } |
| } |
| } |
| |
| internal constructor( |
| context: Context, |
| attrs: AttributeSet, |
| fm: FragmentManager |
| ) : super(context, attrs) { |
| var name = attrs.classAttribute |
| var tag: String? = null |
| context.withStyledAttributes(attrs, R.styleable.FragmentContainerView) { |
| if (name == null) { |
| name = getString(R.styleable.FragmentContainerView_android_name) |
| } |
| tag = getString(R.styleable.FragmentContainerView_android_tag) |
| } |
| val id = id |
| val existingFragment: Fragment? = 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 == View.NO_ID) { |
| val tagMessage = if (tag != null) " with tag $tag" else "" |
| throw IllegalStateException( |
| "FragmentContainerView must have an android:id to add Fragment $name$tagMessage" |
| ) |
| } |
| val containerFragment: Fragment = |
| fm.fragmentFactory.instantiate(context.classLoader, name) |
| containerFragment.mFragmentId = id |
| containerFragment.mContainerId = id |
| containerFragment.mTag = tag |
| containerFragment.mFragmentManager = fm |
| containerFragment.mHost = fm.host |
| containerFragment.onInflate(context, attrs, null) |
| fm.beginTransaction() |
| .setReorderingAllowed(true) |
| .add(this, containerFragment, tag) |
| .commitNowAllowingStateLoss() |
| } |
| fm.onContainerAvailable(this) |
| } |
| |
| /** |
| * When called, this method throws a [UnsupportedOperationException] on APIs above 17. On APIs |
| * 17 and below, it calls [FrameLayout.setLayoutTransition]. This can be called either |
| * explicitly, or implicitly by setting animateLayoutChanges to `true`. |
| * |
| * View animations and transitions are disabled for FragmentContainerView for APIs above 17. Use |
| * [FragmentTransaction.setCustomAnimations] and [FragmentTransaction.setTransition]. |
| * |
| * @param transition The LayoutTransition object that will animated changes in layout. A value |
| * of `null` means no transition will run on layout changes. |
| * @attr ref android.R.styleable#ViewGroup_animateLayoutChanges |
| */ |
| public override fun setLayoutTransition(transition: LayoutTransition?) { |
| throw UnsupportedOperationException( |
| "FragmentContainerView does not support Layout Transitions or " + |
| "animateLayoutChanges=\"true\"." |
| ) |
| } |
| |
| public override fun setOnApplyWindowInsetsListener(listener: OnApplyWindowInsetsListener) { |
| applyWindowInsetsListener = listener |
| } |
| |
| @RequiresApi(20) |
| public override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets = insets |
| |
| /** |
| * {@inheritDoc} |
| * |
| * The sys ui flags must be set to enable extending the layout into the window insets. |
| */ |
| @RequiresApi(20) |
| public override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets { |
| val insetsCompat = WindowInsetsCompat.toWindowInsetsCompat(insets) |
| val dispatchInsets = |
| if (applyWindowInsetsListener != null) { |
| WindowInsetsCompat.toWindowInsetsCompat( |
| Api20Impl.onApplyWindowInsets(applyWindowInsetsListener!!, this, insets) |
| ) |
| } else { |
| ViewCompat.onApplyWindowInsets(this, insetsCompat) |
| } |
| if (!dispatchInsets.isConsumed) { |
| for (i in 0 until childCount) { |
| ViewCompat.dispatchApplyWindowInsets(getChildAt(i), dispatchInsets) |
| } |
| } |
| return insets |
| } |
| |
| protected override fun dispatchDraw(canvas: Canvas) { |
| if (drawDisappearingViewsFirst) { |
| disappearingFragmentChildren.forEach { child -> |
| super.drawChild(canvas, child, drawingTime) |
| } |
| } |
| super.dispatchDraw(canvas) |
| } |
| |
| protected override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean { |
| if (drawDisappearingViewsFirst && disappearingFragmentChildren.isNotEmpty()) { |
| // If the child is disappearing, we have already drawn it so skip. |
| if (disappearingFragmentChildren.contains(child)) { |
| return false |
| } |
| } |
| return super.drawChild(canvas, child, drawingTime) |
| } |
| |
| public override fun startViewTransition(view: View) { |
| if (view.parent === this) { |
| transitioningFragmentViews.add(view) |
| } |
| super.startViewTransition(view) |
| } |
| |
| public override fun endViewTransition(view: View) { |
| transitioningFragmentViews.remove(view) |
| if (disappearingFragmentChildren.remove(view)) { |
| drawDisappearingViewsFirst = true |
| } |
| super.endViewTransition(view) |
| } |
| |
| // Used to indicate the container should change the default drawing order. |
| @JvmName("setDrawDisappearingViewsLast") |
| internal fun setDrawDisappearingViewsLast(drawDisappearingViewsFirst: Boolean) { |
| this.drawDisappearingViewsFirst = drawDisappearingViewsFirst |
| } |
| |
| /** |
| * FragmentContainerView will only allow views returned by a Fragment's [Fragment.onCreateView]. |
| * Attempting to add any other view will result in an [IllegalStateException]. |
| * |
| * {@inheritDoc} |
| */ |
| public override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) { |
| checkNotNull(FragmentManager.getViewFragment(child)) { |
| ("Views added to a FragmentContainerView must be associated with a Fragment. View " + |
| "$child is not associated with a Fragment.") |
| } |
| super.addView(child, index, params) |
| } |
| |
| public override fun removeViewAt(index: Int) { |
| val view = getChildAt(index) |
| addDisappearingFragmentView(view) |
| super.removeViewAt(index) |
| } |
| |
| public override fun removeViewInLayout(view: View) { |
| addDisappearingFragmentView(view) |
| super.removeViewInLayout(view) |
| } |
| |
| public override fun removeView(view: View) { |
| addDisappearingFragmentView(view) |
| super.removeView(view) |
| } |
| |
| public override fun removeViews(start: Int, count: Int) { |
| for (i in start until start + count) { |
| val view = getChildAt(i) |
| addDisappearingFragmentView(view) |
| } |
| super.removeViews(start, count) |
| } |
| |
| public override fun removeViewsInLayout(start: Int, count: Int) { |
| for (i in start until start + count) { |
| val view = getChildAt(i) |
| addDisappearingFragmentView(view) |
| } |
| super.removeViewsInLayout(start, count) |
| } |
| |
| public override fun removeAllViewsInLayout() { |
| for (i in childCount - 1 downTo 0) { |
| val view = getChildAt(i) |
| addDisappearingFragmentView(view) |
| } |
| super.removeAllViewsInLayout() |
| } |
| |
| /** |
| * This method adds a [View] to the list of disappearing views only if it meets the proper |
| * conditions to be considered a disappearing view. |
| * |
| * @param v [View] that might be added to list of disappearing views |
| */ |
| private fun addDisappearingFragmentView(v: View) { |
| if (transitioningFragmentViews.contains(v)) { |
| disappearingFragmentChildren.add(v) |
| } |
| } |
| |
| /** |
| * This method grabs the [Fragment] whose view was most recently added to the container. This |
| * may used as an alternative to calling [FragmentManager.findFragmentById] and passing in the |
| * [FragmentContainerView]'s id. |
| * |
| * @return The fragment if any exist, null otherwise. |
| */ |
| @Suppress("UNCHECKED_CAST") // a ClassCastException is automatically thrown if the given type |
| // of F is wrong |
| public fun <F : Fragment?> getFragment(): F = |
| FragmentManager.findFragmentManager(this).findFragmentById(this.id) as F |
| |
| @RequiresApi(20) |
| internal object Api20Impl { |
| fun onApplyWindowInsets( |
| onApplyWindowInsetsListener: OnApplyWindowInsetsListener, |
| v: View, |
| insets: WindowInsets |
| ): WindowInsets = onApplyWindowInsetsListener.onApplyWindowInsets(v, insets) |
| } |
| } |