Как я создал трекер бюджета с помощью Jetpack Compose
3 февраля 2023 г.Я всегда с нетерпением жду возможности узнать что-то новое, и на этот раз я решил изучить Jetpack Compose.
Обычно я использую Google Sheets для отслеживания своих доходов/расходов, поэтому я решил создать приложение с помощью Compose, которое будет делать то же самое.
В этой статье я покажу ошибки, которые я допустил, и то, как я создал некоторые из настраиваемых компонентов, которые использовались в проекте.
Главный фоновый баннер
Я искал вдохновения и наткнулся на несколько других приложений, в которых использовался этот изогнутый фон, поэтому я решил использовать его для домашнего заголовка.
Этот компонент довольно прост, у нас есть изогнутый фон и остальное содержимое над ним. Что делает это немного сложнее, так это тот факт, что высота фона не статична, я сделал ее такой, чтобы она адаптировалась к содержимому, отображаемому над ним.
Сначала я начал с создания нестандартной формы с нужной мне кривизной. Для этого я расширил GenericShape
и использовал cubicTo
, чтобы нарисовать его. Я пришел к этим числам методом проб и ошибок, вы можете изменить их, если хотите, чтобы ваша фигура выглядела по-другому.
class SemiOvalShape : Shape by GenericShape(builder = { size, _ ->
lineTo(size.width, 0f)
relativeLineTo(0f, size.height * 0.8f)
cubicTo(
x1 = size.width * .7f,
y1 = size.height,
x2 = size.width * .3f,
y2 = size.height,
x3 = 0f,
y3 = size.height * 0.8f
)
})
После того, как у меня была пользовательская форма, я создал компонент заголовка.
@Composable
private fun HomeHeader(
...
) {
Box {
val density = LocalDensity.current
// How big the curved section of the box is
val backgroundBottomPadding = 96.dp
// How much spacing you have between the content inside your box and the content placed below it
val bottomSpacer = 16.dp
// Set to 264.dp initilly just so the calculatation below doesn't result in a negative number
var boxHeight by remember { mutableStateOf(264.dp) }
val contentTopPadding by remember {
derivedStateOf { boxHeight - backgroundBottomPadding + bottomSpacer }
}
Column(
modifier = Modifier
.fillMaxWidth()
// Code that draws the curved background
.background(remember {
Brush.verticalGradient(listOf(Purple40, Purple20))
}, remember {
SemiOvalShape()
})
.padding(horizontal = 16.dp)
.onSizeChanged {
boxHeight = with(density) { it.height.toDp() }
}
) {
... // The content that's shown inside the box goes here (Welcome Back, Income/Expenses)
Spacer(modifier = Modifier.height(backgroundBottomPadding))
}
Box(
modifier = Modifier
.padding(top = contentTopPadding)
.fillMaxWidth()
) {
... // The content that's shown below the box goes here (Add Expense, Add Income, ...)
}
}
}
Далее:
- Я определил некоторые переменные, чтобы указать, насколько большим должно быть расстояние между компонентами, при необходимости вы можете настроить их.
- Затем я рассчитал
contentTopPadding
, это то, что делает блок адаптируемым к разным размерам содержимого. - После этого я добавил столбец, который рисует изогнутый фон, его дочерние элементы рисуются внутри поля (Добро пожаловать обратно и Доходы/Расходы).
- Ниже я добавил блок, в котором размещается содержимое, показанное под изогнутым фоном (меню «Сетка»).
На изображении ниже вы можете видеть, что я добавил еще один компонент, и размер блока изменился правильно.
Анимация кнопки выбора
Это простой фильтр, который использует поле на заднем плане, чтобы указать, какой параметр выбран в данный момент.
Здесь происходит 3 вещи:
- Ширина поля изменяется
- Положение поля меняется
- Радиус угла изменяется (посмотрите на внутренние углы, когда выбран только 1 вариант)
Мы используем animateDpState
, чтобы легко анимировать значения dp. К сожалению, это невозможно для угловых форм, поэтому нам приходится создавать новую форму каждый раз, когда изменяется радиус.
val selectionOffsetX by animateDpAsState(
targetValue = if (selected == CategoryType.EXPENSE) halfBoxWidth else 0.dp,
)
val selectionWidth by animateDpAsState(
targetValue = if (selected == null) boxWidth else halfBoxWidth,
)
val leftCornerRadius by animateDpAsState(
targetValue = when (selected) {
CategoryType.EXPENSE -> 0.dp
else -> 4.dp
}
)
val rightCornerRadius by animateDpAsState(
targetValue = when (selected) {
CategoryType.INCOME -> 0.dp
else -> 4.dp
}
)
val selectionShape by remember {
derivedStateOf {
RoundedCornerShape(
topStart = leftCornerRadius,
bottomStart = leftCornerRadius,
topEnd = rightCornerRadius,
bottomEnd = rightCornerRadius,
)
}
}
Затем нам просто нужно определить компоненты, а Compose обрабатывает всю анимацию.
Мне понравилось, как получилась эта кнопка, учитывая ее простоту.
Box(modifier = modifier.width(boxWidth)) {
// this is the background that moves to hightlight what's currently selected
Box(
modifier = Modifier
.offset(x = selectionOffsetX)
.width(selectionWidth)
.height(buttonHeight)
.background(Purple20, selectionShape)
)
Row {
Button(
modifier = Modifier
...
.clickable(
// remove ripple from button
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { onSelected(...) }
)
)
Button(
modifier = Modifier
...
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { onSelected(...) }
)
)
}
}
Вертикальная/горизонтальная прокрутка
У меня есть таблица, на которой показано, сколько я тратил по категориям по месяцам. Это довольно легко визуализировать, когда вы используете большой экран, но для мобильных устройств мне пришлось придумать другой подход.
Мне не нравится ландшафтный режим, поэтому я даже не рассматривал его здесь как вариант. В итоге я сделал все прокручиваемым, но месяцы и категории были фиксированными.
val verticalScroll = rememberScrollState()
val horizontalScroll = rememberScrollState()
Row(
modifier = Modifier.horizontalScroll(horizontalScroll)
) {
... // Oct/22, Sep/22, ...
}
Column(
modifier = Modifier
.verticalScroll(verticalScroll)
) {
// Food, Gym, Car, Subscriptions
}
Column(
modifier = Modifier
.verticalScroll(verticalScroll)
.horizontalScroll(horizontalScroll)
) {
// 563, 212, 525, 222, 662, 12, 661, ...
}
Если вы еще раз посмотрите на видео выше, вы увидите, что всякий раз, когда я прокручиваю основной контент, другие части также прокручиваются. Я обнаружил, что можно использовать один и тот же ScrollState
несколько раз, я понятия не имел, что это сработает, но с помощью одного и того же ScrollState
я могу синхронизировать все компоненты.< /p>
Если вы хотите увидеть, как я создал весь этот компонент, вы можете взглянуть на этот файл.
Весь код для этого проекта доступен в этом репозитории.
ПС:
Я далеко не эксперт в Compose. В этой статье показано, как я создавал эти компоненты, используя знания, которые у меня были на тот момент, а это означает, что это не обязательно самый производительный код или самый простой способ сделать определенную вещь. Если вы знаете, что можно улучшить, сообщите мне об этом в комментариях.
Также опубликовано здесь
Фото на обложке Маркус Винклер на Unsplash
Оригинал