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.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.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.currentWindowAdaptiveInfo
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.calculatePaneScaffoldDirective
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.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
@ -61,6 +67,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.tasks.BuildConfig
import org.tasks.R
import org.tasks.TasksApplication
@ -128,7 +135,6 @@ class MainActivity : AppCompatActivity() {
/** @see android.app.Activity.onCreate
*/
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
theme.applyTheme(this)
@ -147,257 +153,270 @@ class MainActivity : AppCompatActivity() {
)
setContent {
TasksTheme(theme = theme.themeBase.index) {
val navigator = rememberListDetailPaneScaffoldNavigator(
calculatePaneScaffoldDirective(
windowAdaptiveInfo = currentWindowAdaptiveInfo(),
verticalHingePolicy = HingePolicy.AlwaysAvoid,
).copy(
horizontalPartitionSpacerSize = 0.dp,
verticalPartitionSpacerSize = 0.dp,
),
val windowInsets = WindowInsets.systemBars.asPaddingValues()
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 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) {
Timber.d("onBackPressed")
if (intent.finishAffinity) {
finishAffinity()
} else if (isDetailVisible && navigator.canNavigateBack()) {
scope.launch {
navigator.navigateBack()
}
} else {
finish()
if (!preferences.getBoolean(R.string.p_open_last_viewed_list, true)) {
runBlocking {
viewModel.resetFilter()
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet(
drawerState = drawerState,
windowInsets = WindowInsets(0, 0, 0, 0),
) {
val context = LocalContext.current
val settingsRequest = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
context.findActivity()?.recreate()
}
}
}
}
LaunchedEffect(state.filter, state.task) {
actionMode?.finish()
actionMode = null
viewModel.closeDrawer()
}
ListDetailPaneScaffold(
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = {
key (state.filter) {
AndroidFragment<TaskListFragment>(
fragmentState = rememberFragmentState(),
arguments = remember(state.filter) {
Bundle()
.apply { putParcelable(EXTRA_FILTER, state.filter) }
val scope = rememberCoroutineScope()
val bottomSearchBar = atLeastR()
TaskListDrawer(
arrangement = when {
state.menuQuery.isBlank() -> Arrangement.Top
bottomSearchBar -> Arrangement.Bottom
else -> Arrangement.Top
},
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) {
withContext(Dispatchers.Main) {
context.findActivity()?.hideKeyboard()
}
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 (isListVisible && isDetailVisible) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(org.tasks.kmp.R.drawable.ic_launcher_no_shadow_foreground),
contentDescription = null,
modifier = Modifier.size(192.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
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) {
Timber.d("onBackPressed")
if (intent.finishAffinity) {
finishAffinity()
} else if (isDetailVisible && navigator.canNavigateBack()) {
scope.launch {
navigator.navigateBack()
}
} else {
key (state.task) {
AndroidFragment<TaskEditFragment>(
finish()
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(),
arguments = remember(state.task) {
arguments = remember(state.filter) {
Bundle()
.apply { putParcelable(EXTRA_TASK, state.task) }
.apply { putParcelable(EXTRA_FILTER, state.filter) }
},
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
)
},
) {
val context = LocalContext.current
val settingsRequest = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
context.findActivity()?.recreate()
}
val scope = rememberCoroutineScope()
val bottomSearchBar = atLeastR()
TaskListDrawer(
arrangement = when {
state.menuQuery.isBlank() -> Arrangement.Top
bottomSearchBar -> Arrangement.Bottom
else -> Arrangement.Top
},
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)
detailPane = {
Box(
modifier = Modifier
.fillMaxSize()
.padding(windowInsets),
contentAlignment = Alignment.Center,
) {
if (state.task == null) {
if (isListVisible && isDetailVisible) {
Icon(
painter = painterResource(org.tasks.kmp.R.drawable.ic_launcher_no_shadow_foreground),
contentDescription = null,
modifier = Modifier.size(192.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
},
onAddClick = {
scope.launch(Dispatchers.Default) {
sheetState.hide()
viewModel.closeDrawer()
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")
} else {
key(state.task) {
AndroidFragment<TaskEditFragment>(
fragmentState = rememberFragmentState(),
arguments = remember(state.task) {
Bundle()
.apply { putParcelable(EXTRA_TASK, state.task) }
},
modifier = Modifier.fillMaxSize(),
)
}
}
},
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 = "") }
}
fun openDrawer() {
_drawerOpen.update { true }
fun setDrawerState(opened: Boolean) {
_drawerOpen.update { opened }
if (!opened) {
_state.update { it.copy(menuQuery = "") }
}
}
init {

@ -29,6 +29,7 @@ import androidx.appcompat.widget.Toolbar
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.app.ShareCompat
@ -190,6 +191,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
private lateinit var search: MenuItem
private var mode: ActionMode? = null
lateinit var themeColor: ThemeColor
private var onClickMenu: () -> Unit = {}
private lateinit var binding: FragmentTaskListBinding
private val listPickerLauncher = registerForListPickerResult {
val selected = taskAdapter.getSelected()
@ -267,6 +269,32 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
.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)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@ -333,14 +361,12 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
}
val toolbar = run {
themeColor.apply(binding.bottomAppBar)
binding.bottomAppBar.isVisible = true
binding.toolbar.navigationIcon = null
binding.bottomAppBar
}
toolbar.setOnMenuItemClickListener(this)
toolbar.setNavigationOnClickListener {
activity?.hideKeyboard()
mainViewModel.openDrawer()
onClickMenu()
}
setupMenu(toolbar)
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.RowScope
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.fillMaxWidth
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.width
import androidx.compose.foundation.lazy.LazyColumn
@ -82,6 +80,7 @@ fun TaskListDrawer(
val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold(
modifier = Modifier
.imePadding()
.nestedScroll(
if (bottomSearchBar)
bottomAppBarScrollBehavior.nestedScrollConnection
@ -123,33 +122,20 @@ fun TaskListDrawer(
)
}
}
) { contentPadding ->
) { paddingValues ->
LazyColumn(
modifier = Modifier
.fillMaxSize(),
contentPadding = PaddingValues(
top = if (bottomSearchBar) 0.dp else contentPadding.calculateTopPadding(),
bottom = if (bottomSearchBar)
maxOf(
WindowInsets.mandatorySystemGestures
.asPaddingValues()
.calculateBottomPadding(),
contentPadding.calculateBottomPadding()
) else
48.dp
),
modifier = Modifier.fillMaxSize().imePadding(),
contentPadding = paddingValues,
verticalArrangement = arrangement,
) {
items(items = filters, key = { it.key() }) {
when (it) {
is DrawerItem.Filter -> FilterItem(
// modifier = Modifier.animateItemPlacement(),
item = it,
onClick = { onClick(it) }
)
is DrawerItem.Header -> HeaderItem(
// modifier = Modifier.animateItemPlacement(),
item = it,
canAdd = it.canAdd,
toggleCollapsed = { onClick(it) },

Loading…
Cancel
Save