September 5

Создаем атомный лоадер на Jetpack Compose

В этой статье мы создадим потрясающую 3D-анимацию Atomic Loader в Jetpack Compose.

Вдохновение

Эта анимация загрузчика вдохновлена оригинальной работой, созданной Мартином ван Дриелем с помощью HTML и CSS, которая распространяется по MIT License.

Я воссоздал этот эффект с помощью Jetpack Compose, чтобы перенести его на Android.

Создание вращающегося круга

Для начала нам нужно создать composable функцию RotatingCircle. Эта функция будет отображать один круг, который мы будем использовать для создания лоадера.

@Composable
fun RotatingCircle(
    modifier: Modifier,
    rotationX: Float,
    rotationY: Float,
    rotationZ: Float,
    borderColor: Color,
    borderWidth: Dp
) {
    // Further implementation
}

Рисуем круг

Чтобы добиться нужного эффекта, мы вычитаем один круг из другого и рисуем результат на Canvas:

Canvas(modifier) {
    // Define the path for the main circle
    val mainCircle = Path().apply {
        addOval(Rect(size.center, size.minDimension / 2))
    }

    // Adjust the position of the clipping circle to the left by borderWidth
    val clipCenter = Offset(size.width / 2f - borderWidth.toPx(), size.height / 2f)
    // Define the path for the clipping circle
    val clipCircle = Path().apply {
        addOval(Rect(clipCenter, size.minDimension / 2))
    }

    // Subtract the clipping circle from the main circle
    val path = Path().apply {
        op(mainCircle, clipCircle, PathOperation.Difference)
    }

    // Draw the subtracted path
    drawPath(path, borderColor)
}

Трансформация

Далее мы используем graphicsLayer для поворота холста в трехмерном пространстве:

Canvas(
    modifier.graphicsLayer {
        this.rotationX = rotationX
        this.rotationY = rotationY
        this.rotationZ = rotationZ
        // To create a depth effect adjust the cameraDistance
        cameraDistance = 12f * density
    }
) {
    // Drawing logic
}

Составляем лоадер

Для рендеринга загрузчика мы определяем композабл функцию AtomicLoader:

@Composable
fun AtomicLoader(
    modifier: Modifier,
    color: Color = Color.White,
    borderWidth: Dp = 3.dp,
    cycleDuration: Int = 1000
)

Параметры:

  • modifier — модификатор, который будет применен к контейнеру загрузчика
  • color — цвет загрузчика
  • borderWidth — ширина границы загрузчика
  • cycleDuration — продолжительность одного полного цикла вращения в миллисекундах

Создаем анимацию вращения

Чтобы анимировать загрузчик, мы зададим бесконечное вращение, которое будет циклически изменяться от 0 до 360 градусов:

val infiniteTransition = rememberInfiniteTransition("InfiniteAtomicLoaderTransition")

val rotation by infiniteTransition.animateFloat(
    initialValue = 0f,
    targetValue = 360f,
    animationSpec = infiniteRepeatable(
        animation = tween(cycleDuration, easing = LinearEasing)
    ),
    label = "AtomicLoaderRotation"
)

Размещаем круги

Наконец, мы разместим круги внутри блока Box и применим анимацию вращения:

Box(modifier) {
    RotatingCircle(
        modifier = Modifier.matchParentSize(),
        rotationX = 35f,
        rotationY = -45f,
        rotationZ = -90f + rotation,
        borderColor = color,
        borderWidth = borderWidth
    )
    RotatingCircle(
        modifier = Modifier.matchParentSize(),
        rotationX = 50f,
        rotationY = 10f,
        rotationZ = rotation,
        borderColor = color,
        borderWidth = borderWidth
    )
    RotatingCircle(
        modifier = Modifier.matchParentSize(),
        rotationX = 35f,
        rotationY = 55f,
        rotationZ = 90f + rotation,
        borderColor = color,
        borderWidth = borderWidth
    )
}

Поздравляю! Мы сделали это. Полный код вы можете найти на GitHub. Давайте изучим возможности использования.

Практическое использование

Давайте создадим Box с радиальным градиентным фоном и поместим загрузчик в центр, чтобы увидеть результат.

⚠️ Убедитесь, что загрузчик имеет четко определенный размер! В противном случае он может не отобразиться или вызвать неожиданное поведение.

Box(
    modifier = Modifier
        .fillMaxSize()
        .background(
            brush = Brush.radialGradient(
                listOf(Color(0xFF3C4B57), Color(0xFF1C262B))
            )
        ),
    contentAlignment = Alignment.Center
) {
    AtomicLoader(
        Modifier.size(100.dp)
    )
}

Вот что получилось:

(Источник)