-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Android topic completion screen (#1063)
- Loading branch information
Showing
62 changed files
with
939 additions
and
214 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/extensions/Screenshoot.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package org.hyperskill.app.android.core.extensions | ||
|
||
import android.app.Activity | ||
import android.os.Build | ||
import androidx.fragment.app.Fragment | ||
import androidx.lifecycle.Lifecycle | ||
import androidx.lifecycle.LifecycleEventObserver | ||
|
||
/** | ||
* Sets a callback to be invoked when a screenshot is captured on the screen. | ||
* Works only on Android 14 and higher. | ||
* | ||
* @param block The code block to be executed when a screenshot is captured. | ||
*/ | ||
inline fun Fragment.doOnScreenShootCaptured( | ||
crossinline block: () -> Unit | ||
) { | ||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { | ||
val callBack = Activity.ScreenCaptureCallback { | ||
block() | ||
} | ||
lifecycle.addObserver( | ||
LifecycleEventObserver { _, event -> | ||
when (event) { | ||
Lifecycle.Event.ON_START -> { | ||
requireActivity().registerScreenCaptureCallback( | ||
/* executor = */ requireActivity().mainExecutor, | ||
/* callback = */ callBack | ||
) | ||
} | ||
Lifecycle.Event.ON_STOP -> { | ||
requireActivity().unregisterScreenCaptureCallback(callBack) | ||
} | ||
else -> { | ||
// no op | ||
} | ||
} | ||
} | ||
) | ||
} | ||
} |
File renamed without changes.
73 changes: 73 additions & 0 deletions
73
...p/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/ShimmerModifier.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package org.hyperskill.app.android.core.view.ui.widget.compose | ||
|
||
import androidx.compose.animation.core.AnimationSpec | ||
import androidx.compose.animation.core.Easing | ||
import androidx.compose.animation.core.FastOutSlowInEasing | ||
import androidx.compose.animation.core.animateFloatAsState | ||
import androidx.compose.animation.core.tween | ||
import androidx.compose.runtime.Stable | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.setValue | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.composed | ||
import androidx.compose.ui.draw.drawWithContent | ||
import androidx.compose.ui.geometry.Offset | ||
import androidx.compose.ui.graphics.Brush | ||
import androidx.compose.ui.graphics.Color | ||
|
||
/** | ||
* Applies shimmer animation to the target Composable. | ||
* Animation is playing one time. | ||
* To start animation call [ShimmerState.runShimmerAnimation] on the [ShimmerState] instance. | ||
*/ | ||
fun Modifier.shimmerShot(shimmerState: ShimmerState): Modifier = | ||
composed { | ||
val startOffsetX by animateFloatAsState( | ||
targetValue = shimmerState.targetValue, | ||
animationSpec = shimmerState.startOffsetXAnimationSpec, | ||
label = "shimmer" | ||
) | ||
drawWithContent { | ||
val width = size.width | ||
val height = size.height | ||
val offset = startOffsetX * width | ||
|
||
drawContent() | ||
val brush = Brush.linearGradient( | ||
colors = shimmerState.colors, | ||
start = Offset(offset, 0f), | ||
end = Offset(offset + width, height) | ||
) | ||
drawRect(brush) | ||
} | ||
} | ||
|
||
@Stable | ||
class ShimmerState( | ||
val colors: List<Color> = listOf( | ||
Color.Transparent, | ||
Color.White.copy(alpha = 0.7f), | ||
Color.Transparent | ||
), | ||
durationMillis: Int = 1200, | ||
easing: Easing = FastOutSlowInEasing | ||
) { | ||
|
||
companion object { | ||
private const val INITIAL_VALUE = -2f | ||
private const val TARGET_VALUE = 2f | ||
} | ||
|
||
var targetValue: Float by mutableStateOf(INITIAL_VALUE) | ||
private set | ||
|
||
val startOffsetXAnimationSpec: AnimationSpec<Float> = tween( | ||
durationMillis = durationMillis, | ||
easing = easing | ||
) | ||
|
||
fun runShimmerAnimation() { | ||
targetValue = TARGET_VALUE | ||
} | ||
} |
77 changes: 77 additions & 0 deletions
77
.../main/java/org/hyperskill/app/android/core/view/ui/widget/compose/TypewriterTextEffect.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
package org.hyperskill.app.android.core.view.ui.widget.compose | ||
|
||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.LaunchedEffect | ||
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 kotlin.random.Random | ||
import kotlin.time.Duration.Companion.milliseconds | ||
import kotlinx.coroutines.delay | ||
|
||
/** | ||
* A composable function that displays a text with a typewriter-like effect, revealing characters in chunks. | ||
* | ||
* @param text The input text to be displayed with the typewriter effect. | ||
* @param minDelayInMillis The minimum delay in milliseconds between revealing character chunks, defaults to 30ms. | ||
* @param maxDelayInMillis The maximum delay in milliseconds between revealing character chunks, defaults to 80ms. | ||
* @param minCharacterChunk The minimum number of characters to reveal at once, defaults to 1. | ||
* @param maxCharacterChunk The maximum number of characters to reveal at once, defaults to 3. | ||
* @param onEffectCompleted A callback function invoked when the entire text has been revealed. | ||
* @param displayTextComposable A composable function that receives the text to display with the typewriter effect. | ||
* | ||
* @throws IllegalArgumentException if [minDelayInMillis] is greater than [maxDelayInMillis]. | ||
* @throws IllegalArgumentException if [minCharacterChunk] is greater than [maxCharacterChunk]. | ||
*/ | ||
@Suppress("MaxLineLength") | ||
@Composable | ||
fun TypewriterTextEffect( | ||
text: String, | ||
startTypingDelayInMillis: Int? = null, | ||
minDelayInMillis: Long = 30, | ||
maxDelayInMillis: Long = 80, | ||
minCharacterChunk: Int = 1, | ||
maxCharacterChunk: Int = 3, | ||
onEffectCompleted: () -> Unit = {}, | ||
displayTextComposable: @Composable (displayedText: String) -> Unit | ||
) { | ||
// Ensure minDelayInMillis is less than or equal to maxDelayInMillis | ||
require(minDelayInMillis <= maxDelayInMillis) { | ||
"TypewriterTextEffect: Invalid delay range. minDelayInMillis ($minDelayInMillis) must be less than or equal to maxDelayInMillis ($maxDelayInMillis)." //ktlint-disable | ||
} | ||
|
||
// Ensure minCharacterChunk is less than or equal to maxCharacterChunk | ||
require(minCharacterChunk <= maxCharacterChunk) { | ||
"TypewriterTextEffect: Invalid character chunk range. minCharacterChunk ($minCharacterChunk) must be less than or equal to maxCharacterChunk ($maxCharacterChunk)." //ktlint-disable | ||
} | ||
|
||
val currentOnEffectCompleted by rememberUpdatedState(newValue = onEffectCompleted) | ||
|
||
// Initialize and remember the displayedText | ||
var displayedText by remember { mutableStateOf("") } | ||
|
||
// Call the displayTextComposable with the current displayedText value | ||
displayTextComposable(displayedText) | ||
|
||
// Launch the effect to update the displayedText value over time | ||
LaunchedEffect(text) { | ||
if (startTypingDelayInMillis != null) { | ||
delay(startTypingDelayInMillis.milliseconds) | ||
} | ||
|
||
val textLength = text.length | ||
var endIndex = 0 | ||
|
||
while (endIndex < textLength) { | ||
endIndex = minOf( | ||
endIndex + Random.nextInt(minCharacterChunk, maxCharacterChunk + 1), | ||
textLength | ||
) | ||
displayedText = text.substring(startIndex = 0, endIndex = endIndex) | ||
delay(Random.nextLong(minDelayInMillis, maxDelayInMillis)) | ||
} | ||
currentOnEffectCompleted() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.