Создавайте интуитивно понятные пользовательские интерфейсы 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.