-
Notifications
You must be signed in to change notification settings - Fork 12
ALTAPPS-1253: Android topic completion screen #1063
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
XanderZhu
merged 21 commits into
develop
from
feature/ALTAPPS-1253/Android-topic-completion-screen
May 28, 2024
Merged
Changes from 8 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
f404588
Implement background video play
XanderZhu 48694b6
Implement base ui
XanderZhu 7712639
Add enter animation
XanderZhu 9c80578
Make topic completed fragment a dialog fragment with animation
XanderZhu 8df5a9d
Clean up ui
XanderZhu 28be44f
Update enter animation
XanderZhu 23e8193
Run shimmer animation on the button just after text typing is finished
XanderZhu 040f4d9
Merge branch 'develop' into feature/ALTAPPS-1253/Android-topic-comple…
XanderZhu a78ba30
Fix detekt
XanderZhu bd75d5e
Refactor shimmer
XanderZhu 1cefdd2
Integrate shared module
XanderZhu eee93e1
Fix ktlint
XanderZhu c1bc392
Fix navigation to next step and study plan
XanderZhu 82e708f
Merge branch 'develop' into feature/ALTAPPS-1253/Android-topic-comple…
XanderZhu 980200b
Cleanup ui
XanderZhu 35a763e
Extract mediaPlayer functionality into a delegate
XanderZhu fd20869
Update compile SDK to 34 to use screen shoot api
XanderZhu 38e0d9d
Use screen shoot api to send a screenshot message
XanderZhu df06530
Fix enter transition plays every time the screen is show
XanderZhu cf4239f
Fix ktlint
XanderZhu 296ca1e
Merge branch 'develop' into feature/ALTAPPS-1253/Android-topic-comple…
XanderZhu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
File renamed without changes.
63 changes: 63 additions & 0 deletions
63
...p/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/ShimmerModifier.kt
This file contains hidden or 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,63 @@ | ||
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 | ||
|
||
@Stable | ||
class ShimmerState( | ||
val colors: List<Color> = listOf( | ||
Color.Transparent, | ||
Color.White.copy(alpha = 0.7f), | ||
Color.Transparent | ||
), | ||
durationMillis: Int = 1200, | ||
easing: Easing = FastOutSlowInEasing | ||
) { | ||
|
||
var targetValue: Float by mutableStateOf(-2f) | ||
private set | ||
|
||
val startOffsetXAnimationSpec: AnimationSpec<Float> = tween( | ||
durationMillis = durationMillis, | ||
easing = easing | ||
) | ||
|
||
fun runShimmerAnimation() { | ||
targetValue = 2f | ||
|
||
} | ||
} | ||
|
||
fun Modifier.shimmer(shimmerState: ShimmerState) = | ||
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) | ||
} | ||
} |
76 changes: 76 additions & 0 deletions
76
.../main/java/org/hyperskill/app/android/core/view/ui/widget/compose/TypewriterTextEffect.kt
This file contains hidden or 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,76 @@ | ||
package org.hyperskill.app.android.core.view.ui.widget.compose | ||
|
||
import android.util.Log | ||
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.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]. | ||
*/ | ||
@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)." | ||
|
||
} | ||
|
||
// 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)." | ||
|
||
} | ||
|
||
// 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) { | ||
Log.d("TypewriterTextEffect", "Start LaunchedEffect") | ||
if (startTypingDelayInMillis != null) { | ||
delay(startTypingDelayInMillis.milliseconds) | ||
} | ||
Log.d("TypewriterTextEffect", "Start typing") | ||
|
||
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)) | ||
} | ||
onEffectCompleted() | ||
} | ||
} |
127 changes: 127 additions & 0 deletions
127
...java/org/hyperskill/app/android/topic_completion/fragment/TopicCompletedDialogFragment.kt
This file contains hidden or 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,127 @@ | ||
package org.hyperskill.app.android.topic_completion.fragment | ||
|
||
import android.app.Dialog | ||
import android.media.MediaPlayer | ||
import android.os.Bundle | ||
import android.view.SurfaceHolder | ||
import android.view.View | ||
import android.view.ViewGroup | ||
import android.view.Window | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.ui.platform.ViewCompositionStrategy | ||
import androidx.fragment.app.DialogFragment | ||
import androidx.lifecycle.compose.collectAsStateWithLifecycle | ||
import by.kirich1409.viewbindingdelegate.viewBinding | ||
import kotlinx.coroutines.flow.MutableStateFlow | ||
import org.hyperskill.app.android.R | ||
import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme | ||
import org.hyperskill.app.android.databinding.FragmentTopicCompletedBinding | ||
import org.hyperskill.app.android.topic_completion.model.TopicCompletedModalViewState | ||
import org.hyperskill.app.android.topic_completion.ui.TopicCompleted | ||
|
||
class TopicCompletedDialogFragment : DialogFragment(R.layout.fragment_topic_completed), SurfaceHolder.Callback { | ||
|
||
companion object { | ||
const val TAG = "TopicCompletedFragment" | ||
|
||
fun newInstance(): TopicCompletedDialogFragment = | ||
TopicCompletedDialogFragment() | ||
} | ||
|
||
private val viewBinding: FragmentTopicCompletedBinding by viewBinding(FragmentTopicCompletedBinding::bind) | ||
|
||
private var mediaPlayer: MediaPlayer? = null | ||
|
||
private val isVideoBackgroundPlaying: MutableStateFlow<Boolean> = MutableStateFlow(false) | ||
|
||
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
setStyle(STYLE_NO_TITLE, R.style.ThemeOverlay_AppTheme_Dialog_Fullscreen) | ||
} | ||
|
||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = | ||
super.onCreateDialog(savedInstanceState).apply { | ||
setCanceledOnTouchOutside(false) | ||
setCancelable(false) | ||
requestWindowFeature(Window.FEATURE_NO_TITLE) | ||
} | ||
|
||
override fun onResume() { | ||
super.onResume() | ||
mediaPlayer?.start() | ||
} | ||
|
||
override fun onStart() { | ||
super.onStart() | ||
dialog | ||
?.window | ||
?.let { window -> | ||
window.setLayout( | ||
ViewGroup.LayoutParams.MATCH_PARENT, | ||
ViewGroup.LayoutParams.MATCH_PARENT | ||
) | ||
window.setWindowAnimations(R.style.ThemeOverlay_AppTheme_Dialog_Fullscreen) | ||
} | ||
mediaPlayer = MediaPlayer() | ||
} | ||
|
||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
super.onViewCreated(view, savedInstanceState) | ||
viewBinding.topicCompletedSurfaceView.holder.addCallback(this@TopicCompletedDialogFragment) | ||
with(viewBinding.topicCompletedComposeView) { | ||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner)) | ||
setContent { | ||
HyperskillTheme { | ||
val isPlayingState by isVideoBackgroundPlaying.collectAsStateWithLifecycle() | ||
if (isPlayingState) { | ||
TopicCompleted( | ||
viewState = TopicCompletedModalViewState(), | ||
onCloseClick = ::onCloseClick, | ||
onCTAButtonClick = ::onCTAButtonClick | ||
) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
private fun onCloseClick() { | ||
dialog?.dismiss() | ||
} | ||
|
||
private fun onCTAButtonClick() { | ||
TODO("Not implemented yet") | ||
} | ||
|
||
override fun onStop() { | ||
super.onStop() | ||
mediaPlayer = null | ||
} | ||
|
||
override fun surfaceCreated(holder: SurfaceHolder) { | ||
mediaPlayer?.apply { | ||
resources | ||
.openRawResourceFd(R.raw.topic_completion_bg_1) | ||
.use(::setDataSource) | ||
setDisplay(holder) | ||
prepareAsync() | ||
isLooping = true | ||
setOnPreparedListener { | ||
it.start() | ||
isVideoBackgroundPlaying.value = true | ||
} | ||
} | ||
} | ||
|
||
override fun surfaceDestroyed(holder: SurfaceHolder) { | ||
mediaPlayer?.apply { | ||
stop() | ||
isVideoBackgroundPlaying.value = false | ||
release() | ||
} | ||
} | ||
|
||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { | ||
// no op | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
...in/java/org/hyperskill/app/android/topic_completion/model/TopicCompletedModalViewState.kt
This file contains hidden or 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,14 @@ | ||
package org.hyperskill.app.android.topic_completion.model | ||
|
||
data class TopicCompletedModalViewState( | ||
val title: String = "{Topic name} completed!", | ||
val description: String = "Learning might be tough, but it brings you knowledge that lasts forever", | ||
val earnedGemsText: String = "+ 5", | ||
val callToActionButtonTitle: String = "Continue with next topic", | ||
val spacebotAvatarVariantIndex: Int = 0, | ||
val backgroundAnimationStyle: BackgroundAnimationStyle = BackgroundAnimationStyle.FIRST | ||
) { | ||
enum class BackgroundAnimationStyle { | ||
FIRST, SECOND | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.