Skip to content

Commit

Permalink
Fix configuration changes on folding phones
Browse files Browse the repository at this point in the history
- Remove container id values from extensions of `FragmentTransitions`
- Update `FeatureFragments` to be explicit with container ids
- Update `BookmarkListScreen` to collect the previous state when a bookmark detail is being displayed
- Update how the `MainActivity` handles configuration changes from folding phones
  • Loading branch information
fibelatti committed Jul 26, 2023
1 parent 4a9d35e commit ba3870e
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 81 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.fibelatti.pinboard.core.extension

import androidx.annotation.IdRes
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.commit
Expand All @@ -16,10 +17,10 @@ fun FragmentActivity.popTo(tag: String) {
}

fun FragmentActivity.slideFromTheRight(
@IdRes containerId: Int,
fragment: Fragment,
tag: String,
addToBackStack: Boolean = true,
containerId: Int = R.id.fragment_host,
) {
supportFragmentManager.commit {
setCustomAnimations(
Expand All @@ -37,10 +38,10 @@ fun FragmentActivity.slideFromTheRight(
}

fun FragmentActivity.slideUp(
@IdRes containerId: Int,
fragment: Fragment,
tag: String,
addToBackStack: Boolean = true,
containerId: Int = R.id.fragment_host,
) {
supportFragmentManager.commit {
setCustomAnimations(R.anim.slide_up, -1, -1, R.anim.slide_down)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,6 @@ import javax.inject.Inject

class FeatureFragments @Inject constructor(private val activity: FragmentActivity) {

var multiPanelEnabled: Boolean = false

private fun getDetailContainerId(): Int = if (multiPanelEnabled) {
R.id.fragment_host_side_panel
} else {
R.id.fragment_host
}

fun showLogin() {
with(activity.supportFragmentManager) {
if (findFragmentByTag(AuthFragment.TAG) == null) {
Expand Down Expand Up @@ -63,9 +55,9 @@ class FeatureFragments @Inject constructor(private val activity: FragmentActivit

if (activity.supportFragmentManager.findFragmentByTag(tag) == null) {
activity.slideFromTheRight(
containerId = R.id.fragment_host_side_panel,
fragment = activity.createFragment<PostDetailFragment>(),
tag = tag,
containerId = getDetailContainerId(),
)
} else {
activity.popTo(tag)
Expand All @@ -83,25 +75,41 @@ class FeatureFragments @Inject constructor(private val activity: FragmentActivit

fun showSearch() {
if (activity.supportFragmentManager.findFragmentByTag(PostSearchFragment.TAG) == null) {
activity.slideUp(activity.createFragment<PostSearchFragment>(), PostSearchFragment.TAG)
activity.slideUp(
containerId = R.id.fragment_host,
fragment = activity.createFragment<PostSearchFragment>(),
tag = PostSearchFragment.TAG,
)
}
}

fun showAddPost() {
if (activity.supportFragmentManager.findFragmentByTag(EditPostFragment.TAG) == null) {
activity.slideUp(activity.createFragment<EditPostFragment>(), EditPostFragment.TAG)
activity.slideUp(
containerId = R.id.fragment_host,
fragment = activity.createFragment<EditPostFragment>(),
tag = EditPostFragment.TAG,
)
}
}

fun showTags() {
if (activity.supportFragmentManager.findFragmentByTag(TagsFragment.TAG) == null) {
activity.slideUp(activity.createFragment<TagsFragment>(), TagsFragment.TAG)
activity.slideUp(
containerId = R.id.fragment_host,
fragment = activity.createFragment<TagsFragment>(),
tag = TagsFragment.TAG,
)
}
}

fun showNotes() {
if (activity.supportFragmentManager.findFragmentByTag(NoteListFragment.TAG) == null) {
activity.slideUp(activity.createFragment<NoteListFragment>(), NoteListFragment.TAG)
activity.slideUp(
containerId = R.id.fragment_host,
fragment = activity.createFragment<NoteListFragment>(),
tag = NoteListFragment.TAG,
)
} else {
activity.popTo(NoteListFragment.TAG)
}
Expand All @@ -110,30 +118,39 @@ class FeatureFragments @Inject constructor(private val activity: FragmentActivit
fun showNoteDetails() {
if (activity.supportFragmentManager.findFragmentByTag(NoteDetailsFragment.TAG) == null) {
activity.slideFromTheRight(
containerId = R.id.fragment_host_side_panel,
fragment = activity.createFragment<NoteDetailsFragment>(),
tag = NoteDetailsFragment.TAG,
containerId = getDetailContainerId(),
)
}
}

fun showPopular() {
if (activity.supportFragmentManager.findFragmentByTag(PopularPostsFragment.TAG) == null) {
activity.slideUp(activity.createFragment<PopularPostsFragment>(), PopularPostsFragment.TAG)
activity.slideUp(
containerId = R.id.fragment_host,
fragment = activity.createFragment<PopularPostsFragment>(),
tag = PopularPostsFragment.TAG,
)
} else {
activity.popTo(PopularPostsFragment.TAG)
}
}

fun showPreferences() {
if (activity.supportFragmentManager.findFragmentByTag(UserPreferencesFragment.TAG) == null) {
activity.slideUp(activity.createFragment<UserPreferencesFragment>(), UserPreferencesFragment.TAG)
activity.slideUp(
containerId = R.id.fragment_host,
fragment = activity.createFragment<UserPreferencesFragment>(),
tag = UserPreferencesFragment.TAG,
)
}
}

fun showEditPost() {
if (activity.supportFragmentManager.findFragmentByTag(EditPostFragment.TAG) == null) {
activity.slideUp(
containerId = R.id.fragment_host,
fragment = activity.createFragment<EditPostFragment>(),
tag = EditPostFragment.TAG,
addToBackStack = !activity.intent.fromBuilder,
Expand Down
101 changes: 64 additions & 37 deletions app/src/main/kotlin/com/fibelatti/pinboard/features/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
package com.fibelatti.pinboard.features

import android.animation.ValueAnimator
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.transition.ChangeBounds
import android.transition.TransitionManager
import android.view.View
import android.view.animation.LinearInterpolator
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.animation.doOnEnd
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.WindowCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.fibelatti.core.android.BaseIntentBuilder
import com.fibelatti.core.android.intentExtras
import com.fibelatti.core.extension.animateChangingTransitions
import com.fibelatti.core.extension.doOnInitializeAccessibilityNodeInfo
import com.fibelatti.core.extension.setupForAccessibility
import com.fibelatti.core.extension.viewBinding
Expand Down Expand Up @@ -80,6 +80,50 @@ class MainActivity : BaseActivity() {

private val binding by viewBinding(ActivityMainBinding::inflate)

// region ConstraintSets
private val constraintSetSidePanelHidden by lazy {
ConstraintSet().apply {
clone(binding.layoutContent)

connect(R.id.fragment_host, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
connect(R.id.fragment_host, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
constrainPercentWidth(R.id.fragment_host, 1f)

connect(R.id.fragment_host_side_panel, ConstraintSet.START, R.id.fragment_host, ConstraintSet.END)
connect(R.id.fragment_host_side_panel, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
setVisibility(R.id.fragment_host_side_panel, View.GONE)
}
}

private val constraintSetSidePanelOverlap by lazy {
ConstraintSet().apply {
clone(binding.layoutContent)

connect(R.id.fragment_host, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
connect(R.id.fragment_host, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
constrainPercentWidth(R.id.fragment_host, 1f)

connect(R.id.fragment_host_side_panel, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
connect(R.id.fragment_host_side_panel, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
setVisibility(R.id.fragment_host_side_panel, View.VISIBLE)
}
}

private val constraintSetSidePanelDivided by lazy {
ConstraintSet().apply {
clone(binding.layoutContent)

connect(R.id.fragment_host, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
connect(R.id.fragment_host, ConstraintSet.END, R.id.fragment_host_side_panel, ConstraintSet.START)
constrainPercentWidth(R.id.fragment_host, 0.4f)

connect(R.id.fragment_host_side_panel, ConstraintSet.START, R.id.fragment_host, ConstraintSet.END)
connect(R.id.fragment_host_side_panel, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
setVisibility(R.id.fragment_host_side_panel, View.VISIBLE)
}
}
// endregion ConstraintSets

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
Expand Down Expand Up @@ -107,13 +151,14 @@ class MainActivity : BaseActivity() {

binding.root.addView(
widthWindowSizeClassReactiveView { windowSizeClass ->
featureFragments.multiPanelEnabled = windowSizeClass != WindowSizeClass.COMPACT
mainViewModel.updateState { currentState ->
currentState.copy(multiPanelEnabled = windowSizeClass != WindowSizeClass.COMPACT)
}
},
)

binding.layoutContent.animateChangingTransitions()

binding.composeViewTopBar.setThemedContent {
MainTopAppBar(
mainViewModel = mainViewModel,
Expand Down Expand Up @@ -168,6 +213,8 @@ class MainActivity : BaseActivity() {
private fun handleContent(content: Content) {
onBackPressedCallback.isEnabled = (content as? ContentWithHistory)?.previousContent !is ExternalContent

setupSidePanel(show = content is SidePanelContent)

if (isRecreating) {
isRecreating = false
return
Expand All @@ -177,7 +224,6 @@ class MainActivity : BaseActivity() {
return
}

setupSidePanel(show = content is SidePanelContent)
showContentScreen(content)
}

Expand Down Expand Up @@ -218,37 +264,18 @@ class MainActivity : BaseActivity() {
currentState.copy(multiPanelContent = show)
}

val showSidePanel = featureFragments.multiPanelEnabled && show

when {
showSidePanel && binding.fragmentHostSidePanel.isGone -> {
binding.fragmentHostSidePanel.isVisible = true
animatedSidePanelWidth(endWidth = binding.root.width / 2)
}

!showSidePanel && binding.fragmentHostSidePanel.isVisible -> {
animatedSidePanelWidth(endWidth = 0) {
binding.fragmentHostSidePanel.isVisible = false
}
}
val constraintSet = when {
show && mainViewModel.state.value.multiPanelEnabled -> constraintSetSidePanelDivided
show -> constraintSetSidePanelOverlap
else -> constraintSetSidePanelHidden
}
}

private inline fun animatedSidePanelWidth(
endWidth: Int,
crossinline doOnEnd: () -> Unit = {},
) {
ValueAnimator.ofInt(binding.fragmentHostSidePanel.measuredWidth, endWidth)
.apply {
addUpdateListener { valueAnimator ->
val value = valueAnimator.animatedValue as Int
binding.fragmentHostSidePanel.updateLayoutParams<ConstraintLayout.LayoutParams> {
width = value
}
}
doOnEnd { doOnEnd() }
}
.start()
val transition = ChangeBounds()
.setInterpolator(LinearInterpolator())
.setDuration(300)

TransitionManager.beginDelayedTransition(binding.layoutContent, transition)
constraintSet.applyTo(binding.layoutContent)
}

override fun handleError(error: Throwable?, postAction: () -> Unit) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,37 +88,41 @@ fun BookmarkListScreen(
Surface(
color = ExtendedTheme.colors.backgroundNoOverlay,
) {
val appState by appStateViewModel.postListContent.collectAsStateWithLifecycle(initialValue = null)
val postListContent by rememberUpdatedState(newValue = appState ?: return@Surface)
val postListContent by appStateViewModel.postListContent.collectAsStateWithLifecycle(initialValue = null)
val postDetailContent by appStateViewModel.postDetailContent.collectAsStateWithLifecycle(initialValue = null)

val postListLoading = postListContent.shouldLoad != Loaded
val currentState by rememberUpdatedState(
newValue = postListContent ?: postDetailContent?.previousContent ?: return@Surface,
)

val postListLoading = currentState.shouldLoad != Loaded
val postDetailScreenState by postDetailViewModel.screenState.collectAsStateWithLifecycle()

val postListError by postListViewModel.error.collectAsStateWithLifecycle()
val postDetailError by postDetailViewModel.error.collectAsStateWithLifecycle()
val hasError = postListError != null || postDetailError != null

val shouldLoadContent = postListContent.shouldLoad is ShouldLoadFirstPage ||
postListContent.shouldLoad is ShouldForceLoad ||
postListContent.shouldLoad is ShouldLoadNextPage
val shouldLoadContent = currentState.shouldLoad is ShouldLoadFirstPage ||
currentState.shouldLoad is ShouldForceLoad ||
currentState.shouldLoad is ShouldLoadNextPage

LaunchedEffect(shouldLoadContent, postListContent) {
if (shouldLoadContent) postListViewModel.loadContent(postListContent)
LaunchedEffect(shouldLoadContent, currentState) {
if (shouldLoadContent) postListViewModel.loadContent(currentState)
}

BookmarkListScreen(
posts = postListContent.posts,
posts = currentState.posts,
isLoading = (postListLoading || postDetailScreenState.isLoading) && !hasError,
onScrollDirectionChanged = mainViewModel::setCurrentScrollDirection,
onNextPageRequested = { appStateViewModel.runAction(GetNextPostPage) },
searchParameters = postListContent.searchParameters,
searchParameters = currentState.searchParameters,
onClearClicked = { appStateViewModel.runAction(ClearSearch) },
onShareClicked = onShareClicked,
onPullToRefresh = { appStateViewModel.runAction(Refresh()) },
onPostClicked = { post -> appStateViewModel.runAction(ViewPost(post)) },
onPostLongClicked = onPostLongClicked,
onTagClicked = { post -> appStateViewModel.runAction(PostsForTag(post)) },
showPostDescription = postListContent.showDescription,
showPostDescription = currentState.showDescription,
)
}
}
Expand Down
Loading

0 comments on commit ba3870e

Please sign in to comment.