Side navigation drawer

pull/3363/head
Alex Baker 9 months ago
parent 840049b206
commit d43bc55c02

@ -20,13 +20,18 @@ import androidx.appcompat.view.ActionMode
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.HingePolicy import androidx.compose.material3.adaptive.layout.HingePolicy
@ -36,9 +41,10 @@ import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -61,6 +67,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.tasks.BuildConfig import org.tasks.BuildConfig
import org.tasks.R import org.tasks.R
import org.tasks.TasksApplication import org.tasks.TasksApplication
@ -128,7 +135,6 @@ class MainActivity : AppCompatActivity() {
/** @see android.app.Activity.onCreate /** @see android.app.Activity.onCreate
*/ */
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
theme.applyTheme(this) theme.applyTheme(this)
@ -147,257 +153,270 @@ class MainActivity : AppCompatActivity() {
) )
setContent { setContent {
TasksTheme(theme = theme.themeBase.index) { val windowInsets = WindowInsets.systemBars.asPaddingValues()
val navigator = rememberListDetailPaneScaffoldNavigator(
calculatePaneScaffoldDirective(
windowAdaptiveInfo = currentWindowAdaptiveInfo(),
verticalHingePolicy = HingePolicy.AlwaysAvoid,
).copy(
horizontalPartitionSpacerSize = 0.dp,
verticalPartitionSpacerSize = 0.dp,
),
TasksTheme(theme = theme.themeBase.index) {
val drawerState = rememberDrawerState(
initialValue = DrawerValue.Closed,
confirmStateChange = {
viewModel.setDrawerState(it == DrawerValue.Open)
true
}
) )
val state = viewModel.state.collectAsStateWithLifecycle().value val state = viewModel.state.collectAsStateWithLifecycle().value
val isListVisible =
navigator.scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded
val isDetailVisible =
navigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded
val scope = rememberCoroutineScope()
LaunchedEffect(state.task) {
if (state.task == null) {
if (intent.finishAffinity) {
finishAffinity()
} else {
if (intent.removeTask && intent.broughtToFront) {
moveTaskToBack(true)
}
hideKeyboard()
navigator.navigateTo(pane = ThreePaneScaffoldRole.Secondary)
}
} else {
navigator.navigateTo(pane = ThreePaneScaffoldRole.Primary)
}
}
BackHandler(enabled = state.task == null) { ModalNavigationDrawer(
Timber.d("onBackPressed") drawerState = drawerState,
if (intent.finishAffinity) { drawerContent = {
finishAffinity() ModalDrawerSheet(
} else if (isDetailVisible && navigator.canNavigateBack()) { drawerState = drawerState,
scope.launch { windowInsets = WindowInsets(0, 0, 0, 0),
navigator.navigateBack() ) {
} val context = LocalContext.current
} else { val settingsRequest = rememberLauncherForActivityResult(
finish() ActivityResultContracts.StartActivityForResult()
if (!preferences.getBoolean(R.string.p_open_last_viewed_list, true)) { ) {
runBlocking { context.findActivity()?.recreate()
viewModel.resetFilter()
} }
} val scope = rememberCoroutineScope()
} val bottomSearchBar = atLeastR()
} TaskListDrawer(
LaunchedEffect(state.filter, state.task) { arrangement = when {
actionMode?.finish() state.menuQuery.isBlank() -> Arrangement.Top
actionMode = null bottomSearchBar -> Arrangement.Bottom
viewModel.closeDrawer() else -> Arrangement.Top
} },
ListDetailPaneScaffold( bottomSearchBar = bottomSearchBar,
directive = navigator.scaffoldDirective, filters = if (state.menuQuery.isNotEmpty()) state.searchItems else state.drawerItems,
value = navigator.scaffoldValue, onClick = {
listPane = { when (it) {
key (state.filter) { is DrawerItem.Filter -> {
AndroidFragment<TaskListFragment>( viewModel.setFilter(it.filter)
fragmentState = rememberFragmentState(), scope.launch(Dispatchers.Default) {
arguments = remember(state.filter) { withContext(Dispatchers.Main) {
Bundle() context.findActivity()?.hideKeyboard()
.apply { putParcelable(EXTRA_FILTER, state.filter) } }
drawerState.close()
}
}
is DrawerItem.Header -> {
viewModel.toggleCollapsed(it.header)
}
}
},
onAddClick = {
scope.launch(Dispatchers.Default) {
drawerState.close()
when (it.header.addIntentRc) {
FilterProvider.REQUEST_NEW_FILTER ->
NewFilterDialog.newFilterDialog().show(
supportFragmentManager,
SubheaderClickHandler.FRAG_TAG_NEW_FILTER
)
REQUEST_NEW_PLACE ->
startActivityForResult(
Intent(
this@MainActivity,
LocationPickerActivity::class.java
),
REQUEST_NEW_PLACE
)
REQUEST_NEW_TAGS ->
startActivityForResult(
Intent(
this@MainActivity,
TagSettingsActivity::class.java
),
REQUEST_NEW_LIST
)
REQUEST_NEW_LIST -> {
val account =
caldavDao.getAccount(it.header.id.toLong())
?: return@launch
when (it.header.subheaderType) {
NavigationDrawerSubheader.SubheaderType.CALDAV,
NavigationDrawerSubheader.SubheaderType.TASKS,
->
startActivityForResult(
Intent(
this@MainActivity,
account.listSettingsClass()
)
.putExtra(
EXTRA_CALDAV_ACCOUNT,
account
),
REQUEST_NEW_LIST
)
else -> {}
}
}
else -> Timber.e("Unhandled request code: $it")
}
}
},
onErrorClick = {
context.startActivity(Intent(context, MainPreferences::class.java))
},
searchBar = {
MenuSearchBar(
begForMoney = state.begForMoney,
onDrawerAction = {
scope.launch {
drawerState.close()
when (it) {
DrawerAction.PURCHASE ->
if (TasksApplication.IS_GENERIC)
context.openUri(R.string.url_donate)
else
context.startActivity(
Intent(
context,
PurchaseActivity::class.java
)
)
DrawerAction.SETTINGS ->
settingsRequest.launch(
Intent(
context,
MainPreferences::class.java
)
)
DrawerAction.HELP_AND_FEEDBACK ->
context.startActivity(
Intent(
context,
HelpAndFeedback::class.java
)
)
}
}
},
query = state.menuQuery,
onQueryChange = { viewModel.queryMenu(it) },
)
}, },
modifier = Modifier.fillMaxSize(),
) )
} }
}, }
detailPane = { ) {
val navigator = rememberListDetailPaneScaffoldNavigator(
calculatePaneScaffoldDirective(
windowAdaptiveInfo = currentWindowAdaptiveInfo(),
verticalHingePolicy = HingePolicy.AlwaysAvoid,
).copy(
horizontalPartitionSpacerSize = 0.dp,
verticalPartitionSpacerSize = 0.dp,
),
)
val isListVisible =
navigator.scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded
val isDetailVisible =
navigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded
val scope = rememberCoroutineScope()
LaunchedEffect(state.task) {
if (state.task == null) { if (state.task == null) {
if (isListVisible && isDetailVisible) { if (intent.finishAffinity) {
Box( finishAffinity()
modifier = Modifier.fillMaxSize(), } else {
contentAlignment = Alignment.Center, if (intent.removeTask && intent.broughtToFront) {
) { moveTaskToBack(true)
Icon(
painter = painterResource(org.tasks.kmp.R.drawable.ic_launcher_no_shadow_foreground),
contentDescription = null,
modifier = Modifier.size(192.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
} }
hideKeyboard()
navigator.navigateTo(pane = ThreePaneScaffoldRole.Secondary)
}
} else {
navigator.navigateTo(pane = ThreePaneScaffoldRole.Primary)
}
}
BackHandler(enabled = state.task == null) {
Timber.d("onBackPressed")
if (intent.finishAffinity) {
finishAffinity()
} else if (isDetailVisible && navigator.canNavigateBack()) {
scope.launch {
navigator.navigateBack()
} }
} else { } else {
key (state.task) { finish()
AndroidFragment<TaskEditFragment>( if (!preferences.getBoolean(R.string.p_open_last_viewed_list, true)) {
runBlocking {
viewModel.resetFilter()
}
}
}
}
LaunchedEffect(state.filter, state.task) {
actionMode?.finish()
actionMode = null
drawerState.close()
}
ListDetailPaneScaffold(
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = {
key (state.filter) {
val fragment = remember { mutableStateOf<TaskListFragment?>(null) }
AndroidFragment<TaskListFragment>(
fragmentState = rememberFragmentState(), fragmentState = rememberFragmentState(),
arguments = remember(state.task) { arguments = remember(state.filter) {
Bundle() Bundle()
.apply { putParcelable(EXTRA_TASK, state.task) } .apply { putParcelable(EXTRA_FILTER, state.filter) }
}, },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) ) { tlf ->
fragment.value = tlf
tlf.setNavigationClickListener {
scope.launch { drawerState.open() }
}
}
LaunchedEffect(fragment, windowInsets) {
fragment.value?.applyInsets(windowInsets)
}
} }
}
},
)
if (viewModel.drawerOpen.collectAsStateWithLifecycle().value) {
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
confirmValueChange = { true },
)
ModalBottomSheet(
modifier = Modifier.statusBarsPadding(),
sheetState = sheetState,
containerColor = MaterialTheme.colorScheme.surface,
onDismissRequest = { viewModel.closeDrawer() },
contentWindowInsets = {
WindowInsets(
left = 0,
top = 0,
right = 0,
bottom = 0
)
}, },
) { detailPane = {
val context = LocalContext.current Box(
val settingsRequest = rememberLauncherForActivityResult( modifier = Modifier
ActivityResultContracts.StartActivityForResult() .fillMaxSize()
) { .padding(windowInsets),
context.findActivity()?.recreate() contentAlignment = Alignment.Center,
} ) {
val scope = rememberCoroutineScope() if (state.task == null) {
val bottomSearchBar = atLeastR() if (isListVisible && isDetailVisible) {
TaskListDrawer( Icon(
arrangement = when { painter = painterResource(org.tasks.kmp.R.drawable.ic_launcher_no_shadow_foreground),
state.menuQuery.isBlank() -> Arrangement.Top contentDescription = null,
bottomSearchBar -> Arrangement.Bottom modifier = Modifier.size(192.dp),
else -> Arrangement.Top tint = MaterialTheme.colorScheme.onSurfaceVariant,
}, )
bottomSearchBar = bottomSearchBar,
filters = if (state.menuQuery.isNotEmpty()) state.searchItems else state.drawerItems,
onClick = {
when (it) {
is DrawerItem.Filter -> {
viewModel.setFilter(it.filter)
scope.launch(Dispatchers.Default) {
sheetState.hide()
viewModel.closeDrawer()
}
}
is DrawerItem.Header -> {
viewModel.toggleCollapsed(it.header)
} }
} } else {
}, key(state.task) {
onAddClick = { AndroidFragment<TaskEditFragment>(
scope.launch(Dispatchers.Default) { fragmentState = rememberFragmentState(),
sheetState.hide() arguments = remember(state.task) {
viewModel.closeDrawer() Bundle()
when (it.header.addIntentRc) { .apply { putParcelable(EXTRA_TASK, state.task) }
FilterProvider.REQUEST_NEW_FILTER -> },
NewFilterDialog.newFilterDialog().show( modifier = Modifier.fillMaxSize(),
supportFragmentManager, )
SubheaderClickHandler.FRAG_TAG_NEW_FILTER
)
REQUEST_NEW_PLACE ->
startActivityForResult(
Intent(
this@MainActivity,
LocationPickerActivity::class.java
),
REQUEST_NEW_PLACE
)
REQUEST_NEW_TAGS ->
startActivityForResult(
Intent(
this@MainActivity,
TagSettingsActivity::class.java
),
REQUEST_NEW_LIST
)
REQUEST_NEW_LIST -> {
val account =
caldavDao.getAccount(it.header.id.toLong())
?: return@launch
when (it.header.subheaderType) {
NavigationDrawerSubheader.SubheaderType.CALDAV,
NavigationDrawerSubheader.SubheaderType.TASKS,
->
startActivityForResult(
Intent(
this@MainActivity,
account.listSettingsClass()
)
.putExtra(
EXTRA_CALDAV_ACCOUNT,
account
),
REQUEST_NEW_LIST
)
else -> {}
}
}
else -> Timber.e("Unhandled request code: $it")
} }
} }
}, }
onErrorClick = { },
context.startActivity(Intent(context, MainPreferences::class.java)) )
},
searchBar = {
MenuSearchBar(
begForMoney = state.begForMoney,
onDrawerAction = {
viewModel.closeDrawer()
when (it) {
DrawerAction.PURCHASE ->
if (TasksApplication.IS_GENERIC)
context.openUri(R.string.url_donate)
else
context.startActivity(
Intent(
context,
PurchaseActivity::class.java
)
)
DrawerAction.SETTINGS ->
settingsRequest.launch(
Intent(
context,
MainPreferences::class.java
)
)
DrawerAction.HELP_AND_FEEDBACK ->
context.startActivity(
Intent(
context,
HelpAndFeedback::class.java
)
)
}
},
query = state.menuQuery,
onQueryChange = { viewModel.queryMenu(it) },
)
},
)
}
} }
} }
} }

@ -119,8 +119,11 @@ class MainActivityViewModel @Inject constructor(
_state.update { it.copy(menuQuery = "") } _state.update { it.copy(menuQuery = "") }
} }
fun openDrawer() { fun setDrawerState(opened: Boolean) {
_drawerOpen.update { true } _drawerOpen.update { opened }
if (!opened) {
_state.update { it.copy(menuQuery = "") }
}
} }
init { init {

@ -29,6 +29,7 @@ import androidx.appcompat.widget.Toolbar
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
@ -190,6 +191,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
private lateinit var search: MenuItem private lateinit var search: MenuItem
private var mode: ActionMode? = null private var mode: ActionMode? = null
lateinit var themeColor: ThemeColor lateinit var themeColor: ThemeColor
private var onClickMenu: () -> Unit = {}
private lateinit var binding: FragmentTaskListBinding private lateinit var binding: FragmentTaskListBinding
private val listPickerLauncher = registerForListPickerResult { private val listPickerLauncher = registerForListPickerResult {
val selected = taskAdapter.getSelected() val selected = taskAdapter.getSelected()
@ -267,6 +269,32 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
.launchIn(viewLifecycleOwner.lifecycleScope) .launchIn(viewLifecycleOwner.lifecycleScope)
} }
fun setNavigationClickListener(onClick: () -> Unit) {
onClickMenu = onClick
}
fun applyInsets(windowInsets: PaddingValues) {
val density = resources.displayMetrics.density
val actionBarHeight = TypedValue.complexToDimensionPixelSize(
getData(requireContext(), android.R.attr.actionBarSize),
resources.displayMetrics
)
with(binding.toolbar) {
val topInset = (windowInsets.calculateTopPadding().value * density).toInt()
val params = layoutParams
params.height = actionBarHeight + topInset
layoutParams = params
updatePadding(top = topInset)
}
with(binding.bottomAppBar) {
val bottomInset = (windowInsets.calculateBottomPadding().value * density).toInt()
val params = layoutParams
params.height = actionBarHeight + bottomInset
layoutParams = params
updatePadding(bottom = bottomInset)
}
}
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@ -333,14 +361,12 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
val toolbar = run { val toolbar = run {
themeColor.apply(binding.bottomAppBar) themeColor.apply(binding.bottomAppBar)
binding.bottomAppBar.isVisible = true
binding.toolbar.navigationIcon = null
binding.bottomAppBar binding.bottomAppBar
} }
toolbar.setOnMenuItemClickListener(this) toolbar.setOnMenuItemClickListener(this)
toolbar.setNavigationOnClickListener { toolbar.setNavigationOnClickListener {
activity?.hideKeyboard() activity?.hideKeyboard()
mainViewModel.openDrawer() onClickMenu()
} }
setupMenu(toolbar) setupMenu(toolbar)
binding.banner.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) binding.banner.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)

@ -12,12 +12,10 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.mandatorySystemGestures import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -82,6 +80,7 @@ fun TaskListDrawer(
val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold( Scaffold(
modifier = Modifier modifier = Modifier
.imePadding()
.nestedScroll( .nestedScroll(
if (bottomSearchBar) if (bottomSearchBar)
bottomAppBarScrollBehavior.nestedScrollConnection bottomAppBarScrollBehavior.nestedScrollConnection
@ -123,33 +122,20 @@ fun TaskListDrawer(
) )
} }
} }
) { contentPadding -> ) { paddingValues ->
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier.fillMaxSize().imePadding(),
.fillMaxSize(), contentPadding = paddingValues,
contentPadding = PaddingValues(
top = if (bottomSearchBar) 0.dp else contentPadding.calculateTopPadding(),
bottom = if (bottomSearchBar)
maxOf(
WindowInsets.mandatorySystemGestures
.asPaddingValues()
.calculateBottomPadding(),
contentPadding.calculateBottomPadding()
) else
48.dp
),
verticalArrangement = arrangement, verticalArrangement = arrangement,
) { ) {
items(items = filters, key = { it.key() }) { items(items = filters, key = { it.key() }) {
when (it) { when (it) {
is DrawerItem.Filter -> FilterItem( is DrawerItem.Filter -> FilterItem(
// modifier = Modifier.animateItemPlacement(),
item = it, item = it,
onClick = { onClick(it) } onClick = { onClick(it) }
) )
is DrawerItem.Header -> HeaderItem( is DrawerItem.Header -> HeaderItem(
// modifier = Modifier.animateItemPlacement(),
item = it, item = it,
canAdd = it.canAdd, canAdd = it.canAdd,
toggleCollapsed = { onClick(it) }, toggleCollapsed = { onClick(it) },

Loading…
Cancel
Save