Создавайте интуитивно понятные пользовательские интерфейсы Android
Pull для обновления — это распространенный шаблон проектирования, который используется пользователем для обновления списка элементов. В Jetpack Compose мы можем использовать библиотеку accompanist для реализации реализации по умолчанию в большинстве приложений для дизайна материалов.
Но, учитывая интерактивный характер этого шаблона проектирования, есть много возможностей порадовать пользователя анимацией и обратной связью.
Сегодня мы узнаем, как реализовать пользовательскую анимацию притягивания для обновления в компоновке.
Базовая реализация
Мы будем строить из имеющейся концертмейстерской библиотеки.
Он имеет реализацию по умолчанию, но также предоставляет свободу настройки по своему вкусу.
// Add the library to your project
implementation "com.google.accompanist:accompanist-swiperefresh:0.24.9-beta"
Затем мы создадим компонуемую оболочку, в которой мы реализуем нашу пользовательскую логику и заполним ее образцом пользовательского интерфейса.
CustomPullToRefresh( isRefreshing = isRefreshing, onRefresh = { refresh() } ) { LazyColumn { items(100) { index -> ListItem(index = index) } } }
Внутри компонуемого CustomPullToRefresh
мы можем добавить компонуемый SwipeRefresh
из библиотеки аккомпаниатора.
В параметре indicator
мы можем решить, как должен выглядеть наш пользовательский индикатор.
@Composable fun CustomPullToRefresh( modifier: Modifier = Modifier, isRefreshing: Boolean, onRefresh: () -> Unit, content: @Composable () -> Unit, ) { ... SwipeRefresh( modifier = modifier, state = pullState, onRefresh = onRefresh, refreshTriggerDistance = trigger, indicator = { state, triggerSize -> // Custom indicator logic here } ) { // List content } }
Параметр indicator
принимает компонуемую функцию и передает данные, которые мы можем использовать для создания пользовательского индикатора.
Переменная state
содержит такие данные, как расстояние, пройденное пользователем, и обновляется ли список, а triggerSize
— насколько велика область триггера.
Для моей пользовательской реализации я использую три состояния для создания пользовательского интерфейса:
isSwiping
(пользователь в данный момент проводит пальцем по экрану)willRefresh
(пользователь провел пальцем по экрануtriggerSize
. Отпускание в этот момент приведет к обновлению)isRefreshing
(Идет обновление)
val willRefresh = state.indicatorOffset.roundToInt() > triggerPx offset = when { willRefresh -> triggerPx.roundToInt() + (state.indicatorOffset.roundToInt() * .1f).roundToInt() // willRefresh state.isRefreshing -> triggerPx.roundToInt() // isRefreshing else -> state.indicatorOffset.roundToInt() //isSwiping }
Мы можем использовать эти состояния для обратной связи с пользователем. В данном случае мы используем их для определения смещения индикатора.
Если выпуск приведет к обновлению, мы можем сообщить об этом пользователю, увеличив смещение немного дальше. Мы могли бы также добавить немного тактильной обратной связи, когда эта точка будет достигнута.
Пока происходит обновление, мы удерживаем значение смещения в позиции триггера. Если ни одно из них не соответствует действительности, мы используем state.indicatorOffset
.
Для теста мы можем использовать эти значения для создания базового индикатора:
Box( modifier = Modifier .offset { IntOffset(0, -indicatorPx.roundToInt()) } .offset { IntOffset(y = offset, x = 0) } .background( color = when { willRefresh -> Color.Magenta state.isRefreshing -> Color.Green else -> Color.DarkGray }, ) .size(indicator) )
Это создаст квадрат, который перемещается со смещением и меняет цвет в зависимости от трех определенных нами состояний.
Необычная реализация
Базовая реализация помогает понять, как расширить SwipeRefresh
, но ее не следует использовать в рабочем приложении.
Для этого мы будем использовать эти состояния плюс несколько анимаций, чтобы создать более красивую реализацию, которой мы можем гордиться.
Анимация содержимого списка
Список занимает большую часть экрана, поэтому его анимация дает пользователю больше информации о том, что он собирается обновить список.
val scale by animateFloatAsState( targetValue = if (willRefresh) .95f else 1f, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, ) ) Box(modifier = Modifier .scale(scale) .offset { IntOffset(x = 0, y = animatedOffset) } .clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)) .fillMaxSize() .background(MaterialTheme.colors.surface) ) { content() }
Теперь мы используем смещение, чтобы переместить список вниз. Это имеет две основные цели.
Во-первых, перемещаем контент вниз, чтобы мы могли разместить нашу причудливую анимацию в пространстве выше.
Во-вторых, мы получаем преимущества визуальной обратной связи при отображении смещения, когда willRefresh
равно true.
Несколько других дополнений: добавление анимации масштаба, когда willRefresh
равно true, и добавление закругленных углов в начало списка.
Пользовательский индикатор обновления
Что касается индикатора, который я имею в виду, я хочу, чтобы он отображался из-за списка, когда пользователь тянет список вниз.
По этой причине я добавил индикатор к содержимому SwipeRefresh
после списка.
FancyRefreshAnimation( modifier = Modifier .align(Alignment.TopCenter) .fillMaxWidth(), isRefreshing = { pullState.isRefreshing }, willRefresh = { offset > triggerPx }, offsetProgress = { min(animatedOffset / triggerPx, 1f) } )
На этом этапе в игру вступают воображение и креативность, и вы можете использовать инструменты анимации, предоставленные вам композицией, чтобы создать что-то красивое.
Другой вариант, если у вас есть команда дизайнеров или вы умеете создавать анимации, — реализовать здесь собственную анимацию Лотти.
В моем случае я просто сделал несколько закругленных цветных прямоугольников и повернул их при обновлении. Вы можете найти FancyRefreshAnimation
компонуемый здесь вместе с остальной частью образца проекта.
Спасибо за чтение и удачи!
Первоначально опубликовано на https://sinasamaki.com.