/*
* 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.preference
import android.content.Context
import android.content.Intent
import android.os.Bundle
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.activity.OnBackPressedDispatcherOwner
import androidx.annotation.CallSuper
import androidx.core.view.doOnLayout
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit
import androidx.slidingpanelayout.widget.SlidingPaneLayout
/**
* [PreferenceHeaderFragmentCompat] implements a two-pane fragment for preferences. The list
* pane is a container of preference headers. Tapping on a preference header swaps out the fragment
* shown in the detail pane. Subclasses are expected to implement [onCreatePreferenceHeader] to
* provide your own [PreferenceFragmentCompat] in the list pane. The preference header hierarchy
* is defined by either providing an XML resource or build in code through
* [PreferenceFragmentCompat]. In both cases, users need to use a [PreferenceScreen] as the root
* component in the hierarchy.
*
* Usage:
*
* ```
* class TwoPanePreference : PreferenceHeaderFragmentCompat() {
* override fun onCreatePreferenceHeader(): PreferenceFragmentCompat {
* return PreferenceHeader()
* }
* }
* ```
*
* [PreferenceHeaderFragmentCompat] handles the fragment transaction when users defines a
* fragment or intent associated with the preference header. By default, the initial state fragment
* for the detail pane is set to the associated fragment that first found in preference
* headers. You can override [onCreateInitialDetailFragment] to provide the custom empty state
* fragment for the detail pane.
*/
abstract class PreferenceHeaderFragmentCompat :
Fragment(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
private var onBackPressedCallback: OnBackPressedCallback? = null
/**
* 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
@CallSuper
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
pref: Preference
): Boolean {
if (caller.id == R.id.preferences_header) {
// Opens the preference header.
openPreferenceHeader(pref)
return true
}
if (caller.id == R.id.preferences_detail) {
// Opens an preference in detail pane.
val frag = childFragmentManager.fragmentFactory.instantiate(
requireContext().classLoader,
pref.fragment!!
)
frag.arguments = pref.extras
childFragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.preferences_detail, frag)
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
addToBackStack(null)
}
return true
}
return false
}
private class InnerOnBackPressedCallback(
private val caller: PreferenceHeaderFragmentCompat
) :
OnBackPressedCallback(true),
SlidingPaneLayout.PanelSlideListener {
init {
caller.slidingPaneLayout.addPanelSlideListener(this)
}
override fun handleOnBackPressed() {
caller.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 onAttach(context: Context) {
super.onAttach(context)
parentFragmentManager.commit {
setPrimaryNavigationFragment(this@PreferenceHeaderFragmentCompat)
}
}
@CallSuper
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val slidingPaneLayout = buildContentView(inflater)
// Now create the header fragment
val existingHeaderFragment = childFragmentManager.findFragmentById(
R.id.preferences_header
)
if (existingHeaderFragment == null) {
onCreatePreferenceHeader().also { newHeaderFragment ->
childFragmentManager.commit {
setReorderingAllowed(true)
add(R.id.preferences_header, newHeaderFragment)
}
}
}
slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
return slidingPaneLayout
}
private fun buildContentView(inflater: LayoutInflater): SlidingPaneLayout {
val slidingPaneLayout = SlidingPaneLayout(inflater.context).apply {
id = R.id.preferences_sliding_pane_layout
}
// Add Preference Header Pane
val headerContainer = FragmentContainerView(inflater.context).apply {
id = R.id.preferences_header
}
val headerLayoutParams = SlidingPaneLayout.LayoutParams(
resources.getDimensionPixelSize(R.dimen.preferences_header_width),
MATCH_PARENT
).apply {
weight = resources.getInteger(R.integer.preferences_header_pane_weight).toFloat()
}
slidingPaneLayout.addView(
headerContainer,
headerLayoutParams
)
// Add Preference Detail Pane
val detailContainer = FragmentContainerView(inflater.context).apply {
id = R.id.preferences_detail
}
val detailLayoutParams = SlidingPaneLayout.LayoutParams(
resources.getDimensionPixelSize(R.dimen.preferences_detail_width),
MATCH_PARENT
).apply {
weight = resources.getInteger(R.integer.preferences_detail_pane_weight).toFloat()
}
slidingPaneLayout.addView(
detailContainer,
detailLayoutParams
)
return slidingPaneLayout
}
/**
* Called to supply the preference header for this fragment. The subclasses are expected
* to call [setPreferenceScreen(PreferenceScreen)] either directly or via helper methods
* such as [setPreferenceFromResource(int)] to set headers.
*/
abstract fun onCreatePreferenceHeader(): PreferenceFragmentCompat
@CallSuper
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onBackPressedCallback = InnerOnBackPressedCallback(this)
slidingPaneLayout.doOnLayout {
onBackPressedCallback!!.isEnabled =
slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen
}
childFragmentManager.addOnBackStackChangedListener {
onBackPressedCallback!!.isEnabled = childFragmentManager.backStackEntryCount == 0
}
val onBackPressedDispatcherOwner = requireContext() as? OnBackPressedDispatcherOwner
onBackPressedDispatcherOwner?.let {
it.onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
onBackPressedCallback!!
)
}
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
if (savedInstanceState == null) {
onCreateInitialDetailFragment()?.let {
childFragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.preferences_detail, it)
}
}
}
}
/**
* Override this method to set initial detail fragment that to be shown. The default
* implementation returns the first preference that has a fragment defined on
* it.
*
* @return Fragment The first fragment that found in the list of preference headers.
*/
open fun onCreateInitialDetailFragment(): Fragment? {
val headerFragment = childFragmentManager.findFragmentById(R.id.preferences_header)
as PreferenceFragmentCompat
if (headerFragment.preferenceScreen.preferenceCount <= 0) {
return null
}
for (index in 0 until headerFragment.preferenceScreen.preferenceCount) {
val header = headerFragment.preferenceScreen.getPreference(index)
if (header.fragment == null) {
continue
}
val fragment = header.fragment?.let {
childFragmentManager.fragmentFactory.instantiate(
requireContext().classLoader,
it
)
}
return fragment
}
return null
}
/**
* Swaps out the fragment that associated with preference header. If associated fragment is
* unspecified, open the preference with the given intent instead.
*
* @param header The preference header that was selected
*/
private fun openPreferenceHeader(header: Preference) {
if (header.fragment == null) {
openPreferenceHeader(header.intent)
return
}
val fragment = header.fragment?.let {
childFragmentManager.fragmentFactory.instantiate(
requireContext().classLoader,
it
)
}
fragment?.apply {
arguments = header.extras
}
// Clear back stack
if (childFragmentManager.backStackEntryCount > 0) {
val entry = childFragmentManager.getBackStackEntryAt(0)
childFragmentManager.popBackStack(entry.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
}
childFragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.preferences_detail, fragment!!)
if (slidingPaneLayout.isOpen) {
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
}
slidingPaneLayout.openPane()
}
}
/**
* Open preference with the given intent
*
* @param intent The intent that associated with preference header
*/
private fun openPreferenceHeader(intent: Intent?) {
if (intent == null) return
// TODO: Change to use WindowManager ActivityView API
startActivity(intent)
}
}