diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/mandate/BankTransferMandateFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/mandate/BankTransferMandateFragment.kt index ec1de0ac3d..41445a7370 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/mandate/BankTransferMandateFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/mandate/BankTransferMandateFragment.kt @@ -5,6 +5,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.mandate +import android.annotation.SuppressLint import android.os.Bundle import android.view.View import androidx.compose.animation.AnimatedVisibility @@ -12,22 +13,33 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.animateScrollBy +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource @@ -37,16 +49,16 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import kotlinx.coroutines.launch import org.signal.core.ui.Buttons import org.signal.core.ui.Dividers -import org.signal.core.ui.Scaffolds import org.signal.core.ui.Texts import org.signal.core.ui.theme.SignalTheme import org.signal.donations.PaymentSourceType import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse import org.thoughtcrime.securesms.compose.ComposeFragment -import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection +import org.thoughtcrime.securesms.compose.StatusBarColorAnimator import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.viewModel @@ -61,15 +73,15 @@ class BankTransferMandateFragment : ComposeFragment() { BankTransferMandateViewModel(PaymentSourceType.Stripe.SEPADebit) } - private lateinit var statusBarColorNestedScrollConnection: StatusBarColorNestedScrollConnection + private lateinit var statusBarColorAnimator: StatusBarColorAnimator override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - statusBarColorNestedScrollConnection = StatusBarColorNestedScrollConnection(requireActivity()) + statusBarColorAnimator = StatusBarColorAnimator(requireActivity()) } override fun onResume() { super.onResume() - statusBarColorNestedScrollConnection.setColorImmediate() + statusBarColorAnimator.setColorImmediate() } @Composable @@ -83,7 +95,7 @@ class BankTransferMandateFragment : ComposeFragment() { onNavigationClick = this::onNavigationClick, onContinueClick = this::onContinueClick, onLearnMoreClick = this::onLearnMoreClick, - modifier = Modifier.nestedScroll(statusBarColorNestedScrollConnection) + onCanScrollUp = statusBarColorAnimator::setCanScrollUp ) } @@ -119,11 +131,14 @@ fun BankTransferScreenPreview() { failedToLoadMandate = false, onNavigationClick = {}, onContinueClick = {}, - onLearnMoreClick = {} + onLearnMoreClick = {}, + onCanScrollUp = {} ) } } +@OptIn(ExperimentalMaterial3Api::class) +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun BankTransferScreen( bankMandate: String, @@ -131,91 +146,123 @@ fun BankTransferScreen( onNavigationClick: () -> Unit, onContinueClick: () -> Unit, onLearnMoreClick: () -> Unit, - modifier: Modifier = Modifier + onCanScrollUp: (Boolean) -> Unit ) { - Scaffolds.Settings( - title = stringResource(id = R.string.BankTransferMandateFragment__bank_transfer), - onNavigationClick = onNavigationClick, - navigationIconPainter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.symbol_arrow_left_24)), - titleContent = { contentOffset, title -> - AnimatedVisibility( - visible = contentOffset < 0f, - enter = fadeIn(), - exit = fadeOut() - ) { - Text(text = title, style = MaterialTheme.typography.titleLarge) - } - }, - modifier = modifier - ) { - LazyColumn( - horizontalAlignment = CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .padding(top = 64.dp) - ) { - item { - Image( - painter = painterResource(id = R.drawable.bank_transfer), - contentScale = ContentScale.Inside, - contentDescription = null, - modifier = Modifier - .size(72.dp) - .background( - SignalTheme.colors.colorSurface2, - CircleShape + val listState = rememberLazyListState() + val scope = rememberCoroutineScope() + + Scaffold( + topBar = { + TopAppBar( + title = { + AnimatedVisibility( + visible = listState.canScrollBackward, + enter = fadeIn(), + exit = fadeOut() + ) { + Text(text = stringResource(id = R.string.BankTransferMandateFragment__bank_transfer), style = MaterialTheme.typography.titleLarge) + } + }, + navigationIcon = { + IconButton( + onClick = onNavigationClick, + Modifier.padding(end = 16.dp) + ) { + Icon( + painter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.symbol_arrow_left_24)), + contentDescription = null ) - ) - } + } + }, + colors = if (listState.canScrollBackward) TopAppBarDefaults.topAppBarColors(containerColor = SignalTheme.colors.colorSurface2) else TopAppBarDefaults.topAppBarColors() + ) + } + ) { + onCanScrollUp(listState.canScrollBackward) - item { - Text( - text = stringResource(id = R.string.BankTransferMandateFragment__bank_transfer), - style = MaterialTheme.typography.headlineMedium, - modifier = Modifier.padding(top = 12.dp, bottom = 15.dp) - ) - } + Column(horizontalAlignment = CenterHorizontally, modifier = Modifier.fillMaxSize()) { + LazyColumn( + state = listState, + horizontalAlignment = CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .weight(1f, true) + .padding(top = 64.dp) + ) { + item { + Image( + painter = painterResource(id = R.drawable.bank_transfer), + contentScale = ContentScale.Inside, + contentDescription = null, + modifier = Modifier + .size(72.dp) + .background( + SignalTheme.colors.colorSurface2, + CircleShape + ) + ) + } - item { - val learnMore = stringResource(id = R.string.BankTransferMandateFragment__learn_more) - val fullString = stringResource(id = R.string.BankTransferMandateFragment__stripe_processes_donations, learnMore) + item { + Text( + text = stringResource(id = R.string.BankTransferMandateFragment__bank_transfer), + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(top = 12.dp, bottom = 15.dp) + ) + } - Texts.LinkifiedText( - textWithUrlSpans = SpanUtil.urlSubsequence(fullString, learnMore, stringResource(id = R.string.donate_url)), // TODO [alex] -- final URL - onUrlClick = { - onLearnMoreClick() - }, - style = MaterialTheme.typography.bodyLarge.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant - ), - modifier = Modifier - .padding(bottom = 12.dp) - .padding(horizontal = dimensionResource(id = R.dimen.bank_transfer_mandate_gutter)) - ) - } + item { + val learnMore = stringResource(id = R.string.BankTransferMandateFragment__learn_more) + val fullString = stringResource(id = R.string.BankTransferMandateFragment__stripe_processes_donations, learnMore) - item { - Dividers.Default() - } + Texts.LinkifiedText( + textWithUrlSpans = SpanUtil.urlSubsequence(fullString, learnMore, stringResource(id = R.string.donate_url)), // TODO [alex] -- final URL + onUrlClick = { + onLearnMoreClick() + }, + style = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + ), + modifier = Modifier + .padding(bottom = 12.dp) + .padding(horizontal = dimensionResource(id = R.dimen.bank_transfer_mandate_gutter)) + ) + } - item { - Text( - text = if (failedToLoadMandate) stringResource(id = R.string.BankTransferMandateFragment__failed_to_load_mandate) else bankMandate, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.bank_transfer_mandate_gutter), vertical = 16.dp) - ) + item { + Dividers.Default() + } + + item { + Text( + text = if (failedToLoadMandate) stringResource(id = R.string.BankTransferMandateFragment__failed_to_load_mandate) else bankMandate, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.bank_transfer_mandate_gutter), vertical = 16.dp) + ) + } } if (!failedToLoadMandate) { - item { + Surface( + shadowElevation = if (listState.canScrollForward) 8.dp else 0.dp, + modifier = Modifier.fillMaxWidth() + ) { Buttons.LargeTonal( - onClick = onContinueClick, + onClick = { + if (!listState.canScrollForward) { + onContinueClick() + } else { + scope.launch { + listState.animateScrollBy(value = 1000f) + } + } + }, modifier = Modifier - .padding(top = 16.dp, bottom = 46.dp) + .wrapContentWidth() + .padding(top = 16.dp, bottom = 16.dp) .defaultMinSize(minWidth = 220.dp) ) { - Text(text = stringResource(id = R.string.BankTransferMandateFragment__continue)) + Text(text = if (listState.canScrollForward) stringResource(id = R.string.BankTransferMandateFragment__read_more) else stringResource(id = R.string.BankTransferMandateFragment__continue)) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/compose/StatusBarColorAnimator.kt b/app/src/main/java/org/thoughtcrime/securesms/compose/StatusBarColorAnimator.kt new file mode 100644 index 0000000000..ff1ef7567d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/compose/StatusBarColorAnimator.kt @@ -0,0 +1,62 @@ +package org.thoughtcrime.securesms.compose + +import android.animation.ValueAnimator +import android.app.Activity +import androidx.core.content.ContextCompat +import com.google.android.material.animation.ArgbEvaluatorCompat +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.util.WindowUtil + +/** + * Controls status-bar color based off ability to scroll up + */ +class StatusBarColorAnimator( + private val activity: Activity +) { + private var animator: ValueAnimator? = null + private var previousCanScrollUp: Boolean = false + + private val normalColor = ContextCompat.getColor(activity, R.color.signal_colorBackground) + private val scrollColor = ContextCompat.getColor(activity, R.color.signal_colorSurface2) + + fun setCanScrollUp(canScrollUp: Boolean) { + if (previousCanScrollUp == canScrollUp) { + return + } + + previousCanScrollUp = canScrollUp + applyState(canScrollUp) + } + + fun setColorImmediate() { + val end = when { + previousCanScrollUp -> scrollColor + else -> normalColor + } + + animator?.cancel() + WindowUtil.setStatusBarColor( + activity.window, + end + ) + } + + private fun applyState(canScrollUp: Boolean) { + val (start, end) = when { + canScrollUp -> normalColor to scrollColor + else -> scrollColor to normalColor + } + + animator?.cancel() + animator = ValueAnimator.ofFloat(0f, 1f).apply { + duration = 200 + addUpdateListener { + WindowUtil.setStatusBarColor( + activity.window, + ArgbEvaluatorCompat.getInstance().evaluate(it.animatedFraction, start, end) + ) + } + start() + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3172c85613..469594f508 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5890,6 +5890,8 @@ Learn more Continue + + Read more Failed to load mandate