Skip to content

Commit

Permalink
Add shimmer implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
tonykolomeytsev committed Oct 2, 2023
1 parent 25157b9 commit 6d4d8f3
Show file tree
Hide file tree
Showing 10 changed files with 452 additions and 9 deletions.
1 change: 1 addition & 0 deletions modules/app_ui_kit/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ android {
dependencies {
implementation(project(":ui_theme"))
implementation(project(":ui_icons"))
implementation(project(":ui_shimmer"))
implementation(project(":ui_kit_lists"))
implementation(project(":ui_kit_topappbar"))
implementation(project(":ui_kit_navigationbar"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import kekmech.ru.lib_navigation_compose.LocalBackStackNavigator
import kekmech.ru.mpeiapp.demo.screens.colors.ColorsScreenNavTarget
import kekmech.ru.mpeiapp.demo.screens.components.ComponentsScreenNavTarget
import kekmech.ru.mpeiapp.demo.screens.elmslie.ElmDemoScreenNavTarget
import kekmech.ru.mpeiapp.demo.screens.shimmers.ShimmersScreenNavTarget
import kekmech.ru.mpeiapp.demo.screens.typography.TypographyScreenNavTarget
import kekmech.ru.mpeiapp.demo.ui.SectionItem
import kekmech.ru.mpeiapp.demo.ui.ToggleThemeActionButton
Expand Down Expand Up @@ -94,6 +95,15 @@ private fun MainScreen(greetings: String) {
modifier = Modifier.fillMaxWidth()
)
}
item("shimmers") {
SectionItem(
onClick = {
navigator.navigate(ShimmersScreenNavTarget())
},
name = "Shimmers",
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package kekmech.ru.mpeiapp.demo.screens.shimmers

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.node
import kekmech.ru.lib_navigation_api.NavTarget
import kekmech.ru.mpeiapp.demo.ui.UiKitScreen
import kekmech.ru.ui_shimmer.shimmer
import kekmech.ru.ui_theme.theme.MpeixTheme
import kotlinx.parcelize.Parcelize

@Parcelize
internal class ShimmersScreenNavTarget : NavTarget {

override fun resolve(buildContext: BuildContext): Node =
node(buildContext) { ShimmersScreen() }
}

@Composable
private fun ShimmersScreen() {
UiKitScreen(title = "Shimmers") { innerPadding ->
Column(Modifier.padding(innerPadding)) {
ShimmerItem()
ShimmerItem()
ShimmerItem()
ShimmerItem()
ShimmerItem()
}
}
}

@Composable
private fun ShimmerItem() {
val color = MpeixTheme.palette.surfacePlus3
val radius = 4.dp
Row(
Modifier
.padding(bottom=8.dp)
.padding(horizontal = 16.dp)
.shimmer()
) {
Box(
Modifier
.padding(end = 8.dp)
.size(64.dp)
.background(color, RoundedCornerShape(radius))
)
Column {
Box(
Modifier
.fillMaxWidth()
.padding(bottom = 4.dp)
.height(18.dp)
.background(color, RoundedCornerShape(radius))
)
Box(
Modifier
.fillMaxWidth(fraction = 0.75f)
.height(14.dp)
.background(color, RoundedCornerShape(radius))
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ package kekmech.ru.ui_kit_dialogs
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ShoppingCart
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
Expand Down Expand Up @@ -58,6 +62,21 @@ fun rememberAlertDialogState(dialogVisible: Boolean = false): AlertDialogState =
* defines. If required, these constraints can be overwritten by providing a `width` or `height`
* [Modifier]s.
*
* Usage:
*
* ```kotlin
* val myAlert = rememberAlertDialogState()
*
* Button(
* onClick = { myAlert.showDialog() },
* ) { ... }
*
* AlertDialog(
* onDismissRequest = { myAlert.hideDialog() },
* state = myAlert,
* ) { ... }
* ```
*
* @param onDismissRequest called when the user tries to dismiss the Dialog by clicking outside
* or pressing the back button. This is not called when the dismiss button is clicked.
* @param state the state by which the showing and hiding of the dialog is controlled. See
Expand Down Expand Up @@ -109,6 +128,23 @@ fun AlertDialog(
* By default it will try to place them horizontally next to each other and fallback to horizontal
* placement if not enough space is available.
*
* Usage:
*
* ```kotlin
* val myAlert = rememberAlertDialogState()
*
* Button(
* onClick = { myAlert.showDialog() },
* ) { ... }
*
* AlertDialog(
* onDismissRequest = { myAlert.hideDialog() },
* confirmButton = { ... },
* state = myAlert,
* ...
* )
* ```
*
* @param onDismissRequest called when the user tries to dismiss the Dialog by clicking outside
* or pressing the back button. This is not called when the dismiss button is clicked.
* @param confirmButton button which is meant to confirm a proposed action, thus resolving what
Expand Down Expand Up @@ -138,6 +174,7 @@ fun AlertDialog(
icon: @Composable (() -> Unit)? = null,
title: String? = null,
text: String? = null,
content: @Composable (() -> Unit)? = null,
properties: DialogProperties = DialogProperties(),
) {
AnimatedVisibility(
Expand All @@ -162,15 +199,34 @@ fun AlertDialog(
)
}
} else null,
text = if (text != null) {
@Composable {
Text(
text = text,
style = MpeixTheme.typography.paragraphNormal,
color = MpeixTheme.palette.contentVariant,
)
text = when {
text != null && content != null -> {
{
Column {
Text(
text = text,
style = MpeixTheme.typography.paragraphNormal,
color = MpeixTheme.palette.contentVariant,
modifier = Modifier.padding(bottom = 16.dp),
)
Divider()
content.invoke()
Divider()
}
}
}
} else null,
text != null && content == null -> {
{
Text(
text = text,
style = MpeixTheme.typography.paragraphNormal,
color = MpeixTheme.palette.contentVariant,
)
}
}
text == null && content != null -> content
else -> null
},
shape = RoundedCornerShape(28.dp),
containerColor = MpeixTheme.palette.surface,
iconContentColor = MpeixTheme.palette.contentVariant,
Expand Down Expand Up @@ -228,7 +284,18 @@ private fun LayoutAlertDialogPreview() {
Icon(Icons.Outlined.ShoppingCart, contentDescription = null)
},
title = "Are you sure?",
text = "Are you really sure what you do? Just go out and touch the grass!",
text = "Are you really sure what you do? Just go out and touch the grass:",
content = {
Box(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.background(MpeixTheme.palette.surfacePlus1),
contentAlignment = Alignment.Center,
) {
Text(text = "Some grass to be touched")
}
}
)
}
}
7 changes: 7 additions & 0 deletions modules/ui/shimmer/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
plugins {
id("mpeix.android.ui")
}

dependencies {
implementation(project(":ui_theme"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package kekmech.ru.ui_shimmer


import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import kotlin.math.acos
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sqrt

/**
* Describes the area in which the shimmer effect will be drawn.
*/
internal class ShimmerArea(
private val shimmerWidthPx: Float,
internal val shimmerBounds: Rect,
) {

private val reducedRotation = ShimmerTokens.Rotation.toRadian()

private var shimmerSize: Size = Size.Zero

var translationDistance = 0f
private set

var pivotPoint = Offset.Unspecified
private set

var viewBounds = Rect.Zero
set(value) {
if (value == field) return
field = value
computeShimmerBounds()
}

private fun computeShimmerBounds() {
if (viewBounds.isEmpty) return

// Pivot point in the view's frame of reference
pivotPoint = -viewBounds.topLeft + shimmerBounds.center

val newShimmerSize = shimmerBounds.size
if (shimmerSize != newShimmerSize) {
shimmerSize = newShimmerSize
computeTranslationDistance()
}
}

/**
* Rotating the shimmer results in an effect that will first be visible in one of the corners.
* It will afterwards travel across the view / display until the last visible part of it will
* disappear in the opposite corner.
*
* A simple shimmer going across the device's screen from left to right has to travel until
* it reaches the center of the screen and then the same distance again. Without taking the
* shimmer's own width into account.
*
* If the shimmer is now tilted slightly clockwise around the center of the display, a new
* distance has to be calculated. The required distance is the length of a line, which extends
* from the top left of the display to the rotated shimmer (or center line), hitting it at a
* 90 degree angle. As the height and width of the display (or view) are known, the length of
* the line can be calculated by using basic trigonometric functions.
*/
private fun computeTranslationDistance() {
val width = shimmerSize.width / 2
val height = shimmerSize.height / 2

val distanceCornerToCenter = sqrt(width.pow(2) + height.pow(2))
val beta = acos(width / distanceCornerToCenter)
val alpha = beta - reducedRotation

val distanceCornerToRotatedCenterLine = cos(alpha) * distanceCornerToCenter
translationDistance = distanceCornerToRotatedCenterLine * 2 + shimmerWidthPx
}

private fun Float.toRadian(): Float = this / 180 * Math.PI.toFloat()

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as ShimmerArea

if (shimmerWidthPx != other.shimmerWidthPx) return false
if (reducedRotation != other.reducedRotation) return false

return true
}

override fun hashCode(): Int {
var result = shimmerWidthPx.hashCode()
result = 31 * result + reducedRotation.hashCode()
return result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package kekmech.ru.ui_shimmer


import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.platform.LocalContext

@Composable
internal fun rememberShimmerBounds(): Rect {
val displayMetrics = LocalContext.current.resources.displayMetrics
return remember(displayMetrics) {
Rect(
left = 0f,
top = 0f,
right = displayMetrics.widthPixels.toFloat(),
bottom = displayMetrics.heightPixels.toFloat()
)
}
}

Loading

0 comments on commit 6d4d8f3

Please sign in to comment.