AbstractListDetailFragment.kt

/*
 * Copyright 2021 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.navigation.fragment

import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import androidx.activity.OnBackPressedCallback
import androidx.annotation.CallSuper
import androidx.core.content.res.use
import androidx.core.view.doOnLayout
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.commit
import androidx.slidingpanelayout.widget.SlidingPaneLayout

/**
 * A fragment supports adaptive two-pane layout. The first child is a list pane, which could be a
 * content list or browser, and the second child is NavHostFragment which controls to navigate
 * between different detail views.
 *
 * Implementation of the fragment should override this class and implement
 * [AbstractListDetailFragment.onCreateListPaneView] to supply custom view for the list pane. The
 * fragment provides default [NavHostFragment] with a NavGraph ID passed in the fragment, and it can
 * be overridden by [AbstractListDetailFragment.onCreateDetailPaneNavHostFragment] and provide
 * custom NavHostFragment.
 */
abstract class AbstractListDetailFragment : Fragment() {
    private var onBackPressedCallback: OnBackPressedCallback? = null
    private var _detailPaneNavHostFragment: NavHostFragment? = null
    private var graphId = 0

    /**
     * Return the [SlidingPaneLayout] this fragment is currently controlling.
     *
     * @throws IllegalStateException if the SlidingPaneLayout has not been created by [onCreateView]
     */
    val slidingPaneLayout: SlidingPaneLayout
        get() = requireView() as SlidingPaneLayout

    /**
     * Return the [NavHostFragment] this fragment uses
     *
     * @throws IllegalStateException if the NavHostFragment has not been created by
     * {@link #onCreateView}.
     */
    val detailPaneNavHostFragment: NavHostFragment
        get() {
            checkNotNull(_detailPaneNavHostFragment) {
                "Fragment $this was called before onCreateView()."
            }
            return _detailPaneNavHostFragment as NavHostFragment
        }

    private class InnerOnBackPressedCallback(
        private val slidingPaneLayout: SlidingPaneLayout
    ) :
        OnBackPressedCallback(true),
        SlidingPaneLayout.PanelSlideListener {

        init {
            slidingPaneLayout.addPanelSlideListener(this)
        }

        override fun handleOnBackPressed() {
            slidingPaneLayout.closePane()
        }

        override fun onPanelSlide(panel: View, slideOffset: Float) {}

        override fun onPanelOpened(panel: View) {
            // Intercept the system back button when the detail pane becomes visible.
            isEnabled = true
        }

        override fun onPanelClosed(panel: View) {
            // Disable intercepting the system back button when the user returns to the list pane.
            isEnabled = false
        }
    }

    @CallSuper
    override fun onInflate(
        context: Context,
        attrs: AttributeSet,
        savedInstanceState: Bundle?
    ) {
        super.onInflate(context, attrs, savedInstanceState)
        context.obtainStyledAttributes(
            attrs,
            androidx.navigation.R.styleable.NavHost
        ).use { navHost ->
            val graphId = navHost.getResourceId(
                androidx.navigation.R.styleable.NavHost_navGraph, 0
            )
            if (graphId != 0) {
                this.graphId = graphId
            }
        }
    }

    /**
     * Create the view for the fragment. This method provides two callbacks to instantiate a
     * list pane view and a NavHostFragment to control navigation between different detail views.
     *
     * @param inflater The [LayoutInflater] that used to inflate the fragment's views.
     * @param container The parent view that the fragment's UI should be attached to.
     * @param savedInstanceState The previous saved state of the fragment.
     *
     * @return Return the view for the fragment's UI
     *
     * @see onCreateListPaneView
     * @see onCreateDetailPaneNavHostFragment
     */
    @CallSuper
    final override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        if (savedInstanceState != null) {
            graphId = savedInstanceState.getInt(NavHostFragment.KEY_GRAPH_ID)
        }
        val slidingPaneLayout = SlidingPaneLayout(inflater.context).apply {
            id = R.id.sliding_pane_layout
        }

        // Create and add the list pane
        val listPaneView = onCreateListPaneView(inflater, slidingPaneLayout, savedInstanceState)
        if (listPaneView != slidingPaneLayout && listPaneView.parent != slidingPaneLayout) {
            slidingPaneLayout.addView(listPaneView)
        }

        // Set up the detail container
        val detailContainer = FragmentContainerView(inflater.context).apply {
            id = R.id.sliding_pane_detail_container
        }
        val detailWidth = inflater.context.resources.getDimensionPixelSize(
            R.dimen.sliding_pane_detail_pane_width
        )
        val detailLayoutParams = SlidingPaneLayout.LayoutParams(detailWidth, MATCH_PARENT).apply {
            weight = 1F
        }
        slidingPaneLayout.addView(detailContainer, detailLayoutParams)

        // Now create the NavHostFragment for the detail container
        val existingNavHostFragment =
            childFragmentManager.findFragmentById(R.id.sliding_pane_detail_container)
        _detailPaneNavHostFragment = if (existingNavHostFragment != null) {
            existingNavHostFragment as NavHostFragment
        } else {
            onCreateDetailPaneNavHostFragment().also { newNavHostFragment ->
                childFragmentManager
                    .commit {
                        setReorderingAllowed(true)
                        add(R.id.sliding_pane_detail_container, newNavHostFragment)
                    }
            }
        }
        onBackPressedCallback = InnerOnBackPressedCallback(slidingPaneLayout)
        slidingPaneLayout.doOnLayout {
            onBackPressedCallback!!.isEnabled =
                slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen
        }
        requireActivity().onBackPressedDispatcher.addCallback(
            viewLifecycleOwner,
            onBackPressedCallback!!
        )
        return slidingPaneLayout
    }

    /**
     * Provide a list pane view for the fragment. Called when creating the view of the fragment.
     *
     * @param inflater The [LayoutInflater] that used to inflate the list pane view.
     * @param container The parent view of the list pane view. The parent view can be used to
     * generate the LayoutParams of the view.
     * @param savedInstanceState The previous saved state of the fragment.
     *
     * @return Return the list pane view for the fragment.
     */
    abstract fun onCreateListPaneView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View

    /**
     * Return an alternative [NavHostFragment] to swap the default NavHostFragment in the
     * fragment. This method get called when creating the view of the fragment.
     */
    open fun onCreateDetailPaneNavHostFragment(): NavHostFragment {
        if (graphId != 0) {
            return NavHostFragment.create(graphId)
        }
        return NavHostFragment()
    }

    /**
     * This method provides a callback [onListPaneViewCreated] after the view hierarchy has
     * been completely created.
     *
     * @param view The view returned by [onCreateView]
     * @param savedInstanceState The previous saved state of the fragment.
     *
     * @see onListPaneViewCreated
     */
    @CallSuper
    final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val listPaneView = slidingPaneLayout.getChildAt(0)
        onListPaneViewCreated(listPaneView, savedInstanceState)
    }

    /**
     * Provides list pane view created in the fragment. Called when the fragment's [onViewCreated]
     * get called.
     *
     * @param view The list pane view created by [onCreateListPaneView] and added to view hierarchy
     * @param savedInstanceState The previous saved state of the fragment.
     */
    open fun onListPaneViewCreated(view: View, savedInstanceState: Bundle?) {}

    @CallSuper
    override fun onViewStateRestored(savedInstanceState: Bundle?) {
        super.onViewStateRestored(savedInstanceState)
        onBackPressedCallback!!.isEnabled =
            slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen
    }

    @CallSuper
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        if (graphId != 0) {
            outState.putInt(NavHostFragment.KEY_GRAPH_ID, graphId)
        }
    }
}