From 5465c0cb94552df7ab4100c6c84bbb27eb0d804b Mon Sep 17 00:00:00 2001 From: "yasin.cidem" Date: Fri, 21 Jul 2023 15:15:38 +0300 Subject: [PATCH 1/3] Add shimmer module --- README.md | 2 +- buildSrc/src/main/kotlin/Dependencies.kt | 1 + libraries/shimmer-compose/.gitignore | 1 + libraries/shimmer-compose/README.md | 63 +++ libraries/shimmer-compose/build.gradle.kts | 47 +++ .../uicomponents/shimmercompose/Shimmer.kt | 388 ++++++++++++++++++ sample-compose/build.gradle.kts | 1 + .../samplecompose/ComponentsScreen.kt | 4 + .../uicomponents/samplecompose/Route.kt | 2 + .../samplecompose/SampleComposeNavGraph.kt | 4 + .../samplecompose/ShimmerScreen.kt | 126 ++++++ settings.gradle.kts | 1 + 12 files changed, 639 insertions(+), 1 deletion(-) create mode 100644 libraries/shimmer-compose/.gitignore create mode 100644 libraries/shimmer-compose/README.md create mode 100644 libraries/shimmer-compose/build.gradle.kts create mode 100644 libraries/shimmer-compose/src/main/java/com/trendyol/uicomponents/shimmercompose/Shimmer.kt create mode 100644 sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ShimmerScreen.kt diff --git a/README.md b/README.md index 5477fb10..e18a48c9 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ UI Components includes several custom views for Android platform to make develop * [TimelineView](libraries/timeline-view): **TimelineView** for creating a timeline and show actions on it. * [TimelineViewCompose](libraries/timeline-view-compose): **TimelineView** for creating a timeline for compose and show actions on it. * [FitOptionMessage](libraries/fit-option-message-view): **FitOptionMessageView** for displaying text views with clipped imageviews. - +* [Shimmer](libraries/shimmer-compose): **Shimmer** for displaying shimmer for loading state. License -------- Copyright 2022 Trendyol.com diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 014db04c..4e627995 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -15,6 +15,7 @@ object Dependencies { const val composeBom = "androidx.compose:compose-bom:2023.05.01" const val composeActivity = "androidx.activity:activity-compose:1.3.0-alpha07" const val composeMaterial = "androidx.compose.material:material" + const val composeUiUtil = "androidx.compose.ui:ui-util" const val composeUiTooling = "androidx.compose.ui:ui-tooling" const val composeRuntime = "androidx.compose.runtime:runtime" const val composeCoil = "io.coil-kt:coil-compose:2.2.1" diff --git a/libraries/shimmer-compose/.gitignore b/libraries/shimmer-compose/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/libraries/shimmer-compose/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/libraries/shimmer-compose/README.md b/libraries/shimmer-compose/README.md new file mode 100644 index 00000000..12a529a3 --- /dev/null +++ b/libraries/shimmer-compose/README.md @@ -0,0 +1,63 @@ +shimmerComposeVersion = **shimmer-compose-1.0.0** [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + +# Shimmer Modifier + +The `shimmer` modifier allows you to display a shimmer with a custom shape for loading state. + +# Parameters + +The `Shimmer` modifier accepts the following parameters: + + +| Parameter Name | Type | Description | +|-------------------------|-------------|--------------------------------| +| isVisible | Boolean | It determines if the shimmer isVisible or not | +| isWipeOffAnimationEnabled | Boolean | It enables or disables wipe-off animation of the shimmer | +| shape | Shape | It determines the shimmer's shape | + +# Usage + +```kotlin +val title by mutableStateOf("") + +Text( + text = title, + modifier = Modifier + .fillMaxWidth() + .shimmer(title == "") +) +``` + +# Installation + +- To implement **Shimmer** to your Android project via Gradle, you need to add JitPack repository to your root build.gradle. + +```groovy + allprojects { + repositories { + // ... + + maven { url 'https://jitpack.io' } + } + } +``` + +Open your module-level `build.gradle` file and add the `Shimmer` dependency: + +```groovy +dependencies { + implementation "com.github.Trendyol.android-ui-components:shimmer-compose:$shimmerComposeVersion" +} +``` + +# Contributors + +This library is maintained mainly by Trendyol Android Team members but also other Android lovers contributes. + +# License + +``` +Copyright 2023 Trendyol.com Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +``` \ No newline at end of file diff --git a/libraries/shimmer-compose/build.gradle.kts b/libraries/shimmer-compose/build.gradle.kts new file mode 100644 index 00000000..1537bc0a --- /dev/null +++ b/libraries/shimmer-compose/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + id(Plugins.androidLibrary) + id(Plugins.kotlinAndroid) + id(Plugins.mavenPublish) +} + +android { + namespace = "com.trendyol.shimmercompose" + compileSdk = Configs.compileSdkVersion + + defaultConfig { + minSdk = Configs.minSdkVersion + targetSdk = Configs.targetSdkVersion + + vectorDrawables.useSupportLibrary = true + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + setProguardFiles( + mutableListOf( + getDefaultProguardFile("proguard-android.txt"), + "proguard-rules.pro" + ) + ) + } + } + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = Dependencies.kotlinCompilerExtensionVersion + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(platform(Dependencies.composeBom)) + implementation(Dependencies.composeMaterial) + implementation(Dependencies.composeUiUtil) + implementation(Dependencies.composeUiTooling) + implementation(Dependencies.composeRuntime) +} \ No newline at end of file diff --git a/libraries/shimmer-compose/src/main/java/com/trendyol/uicomponents/shimmercompose/Shimmer.kt b/libraries/shimmer-compose/src/main/java/com/trendyol/uicomponents/shimmercompose/Shimmer.kt new file mode 100644 index 00000000..c06fa614 --- /dev/null +++ b/libraries/shimmer-compose/src/main/java/com/trendyol/uicomponents/shimmercompose/Shimmer.kt @@ -0,0 +1,388 @@ +package com.trendyol.uicomponents.shimmercompose + +import androidx.compose.animation.core.withInfiniteAnimationFrameMillis +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.DrawModifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.OnGloballyPositionedModifier +import androidx.compose.ui.layout.positionInRoot +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.util.lerp +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.isActive +import kotlin.math.max + +@Stable +class PlaceholderState internal constructor( + private val isVisible: State, + private val maxScreenDimension: Float +) { + suspend fun startShimmer() { + coroutineScope { + while (isActive) { + withInfiniteAnimationFrameMillis { + frameMillis.value = it + } + } + } + } + + val placeholderProgression: Float by derivedStateOf { + val absoluteProgression = + (frameMillis.value.mod(PLACEHOLDER_GAP_BETWEEN_ANIMATION_LOOPS_MS).coerceAtMost( + PLACEHOLDER_PROGRESSION_DURATION_MS + ).toFloat() / + PLACEHOLDER_PROGRESSION_DURATION_MS) + val progression = lerp(-maxScreenDimension, maxScreenDimension, absoluteProgression) + progression + } + + + val isShowContent: Boolean by derivedStateOf { + placeholderStage == PlaceholderStage.ShowContent + } + + val isWipeOff: Boolean by derivedStateOf { + placeholderStage == PlaceholderStage.WipeOff + } + + internal val gradientWidth: Float by derivedStateOf { + maxScreenDimension + } + + internal var placeholderStage: PlaceholderStage = + if (isVisible.value) PlaceholderStage.ShowPlaceholder + else PlaceholderStage.ShowContent + get() { + if (field != PlaceholderStage.ShowContent) { + if (startOfNextPlaceholderAnimation == 0L) { + startOfNextPlaceholderAnimation = + (frameMillis.value.div(PLACEHOLDER_GAP_BETWEEN_ANIMATION_LOOPS_MS) + 1) * + PLACEHOLDER_GAP_BETWEEN_ANIMATION_LOOPS_MS + } else if (frameMillis.value >= startOfNextPlaceholderAnimation) { + field = checkForStageTransition( + field, + isVisible.value.not(), + ) + startOfNextPlaceholderAnimation = + (frameMillis.value.div(PLACEHOLDER_GAP_BETWEEN_ANIMATION_LOOPS_MS) + 1) * + PLACEHOLDER_GAP_BETWEEN_ANIMATION_LOOPS_MS + } + } + return field + } + + private val frameMillis = mutableStateOf(0L) + + private var startOfNextPlaceholderAnimation = 0L +} + +@Composable +fun rememberShimmerState(isVisible: Boolean): PlaceholderState { + val maxScreenDimension = with(LocalDensity.current) { + Dp(max(screenHeightDp(), screenWidthDp()).toFloat()).toPx() + } + val isShimmerVisibleState = rememberUpdatedState(isVisible) + return remember { + PlaceholderState( + isShimmerVisibleState, + maxScreenDimension + ) + } +} + +private fun checkForStageTransition( + placeholderStage: PlaceholderStage, + contentReady: Boolean +): PlaceholderStage { + return if (placeholderStage == PlaceholderStage.ShowPlaceholder && contentReady) { + PlaceholderStage.WipeOff + } else if (placeholderStage == PlaceholderStage.WipeOff) { + PlaceholderStage.ShowContent + } else placeholderStage +} + +fun Modifier.shimmer( + isVisible: Boolean, + isWipeOffAnimationEnabled: Boolean = true, + shape: Shape? = null, +): Modifier = composed { + + val shimmerState = rememberShimmerState(isVisible) + + if (!shimmerState.isShowContent) { + LaunchedEffect(shimmerState) { + shimmerState.startShimmer() + } + } + + then( + Modifier + .placeholderShimmer(shimmerState, shape) + .placeholder(shimmerState, isWipeOffAnimationEnabled, shape) + ) +} + +fun Modifier.placeholder( + placeholderState: PlaceholderState, + isWipeOffAnimationEnabled: Boolean, + shape: Shape? = null, + color: Color? = null +): Modifier = composed( + inspectorInfo = debugInspectorInfo { + name = "placeholder" + properties["placeholderState"] = placeholderState + properties["shape"] = shape + properties["color"] = color + } +) { + PlaceholderModifier( + placeholderState = placeholderState, + isWipeOffAnimationEnabled = isWipeOffAnimationEnabled, + color = color ?: MaterialTheme.colors.onSurface.copy(alpha = 0.1f) + .compositeOver(MaterialTheme.colors.surface), + shape = shape ?: MaterialTheme.shapes.small + ) +} + +fun Modifier.placeholderShimmer( + placeholderState: PlaceholderState, + shape: Shape? = null, + color: Color? = null +): Modifier = composed( + inspectorInfo = debugInspectorInfo { + name = "placeholderShimmer" + properties["placeholderState"] = placeholderState + properties["shape"] = shape + properties["color"] = color + } +) { + PlaceholderShimmerModifier( + placeholderState = placeholderState, + color = color ?: MaterialTheme.colors.onSurface, + shape = shape ?: MaterialTheme.shapes.small + ) +} + +@Immutable +@JvmInline +internal value class PlaceholderStage internal constructor(internal val type: Int) { + + companion object { + val ShowPlaceholder = PlaceholderStage(0) + val WipeOff = PlaceholderStage(1) + val ShowContent = PlaceholderStage(2) + } + + override fun toString(): String { + return when (this) { + ShowPlaceholder -> "PlaceholderStage.ShowPlaceholder" + WipeOff -> "PlaceholderStage.WipeOff" + else -> "PlaceholderStage.ShowContent" + } + } +} + +private fun wipeOffBrush( + color: Color, + offset: Offset, + placeholderState: PlaceholderState +): Brush { + return Brush.linearGradient( + colors = listOf( + Color.Transparent, + color + ), + start = Offset( + x = placeholderState.placeholderProgression - offset.x, + y = placeholderState.placeholderProgression - offset.y + ), + end = Offset( + x = placeholderState.placeholderProgression - offset.x + placeholderState.gradientWidth.div( + 1.4f + ), + y = placeholderState.placeholderProgression - offset.y + placeholderState.gradientWidth.div( + 1.4f + ) + ) + ) +} + +private abstract class AbstractPlaceholderModifier( + private val alpha: Float = 1.0f, + private val shape: Shape +) : DrawModifier, OnGloballyPositionedModifier { + + private var offset by mutableStateOf(Offset.Zero) + + private var lastSize: Size? = null + private var lastLayoutDirection: LayoutDirection? = null + private var lastOutline: Outline? = null + + override fun onGloballyPositioned(coordinates: LayoutCoordinates) { + offset = coordinates.positionInRoot() + } + + abstract fun generateBrush(offset: Offset): Brush? + + override fun ContentDrawScope.draw() { + val brush = generateBrush(offset) + + drawContent() + if (brush != null) { + if (shape === RectangleShape) { + drawRect(brush) + } else { + drawOutline(brush) + } + } + } + + private fun ContentDrawScope.drawOutline(brush: Brush) { + val outline = + if (size == lastSize && layoutDirection == lastLayoutDirection) { + lastOutline!! + } else { + shape.createOutline(size, layoutDirection, this) + } + drawOutline(outline, brush = brush, alpha = alpha) + lastOutline = outline + lastSize = size + } +} + +private class PlaceholderModifier constructor( + private val placeholderState: PlaceholderState, + private val isWipeOffAnimationEnabled: Boolean, + private val color: Color, + alpha: Float = 1.0f, + val shape: Shape +) : AbstractPlaceholderModifier(alpha, shape) { + override fun generateBrush(offset: Offset): Brush? { + return when (placeholderState.placeholderStage) { + PlaceholderStage.ShowPlaceholder -> { + SolidColor(color) + } + + PlaceholderStage.WipeOff -> { + takeIf { isWipeOffAnimationEnabled }?.let { + wipeOffBrush(color, offset, placeholderState) + } + } + + else -> { + null + } + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PlaceholderModifier + + if (placeholderState != other.placeholderState) return false + if (color != other.color) return false + if (shape != other.shape) return false + + return true + } + + override fun hashCode(): Int { + var result = placeholderState.hashCode() + result = 31 * result + color.hashCode() + result = 31 * result + shape.hashCode() + return result + } +} + +private class PlaceholderShimmerModifier constructor( + private val placeholderState: PlaceholderState, + private val color: Color, + alpha: Float = 1.0f, + val shape: Shape +) : AbstractPlaceholderModifier(alpha, shape) { + override fun generateBrush(offset: Offset): Brush? { + return if (placeholderState.placeholderStage == PlaceholderStage.ShowPlaceholder) { + Brush.linearGradient( + start = Offset( + x = placeholderState.placeholderProgression - offset.x, + y = placeholderState.placeholderProgression - offset.y + ), + end = Offset( + x = placeholderState.placeholderProgression - offset.x + + placeholderState.gradientWidth, + y = placeholderState.placeholderProgression - offset.y + + placeholderState.gradientWidth + ), + colorStops = listOf( + 0f to color.copy(alpha = 0f), + 0.65f to color.copy(alpha = 0.13f), + 1f to color.copy(alpha = 0f), + ).toTypedArray() + ) + } else { + null + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PlaceholderShimmerModifier + + if (placeholderState != other.placeholderState) return false + if (color != other.color) return false + if (shape != other.shape) return false + + return true + } + + override fun hashCode(): Int { + var result = placeholderState.hashCode() + result = 31 * result + color.hashCode() + result = 31 * result + shape.hashCode() + return result + } +} + +@Composable +internal fun screenHeightDp() = LocalContext.current.resources.configuration.screenHeightDp + +@Composable +internal fun screenWidthDp() = LocalContext.current.resources.configuration.screenWidthDp + +internal const val PLACEHOLDER_PROGRESSION_DURATION_MS = 800L +internal const val PLACEHOLDER_DELAY_BETWEEN_PROGRESSIONS_MS = 800L +internal const val PLACEHOLDER_GAP_BETWEEN_ANIMATION_LOOPS_MS = + PLACEHOLDER_PROGRESSION_DURATION_MS + PLACEHOLDER_DELAY_BETWEEN_PROGRESSIONS_MS diff --git a/sample-compose/build.gradle.kts b/sample-compose/build.gradle.kts index 0b88502f..8e0c2e30 100644 --- a/sample-compose/build.gradle.kts +++ b/sample-compose/build.gradle.kts @@ -49,4 +49,5 @@ dependencies { implementation(projects.libraries.timelineViewCompose) implementation(projects.libraries.ratingBarCompose) + implementation(projects.libraries.shimmerCompose) } diff --git a/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ComponentsScreen.kt b/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ComponentsScreen.kt index a9699bda..c920d1f3 100644 --- a/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ComponentsScreen.kt +++ b/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ComponentsScreen.kt @@ -35,6 +35,10 @@ fun ComponentsScreen( ComponentItem(title = "RatingBar") { onItemClick(Route.RatingBar) } + + ComponentItem(title = "Shimmer") { + onItemClick(Route.Shimmer) + } } } diff --git a/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/Route.kt b/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/Route.kt index 9c34bd6b..37307c30 100644 --- a/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/Route.kt +++ b/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/Route.kt @@ -7,4 +7,6 @@ sealed class Route(val destination: String) { object TimelineView : Route("timelineView") object RatingBar : Route("ratingbar") + + object Shimmer : Route("shimmer") } diff --git a/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/SampleComposeNavGraph.kt b/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/SampleComposeNavGraph.kt index 5f7ddee8..165bc88c 100644 --- a/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/SampleComposeNavGraph.kt +++ b/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/SampleComposeNavGraph.kt @@ -32,5 +32,9 @@ fun SampleComposeNavGraph( composable(Route.RatingBar.destination) { RatingBarScreen() } + + composable(Route.Shimmer.destination) { + ShimmerScreen() + } } } \ No newline at end of file diff --git a/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ShimmerScreen.kt b/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ShimmerScreen.kt new file mode 100644 index 00000000..32d5e920 --- /dev/null +++ b/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ShimmerScreen.kt @@ -0,0 +1,126 @@ +package com.trendyol.uicomponents.samplecompose + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel +import coil.compose.AsyncImage +import com.trendyol.uicomponents.samplecompose.ui.theme.ColorPrimary +import com.trendyol.uicomponents.shimmercompose.shimmer +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +fun ShimmerScreen(viewModel: ShimmerViewModel = viewModel()) = with(viewModel) { + Scaffold( + topBar = { + TopAppBar( + backgroundColor = ColorPrimary, + elevation = 0.dp + ) { + Text( + modifier = Modifier.padding(start = 16.dp), + text = "Shimmer", + style = MaterialTheme.typography.h6.copy(color = Color.White) + ) + } + } + ) { paddingValues -> + LazyVerticalGrid( + modifier = Modifier.padding(paddingValues), + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(16.dp) + ) { + items(20) { + ListItem(uiState.headerItem, uiState.isLoading) + } + } + } +} + +@Composable +fun ListItem(item: Item, isLoading: Boolean) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + AsyncImage( + model = item.imageUrl, + contentDescription = null, + modifier = Modifier + .aspectRatio(1f) + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .shimmer(isLoading) + ) + + Text( + text = item.title, + modifier = Modifier + .fillMaxWidth() + .shimmer(isLoading), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +@Stable +interface ShimmerUiState { + val headerItem: Item + val isLoading: Boolean +} + +@Stable +data class Item( + val title: String, + val imageUrl: String +) + +class MutableShimmerUiState : ShimmerUiState { + override var headerItem: Item by mutableStateOf(Item("", "")) + override var isLoading: Boolean by mutableStateOf(true) +} + +class ShimmerViewModel : ViewModel() { + private val _uiState = MutableShimmerUiState() + val uiState: ShimmerUiState = _uiState + + init { + fetchItem() + } + + private fun fetchItem() = viewModelScope.launch { + delay(1_500) + _uiState.isLoading = false + _uiState.headerItem = Item( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit", + "https://via.placeholder.com/600/92c952" + ) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 2c1c2eef..c273793b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,6 +13,7 @@ include(":libraries:touch-delegator") include(":libraries:fit-option-message-view") include(":libraries:timeline-view-compose") include(":libraries:rating-bar-compose") +include(":libraries:shimmer-compose") rootProject.name = "trendyol-android-ui-components" From 1e50eb5672c025ca45abd1db7a3f0d0de313a919 Mon Sep 17 00:00:00 2001 From: "yasin.cidem" Date: Fri, 21 Jul 2023 15:21:37 +0300 Subject: [PATCH 2/3] Fix typo --- .../trendyol/uicomponents/samplecompose/ShimmerScreen.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ShimmerScreen.kt b/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ShimmerScreen.kt index 32d5e920..5037f571 100644 --- a/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ShimmerScreen.kt +++ b/sample-compose/src/main/java/com/trendyol/uicomponents/samplecompose/ShimmerScreen.kt @@ -57,7 +57,7 @@ fun ShimmerScreen(viewModel: ShimmerViewModel = viewModel()) = with(viewModel) { contentPadding = PaddingValues(16.dp) ) { items(20) { - ListItem(uiState.headerItem, uiState.isLoading) + ListItem(uiState.item, uiState.isLoading) } } } @@ -92,7 +92,7 @@ fun ListItem(item: Item, isLoading: Boolean) { @Stable interface ShimmerUiState { - val headerItem: Item + val item: Item val isLoading: Boolean } @@ -103,7 +103,7 @@ data class Item( ) class MutableShimmerUiState : ShimmerUiState { - override var headerItem: Item by mutableStateOf(Item("", "")) + override var item: Item by mutableStateOf(Item("", "")) override var isLoading: Boolean by mutableStateOf(true) } @@ -118,7 +118,7 @@ class ShimmerViewModel : ViewModel() { private fun fetchItem() = viewModelScope.launch { delay(1_500) _uiState.isLoading = false - _uiState.headerItem = Item( + _uiState.item = Item( "Lorem ipsum dolor sit amet, consectetur adipiscing elit", "https://via.placeholder.com/600/92c952" ) From 0461705c3ea443067005d62064344658891d8f87 Mon Sep 17 00:00:00 2001 From: "yasin.cidem" Date: Fri, 4 Aug 2023 14:55:36 +0300 Subject: [PATCH 3/3] Fix license --- libraries/shimmer-compose/README.md | 23 +++++++++++++++---- .../uicomponents/shimmercompose/Shimmer.kt | 15 ++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/libraries/shimmer-compose/README.md b/libraries/shimmer-compose/README.md index 12a529a3..23925e96 100644 --- a/libraries/shimmer-compose/README.md +++ b/libraries/shimmer-compose/README.md @@ -55,9 +55,22 @@ dependencies { This library is maintained mainly by Trendyol Android Team members but also other Android lovers contributes. # License +-------- + Copyright 2023 Trendyol.com -``` -Copyright 2023 Trendyol.com Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -``` \ No newline at end of file + Copyright 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Placeholder.kt file in androidx project has been renamed to Shimmer.kt and modified to fit our needs + https://github.com/androidx/androidx/blob/8da8b8938ee44b72bd258b15eb623680b3fcbb29/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Placeholder.kt \ No newline at end of file diff --git a/libraries/shimmer-compose/src/main/java/com/trendyol/uicomponents/shimmercompose/Shimmer.kt b/libraries/shimmer-compose/src/main/java/com/trendyol/uicomponents/shimmercompose/Shimmer.kt index c06fa614..1983f64c 100644 --- a/libraries/shimmer-compose/src/main/java/com/trendyol/uicomponents/shimmercompose/Shimmer.kt +++ b/libraries/shimmer-compose/src/main/java/com/trendyol/uicomponents/shimmercompose/Shimmer.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.trendyol.uicomponents.shimmercompose import androidx.compose.animation.core.withInfiniteAnimationFrameMillis