<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Patrick Elmquist on Medium]]></title>
        <description><![CDATA[Stories by Patrick Elmquist on Medium]]></description>
        <link>https://medium.com/@patrick.elmquist?source=rss-560a85bfeb34------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*v-Za0sjPqrLcyfEUYbVFxw.jpeg</url>
            <title>Stories by Patrick Elmquist on Medium</title>
            <link>https://medium.com/@patrick.elmquist?source=rss-560a85bfeb34------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 31 May 2026 17:33:49 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@patrick.elmquist/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Teaching a Composable to jump]]></title>
            <link>https://medium.com/flat-pack-tech/teaching-a-composable-to-jump-461456198af9?source=rss-560a85bfeb34------2</link>
            <guid isPermaLink="false">https://medium.com/p/461456198af9</guid>
            <category><![CDATA[animation]]></category>
            <category><![CDATA[ui]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[compose]]></category>
            <category><![CDATA[jetpack-compose]]></category>
            <dc:creator><![CDATA[Patrick Elmquist]]></dc:creator>
            <pubDate>Wed, 21 Aug 2024 13:28:00 GMT</pubDate>
            <atom:updated>2024-08-21T13:28:00.714Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Zb5vDut-AY3NIMXnVvQT4w.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/480/1*POsfsjbunRBnF7Qp9rQ27Q.gif" /><figcaption>GIF of the jumping FRAKTA bag animation in the IKEA Android app</figcaption></figure><p>Learn how to make a simple jump-on-click animation in Jetpack Compose that can be applied to any Composable. We are going to write a modifier and a state class that encapsulate the logic so applying it will be as easy as:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/480/1*lymHeqygN5PbTWNTotdThw.gif" /><figcaption>Example GIF of three emojis that jump when clicked.</figcaption></figure><pre>Text(<br>    text = &quot;🍿&quot;,<br>    fontSize = 96.sp,<br>    modifier = modifier<br>        .jumpOnClick(rememberJumpAnimationState(onClick = onClick))<br>)</pre><p>As always with Compose there are many ways to achieve the same end result, this is my current version, let’s get after it!</p><h3>The Modifier</h3><p>Not a lot of magic in here. We make the Composable clickable using the interactionSource that is provided by the state. The reason for not setting onClick in here is that we want the click to be registered when the animation is coming to an end instead of when the click happens. Why? Well if you react to the click immediately and navigate a way you will miss most of the animation 😉</p><pre>fun Modifier.jumpOnClick(<br>    state: JumpAnimationState<br>) = this then Modifier<br>    .clickable(<br>        interactionSource = state.interactionSource,<br>        indication = null,<br>        onClick = { /* this is handled in the state */ },<br>    )<br>    .graphicsLayer {<br>        transformOrigin = TransformOrigin(<br>            pivotFractionX = 0.5f,<br>            pivotFractionY = 1.0f,<br>        )<br>        scaleY = state.scale.value<br>        translationY = state.translation.value * size.height<br>    }</pre><p>In graphicsLayer { ... } we apply the scale and translation coming from the state, scale directly and translation based on the current height. The only noteworthy thing in this block is that we change transformOrigin to be the in the bottom-center of the Composable. This means that when you apply the scale, the top will be the part moving while the bottom remains in place, mimicking the Composable standing on the ground.</p><h3>Creating the state</h3><p>I’ve wrapped creating, remembering and hooking up the state in a function to make it reusable. JumpAnimationState is responsible for actually executing the animation and in this step we create the state and forward InteractionSource events to it so it can react on user interactions.</p><pre>@Composable<br>fun rememberJumpAnimationState(<br>    onClick: () -&gt; Unit,<br>    scope: CoroutineScope = rememberCoroutineScope(),<br>    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },<br>): JumpAnimationState {<br>    val state = remember(scope, interactionSource) {<br>        JumpAnimationState(<br>            scope = scope,<br>            interactionSource = interactionSource,<br>        )<br>    }<br><br>    val onClickLambda by rememberUpdatedState(onClick)<br>    LaunchedEffect(interactionSource) {<br>        interactionSource.interactions.collect { interaction -&gt;<br>            when (interaction) {<br>                is PressInteraction.Press -&gt; state.onPress()<br>                is PressInteraction.Release -&gt; state.onRelease(onClickLambda)<br>                is PressInteraction.Cancel -&gt; state.onCancel()<br>            }<br>        }<br>    }<br><br>    return state<br>}</pre><h3>Animations using coroutines</h3><p>It’s time to make the animations but first a quick refresher on how to define animations with coroutines:</p><ul><li>Animations <em>invoked</em> in the same CoroutineScope run <em>in sequence</em></li></ul><pre>coroutineScope { <br>    // these run in sequence<br>    scale.animateTo(...)<br>    translation.animateTo(...)<br>}</pre><ul><li>Animations <em>launched</em> in the same CoroutineScope run <em>in parallel</em></li></ul><pre>coroutineScope {<br>    // these run in parallel<br>    launch { scale.animateTo(...) }<br>    launch { translation.animateTo(...) }<br>}</pre><ul><li>The above can be mixed and matched to orchestrate more complex animations</li></ul><pre>coroutineScope {<br>    // this run first and when done...<br>    scale.animateTo(...)<br><br>    // ...both outer launch blocks run in parallel<br>    launch {<br>        // these run in sequence<br>        scale.animateTo(...)<br>        scale.animateTo(...)<br>    }<br>    launch {<br>       // these run in parallel<br>       launch { translation.animateTo(...) }<br>       launch { translation.animateTo(...) }<br>    }<br>}</pre><p>Remember that a CoroutineScope and launch block is not left until all of it’s child jobs have completed, which means that when running animations in parallel the longest one will dictate how long it takes for that block to finish.</p><h3>Performing a jump</h3><p>Here is the full code for the state and the animations. Below the code block there is a more detailed explanation of each function.</p><pre>@Stable<br>class JumpAnimationState(<br>    val interactionSource: MutableInteractionSource,<br>    val scope: CoroutineScope,<br>) {<br>    private var animation: Job? = null<br><br>    val scale = Animatable(initialValue = 1f)<br>    val translation = Animatable(initialValue = 0f)<br><br>    fun onPress() = launchAnimation {<br>        scale.snapTo(defaultScale)<br>        translation.snapTo(defaultTranslation)<br><br>        scale.animateTo(pressedScale, defaultSpring)<br>    }<br><br>    fun onRelease(invokeOnCompletion: () -&gt; Unit) = launchAnimation {<br>        // ensure it&#39;s fully compressed if the user just quickly tapped<br>        scale.animateTo(pressedScale, defaultSpring)<br><br>        launch {<br>            // restore the scale<br>            scale.animateTo(defaultScale, releaseScaleSpring)<br>        }<br><br>        launch {<br>            // up like a sun...<br>            translation.animateTo(launchTranslation, launchTranslationSpring)<br><br>            // ...down like a pancake<br>            var isSquishing = false<br>            translation.animateTo(defaultTranslation, returnTranslationSpring) {<br>                val hitTheGround = value &gt;= defaultTranslation<br>                if (hitTheGround &amp;&amp; !isSquishing) {<br>                    isSquishing = true<br>                    invokeOnCompletion()<br>                    // Add a small squish on impact<br>                    launch {<br>                        scale.animateTo(squishScale, defaultSpring)<br>                        scale.animateTo(defaultScale, defaultSpring)<br>                    }<br>                }<br>            }<br>        }<br>    }<br><br>    fun onCancel() = launchAnimation {<br>        scale.snapTo(defaultScale)<br>        translation.snapTo(defaultTranslation)<br>    }<br><br>    private fun launchAnimation(block: suspend CoroutineScope.() -&gt; Unit) {<br>        animation?.cancel()<br>        animation = scope.launch(block = block)<br>    }<br>}<br><br>private const val defaultScale = 1f<br>private const val pressedScale = 0.6f<br>private const val squishScale = 0.88f<br>private const val defaultTranslation = 0f<br>private const val launchTranslation = -0.8f<br>private val defaultSpring = spring&lt;Float&gt;()<br>private val releaseScaleSpring = spring&lt;Float&gt;(<br>    stiffness = Spring.StiffnessMedium,<br>    dampingRatio = 0.55f<br>)<br>private val launchTranslationSpring = spring&lt;Float&gt;(<br>    dampingRatio = Spring.DampingRatioNoBouncy,<br>    stiffness = Spring.StiffnessMediumLow,<br>)<br>private val returnTranslationSpring = spring&lt;Float&gt;(<br>    dampingRatio = 0.65f,<br>    stiffness = 140f,<br>)</pre><h4><strong>launchAnimation</strong></h4><p>All the animations are running on the CoroutineScope that is provided to the state. If we get a new event while one animation is already running (like with fast taps) then we want to cancel the current animation and process the new event.</p><h4><strong>onPress</strong></h4><p>This function has two responsibilities, reset scale and translation to their default values and then animate the scale to the pressed state.</p><h4><strong>onRelease</strong></h4><p>First makes sure that the scale has actually reached its pressed state, for a quick tap we still want it to be fully pressed before launch. Second we launch two animations in parallel, one responsible for restoring the scale to its default value and one that update the translation to go up in the air and fall down. On the way down we add a callback that is updated on each animation frame to check when the translation is back on the ground and add a small squish at the end. Why not just run the squish <em>after</em> the translation? As the translation is based on a spring it actually overshoots and bounce a tiny bit at the bottom and we want to perform the squish the first time we hit the ground, not when the spring has come to a rest.</p><h4><strong>onCancel</strong></h4><p>The interaction was for some reason cancelled, restore default scale and translation.</p><h4><strong>Constants and Springs</strong></h4><p>These values must be backed up by all kinds of maths and science right? Sure, the science of “trial and error until it looks good”. Feel free to tweak these to fit your design and use case.</p><h3>Wrapping up</h3><p>All that remains now is to draw up some cute emojis, apply the modifier and tap away:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/480/1*lymHeqygN5PbTWNTotdThw.gif" /><figcaption>Example GIF of three emojis that jump when clicked.</figcaption></figure><pre>Text(<br>    text = &quot;🍿&quot;,<br>    fontSize = 96.sp,<br>    modifier = modifier<br>        .jumpOnClick(rememberJumpAnimationState(onClick = onClick))<br>)</pre><p>That’s all there is to it, the code for this project can be found on Github:</p><p><a href="https://github.com/patrick-elmquist/Demo-ComposeJumpAnimation">GitHub - patrick-elmquist/Demo-ComposeJumpAnimation: Code for a future blog post</a></p><p>Have a good one 👋</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=461456198af9" width="1" height="1" alt=""><hr><p><a href="https://medium.com/flat-pack-tech/teaching-a-composable-to-jump-461456198af9">Teaching a Composable to jump</a> was originally published in <a href="https://medium.com/flat-pack-tech">Flat Pack Tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Make a simple “Slide to unlock” in Jetpack Compose]]></title>
            <link>https://medium.com/flat-pack-tech/make-a-simple-slide-to-unlock-in-jetpack-compose-c0161acfbbda?source=rss-560a85bfeb34------2</link>
            <guid isPermaLink="false">https://medium.com/p/c0161acfbbda</guid>
            <category><![CDATA[android]]></category>
            <category><![CDATA[ux]]></category>
            <category><![CDATA[ui]]></category>
            <category><![CDATA[animation]]></category>
            <category><![CDATA[jetpack-compose]]></category>
            <dc:creator><![CDATA[Patrick Elmquist]]></dc:creator>
            <pubDate>Wed, 27 Sep 2023 06:40:53 GMT</pubDate>
            <atom:updated>2023-09-27T13:29:55.739Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eqPvRFSVcaCa3UUMqjKHoA.png" /></figure><p>If you want to learn how to create your own custom “Slide to Unlock” slider in Jetpack Compose, then this is the right place to be. We will cover a good way to make a slider that can easily be customised and reused. In the IKEA app we use a component like this when the user is claiming a Family Reward, functioning both as a confirmation to claim the reward and a loading state when the network request is being made:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/320/1*1MX-dMnkJZ_tTc0y2p7kow.gif" /><figcaption>Example of how the “Slide To Unlock” component is used in the Family Rewards feature in the IKEA app.</figcaption></figure><h4>For the impatient developer 👨‍💻</h4><p>Now if you’re the kind of developer (like myself) that just want to see a gif of the end result and go straight for the final code, here’s a link to the demo project over on Github</p><p><a href="https://github.com/patrick-elmquist/Demo-SlideToUnlock">https://github.com/patrick-elmquist/Demo-SlideToUnlock</a></p><h3>Let’s break it down</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*daB1cIxxdkFv16xmzykR0A.gif" /><figcaption>Animation of the final component</figcaption></figure><p>The component we’re building consists of three parts</p><ul><li><strong>The track</strong>, the container and background<br>– <em>Transitions between two colors as the thumb moves</em></li><li><strong>The thumb</strong>, the button the user slide<br>– <em>Has an idle and loading state and snaps to either side of the track</em></li><li><strong>A hint</strong>, the text shown within the track<br>– <em>Fades out as the thumb moves</em></li></ul><p>Lets start of with something small, like a…</p><h3>Thumb 👍</h3><p>The thumb itself is a rather simple component. Based on a parameter representing loading, it can be in one of two states: Normal or Loading.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*04LCEn6lV8xec1OZyDL2aQ.gif" /><figcaption>The two different states the thumb can be in</figcaption></figure><p>Apart from that we only have to expose a Modifier to allow the parent to position it.</p><pre>@Composable<br>fun Thumb(<br>    isLoading: Boolean,<br>    modifier: Modifier = Modifier,<br>) {<br>    Box(<br>        modifier = modifier<br>            .size(Thumb.Size)<br>            .background(color = Color.White, shape = CircleShape)<br>            .padding(8.dp),<br>    ) {<br>        if (isLoading) {<br>            CircularProgressIndicator(<br>                modifier = Modifier.padding(2.dp),<br>                color = Color.Black,<br>                strokeWidth = 2.dp<br>            )<br>        } else {<br>            Image(<br>                painter = painterResource(R.drawable.arrow_right),<br>                contentDescription = null,<br>            )<br>        }<br>    }<br>}</pre><p>With that out of the way let’s add something for the thumb to move along.</p><h3>Track 🛤️</h3><p>The track functions as a container for the thumb and hint and is responsible for recognising the sliding gesture. As the thumb moves along the track the background color is interpolated from black to yellow.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0OCSttcKDsQvZDPCqGP03g.png" /><figcaption>The two different states the track can be in</figcaption></figure><pre>enum class Anchor { Start, End }<br><br>@Composable<br>fun Track(<br>    swipeState: SwipeableState&lt;Anchor&gt;,<br>    swipeFraction: Float,<br>    enabled: Boolean,<br>    modifier: Modifier = Modifier,<br>    content: @Composable (BoxScope.() -&gt; Unit),<br>) {<br>    val density = LocalDensity.current<br>    var fullWidth by remember { mutableStateOf(0) }<br><br>    val startOfTrackPx = 0f<br>    val endOfTrackPx = remember(fullWidth) {<br>        with(density) { <br>            fullWidth - (2 * Track.HorizontalPadding + Thumb.Size).toPx()<br>        }<br>    }<br><br>    val snapThreshold = 0.8f<br>    val thresholds = { from: Anchor, _: Anchor -&gt;<br>        if (from == Anchor.Start) {<br>            FractionalThreshold(snapThreshold)<br>        } else {<br>            FractionalThreshold(1f - snapThreshold)<br>        }<br>    }<br><br>    val backgroundColor by remember(swipeFraction) {<br>        derivedStateOf { calculateTrackColor(swipeFraction) }<br>    }<br><br>    Box(<br>        modifier = modifier<br>            .onSizeChanged { fullWidth = it.width }<br>            .height(56.dp)<br>            .fillMaxWidth()<br>            .swipeable(<br>                enabled = enabled,<br>                state = swipeState,<br>                orientation = Orientation.Horizontal,<br>                anchors = mapOf(<br>                    startOfTrackPx to Anchor.Start,<br>                    endOfTrackPx to Anchor.End,<br>                ),<br>                thresholds = thresholds,<br>                velocityThreshold = Track.VelocityThreshold,<br>            )<br>            .background(<br>                color = backgroundColor,<br>                shape = RoundedCornerShape(percent = 50),<br>            )<br>            .padding(<br>                PaddingValues(<br>                    horizontal = Track.HorizontalPadding,<br>                    vertical = 8.dp,<br>                )<br>            ),<br>        content = content,<br>    )<br>}</pre><h4>Background color</h4><p>The color is calculated using linear interpolation based on the swipeFraction, a float in the range 0f-1f representing how far the thumb have travelled along the track.</p><pre>val AlmostBlack = Color(0xFF111111)<br>val Yellow = Color(0xFFFFDB00)<br><br>fun calculateTrackColor(swipeFraction: Float): Color {<br>    val endOfColorChangeFraction = 0.4f<br>    val fraction = (swipeFraction / endOfColorChangeFraction).coerceIn(0f..1f)<br>    return lerp(AlmostBlack, Yellow, fraction)<br>}</pre><p>In short this is a standard linear interpolation with the slight change that we want our interpolation to reach it’s end value faster. In this case we want the color to be fully active (yellow) when the thumb has reached 40% of the track, which is why we use endOfColorChangeFraction = 0.4f to adjust the input before running it through the color interpolation.</p><h4>Snapping</h4><p>We’ll be using the .swipable() modifier to have the component snap to the start or the end of the track. To get it up and running there are a couple of things that need to be defined:</p><p><strong>Anchors<br></strong>You need to define a kind of object to represent the snapping points together with pixel values describing what point of the track corresponds to which snapping point. In this example we define an enum class to represent the start and end anchors and then we use the full width of the track to calculate endOfTrackPx , accounting for any space used up by the thumb or padding.</p><pre>enum class Anchor { Start, End }<br><br>val startOfTrackPx = 0f<br>val endOfTrackPx = remember(fullWidth) {<br>    with(density) { fullWidth - (2 * Track.HorizontalPadding + Thumb.Size).toPx() }<br>}<br><br>val anchors = mapOf(<br>    startOfTrackPx to Anchor.Start,<br>    endOfTrackPx to Anchor.End,<br>),</pre><p><strong>Thresholds<br></strong>How much of the track must the user swipe before letting go for the component to snap in either of the states. This can just be an arbitrary value in the range[0,1] but it’s worth mentioning that it’s applied based on the point you are moving from. So in this example we want 0.8f to be the threshold when going from left to right and 0.2f when going from right to left</p><pre>val snapThreshold = 0.8f<br>val thresholds = { from: Anchor, _: Anchor -&gt;<br>    if (from == Anchor.Start) {<br>        FractionalThreshold(snapThreshold)<br>    } else {<br>        FractionalThreshold(1f - snapThreshold)<br>    }<br>}</pre><p><strong>Velocity threshold<br></strong>Is a value that determines how easy or hard it should be to trigger the component with a small but quick fling. For this you can use a default value provided by SwipeableDefaults.VelocityThreshold , personally I’ve found that to be a bit to sensitive and have opted to take that value x10 but your milage may vary.</p><h3>Hint 💬</h3><p>The hint is just a Text with a function call to turn the swipeFraction into a faded text color.</p><pre>@Composable<br>fun Hint(<br>    text: String,<br>    swipeFraction: Float,<br>    modifier: Modifier = Modifier,<br>) {<br>    val hintTextColor by remember(swipeFraction) {<br>        derivedStateOf { calculateHintTextColor(swipeFraction) }<br>    }<br><br>    Text(<br>        text = text,<br>        color = hintTextColor,<br>        style = MaterialTheme.typography.titleSmall,<br>        modifier = modifier<br>    )<br>}<br><br>fun calculateHintTextColor(swipeFraction: Float): Color {<br>    val endOfFadeFraction = 0.35f<br>    val fraction = (swipeFraction / endOfFadeFraction).coerceIn(0f..1f)<br>    return lerp(Color.White, Color.White.copy(alpha = 0f), fraction)<br>}</pre><p>Why fade using text color and not Modifier.alpha()? Both work fine. I tend to change alpha with color instead of a Component/View out of old habit as it historically have been better for performance and it should still apply in Compose.</p><p>Just like with the track color we linearly interpolate the color and make it fully transparent when the thumb has reached 35% of the track.</p><h3>Assembling the parts</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Rw542XPoQGmIsgiFn0eONA.png" /></figure><pre>@Composable<br>fun SlideToUnlock(<br>    isLoading: Boolean,<br>    onUnlockRequested: () -&gt; Unit,<br>    modifier: Modifier = Modifier,<br>) {<br>    val hapticFeedback = LocalHapticFeedback.current<br>    val swipeState = rememberSwipeableState(<br>        initialValue = if (isLoading) Anchor.End else Anchor.Start,<br>        confirmStateChange = { anchor -&gt;<br>            if (anchor == Anchor.End) {<br>                hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)<br>                onUnlockRequested()<br>            }<br>            true<br>        }<br>    )<br><br>    val swipeFraction by remember {<br>        derivedStateOf { calculateSwipeFraction(swipeState.progress) }<br>    }<br><br>    LaunchedEffect(isLoading) {<br>        swipeState.animateTo(if (isLoading) Anchor.End else Anchor.Start)<br>    }<br><br>    Track(<br>        swipeState = swipeState,<br>        swipeFraction = swipeFraction,<br>        enabled = !isLoading,<br>        modifier = modifier,<br>    ) {<br>        Hint(<br>            text = &quot;Swipe to unlock reward&quot;,<br>            swipeFraction = swipeFraction,<br>            modifier = Modifier<br>                .align(Alignment.Center)<br>                .padding(PaddingValues(horizontal = Thumb.Size + 8.dp)),<br>        )<br><br>        Thumb(<br>            isLoading = isLoading,<br>            modifier = Modifier.offset {<br>                IntOffset(swipeState.offset.value.roundToInt(), 0)<br>            },<br>        )<br>    }<br>}</pre><p><strong>Swipe fraction <br></strong>For the swipeFraction one already exists in SwipeProgress#fraction, however it’s always relative to the anchor the interaction started from.</p><pre>// The thumb is moving...<br>Anchor                     Anchor<br>  A ------------ (Thumb) --- B<br><br>// if it started from A<br>fraction = 0.75f<br><br>// if it started from B<br>fraction = -0.25f</pre><p>But we want a value between [0,1] that’s always relative to the start, so we need a function that check the origin of the interaction and, if necessary, invert the fraction. This means that in the example above the value will always be 0.75f</p><pre>fun calculateSwipeFraction(progress: SwipeProgress&lt;Anchor&gt;): Float {<br>    val atAnchor = progress.from == progress.to<br>    val fromStart = progress.from == Anchor.Start<br>    return if (atAnchor) {<br>        if (fromStart) 0f else 1f<br>    } else {<br>        if (fromStart) progress.fraction else 1f - progress.fraction<br>    }<br>}</pre><p><strong>Haptic feedback<br></strong>You can add a small vibration when the user successfully triggered the slider by providing a lambda to confirmStateChange and check for the end state.</p><pre>val hapticFeedback = LocalHapticFeedback.current<br>val swipeState = rememberSwipeableState(<br>    initialValue = if (isLoading) Anchor.End else Anchor.Start,<br>    confirmStateChange = { anchor -&gt;<br>        if (anchor == Anchor.End) {<br>            hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)<br>            onUnlockRequested()<br>        }<br>        true<br>    }<br>)</pre><p>With that in place we have everything in place to use the component. If you followed along this far you should now have something looking like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*daB1cIxxdkFv16xmzykR0A.gif" /><figcaption>Animation of the final product</figcaption></figure><h3>Wrap up</h3><p>That’s all there is to it. Again the demo code can be found on Github:</p><p><a href="https://github.com/patrick-elmquist/Demo-SlideToUnlock">https://github.com/patrick-elmquist/Demo-SlideToUnlock</a></p><p>If you enjoy this kind of post like once a year or so, considering following me here and/or on Github. Thanks for reading and have a good one! 👋</p><h3>Behind the scenes</h3><p>As I was about to wrap up this blog post I noticed that a bunch of the APIs, in true Compose fashion, are being deprecated in alpha versions, where the <em>.swipable(...)</em> modifier and related classes are being replaced by <em>.anchoredDraggable(...)</em>. As I wanted the article to be as up to date as possible I rewrote the examples with the new APIs, however when doing so I ran into a number of issues that I suspect is due to bugs in the alpha version. So this is why the article is covering APIs that are about to be phased out. Either way, the new classes may have different names but they are used in a very similar fashion so it should still be useful :)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c0161acfbbda" width="1" height="1" alt=""><hr><p><a href="https://medium.com/flat-pack-tech/make-a-simple-slide-to-unlock-in-jetpack-compose-c0161acfbbda">Make a simple “Slide to unlock” in Jetpack Compose</a> was originally published in <a href="https://medium.com/flat-pack-tech">Flat Pack Tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Quickly scroll to the top of a RecyclerView]]></title>
            <link>https://medium.com/flat-pack-tech/quickly-scroll-to-the-top-of-a-recyclerview-da15b717f3c4?source=rss-560a85bfeb34------2</link>
            <guid isPermaLink="false">https://medium.com/p/da15b717f3c4</guid>
            <category><![CDATA[smooth-scrolling]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[recyclerview]]></category>
            <category><![CDATA[scroll-to-top]]></category>
            <dc:creator><![CDATA[Patrick Elmquist]]></dc:creator>
            <pubDate>Thu, 03 Nov 2022 13:45:54 GMT</pubDate>
            <atom:updated>2022-11-03T13:45:54.337Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xjULvR26tpuy1D0iFGQ2vw.png" /></figure><h3>Quickly scroll to the top of a list</h3><h4>Learn how to smooth scroll to the top of a RecyclerView in constant time no matter the number of items.</h4><h4>What’s the issue?</h4><p>In the IKEA app we have product lists that tend to be rather long, some categories contain 1000+ items. When a filter is applied to a category we want to first scroll to the top of the list before applying the filter as the content will change. If the user has only scrolled down a couple of products this is not a problem, we can just use the built-in smooth scroll of the RecyclerView. However if the user has scrolled down say 100+ items, the scroll duration will quickly start to turn into a bad user experience. Just have a look at the example below where the user is scrolling past 125 items:</p><figure><img alt="Screen recording of scrolling 125 items using regular smooth scroll." src="https://cdn-images-1.medium.com/max/320/1*J9HboC8PHXpSEVGLdnydwA.gif" /><figcaption>Default smooth scroll of 125 items. Duration 5.08s.</figcaption></figure><p>Using the default smooth scroll it take a little over 5 seconds to scroll past 125 items. As it’s using a LinearSmoothScroller under the hood the time it takes will grow linearly like:</p><pre>// list.smoothScrollToPosition(0)</pre><pre>#Items    Smooth scroll<br>125       5s<br>250       10s<br>500       20s</pre><p>As the distance becomes longer the user experience quickly degrades as it takes longer and longer to reach the top. Already 5 seconds is stretching what the user should have to wait for such a trivial action, 10 and 20 seconds is just horrible. From here there are two obvious paths for solving the time issue:</p><ul><li>Use list.scrollToPosition(0) which results in an instant scroll to the top without any animation. This solves the time issue but is not a smooth experience for the user.</li><li>Create a subclass of LinearSmoothScroller that modify the scroll speed to meet a certain max time. For a medium size list of items this works pretty well, but for a long list the scroll speed will be so fast that it will start looking like the scroll is lagging and make the UI look janky. This is a bit hard to capture in a low quality gif but try cranking up the speed for a long list and it will be easy to spot.</li></ul><h4>So what to do?</h4><p>Instead of adjusting the speed or smoothness to scroll past all items, let’s just scroll past a number of items that we have time for. Let’s define a threshold N that’s the maximum number of items we are willing to scroll past, then instantly scroll the list to N and smooth scroll from there. Something like this:</p><pre>N = max items to scroll past</pre><pre>if number of items to scroll is &lt; N<br>    use regular smooth scroll<br>else <br>    make a jump to item N from the top<br>    use regular smooth scroll from N</pre><p>So for a long list, this is what it would look like in practice:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/692/1*To1PoQCIMrdrccssnF3W-Q.png" /></figure><p>The jump to N is done in a single frame and then the regular smooth scroll is started from N to the top. Basically the solution is to cheat, if the desired number of items can’t be scrolled within the desired time window, just ignore a part of the list and scroll whatever amount there’s time for. This is what it looks like in practice:</p><figure><img alt="Screen recording of scrolling 125 items using quick smooth scroll." src="https://cdn-images-1.medium.com/max/320/1*Vvt8VrG3PJs8xtvXcFFxOQ.gif" /><figcaption>Quick smooth scroll of 125 items. Duration 1.1s.</figcaption></figure><p>The time it takes to scroll the list of 125 items went down from 5 to 1.1 seconds. What is important to note is that it will remain at 1.1s even if the number of items is doubled or quadrupled as it will always just scroll the top N items of the list.</p><pre>// list.smoothScrollToPosition(0)<br>//   vs<br>// list.quickScrollToTop()</pre><pre>#Items    Smooth scroll    Quick scroll<br>125       5s               1.1s<br>250       10s              1.1s<br>500       20s              1.1s</pre><p>But hey now, hold on! That jump, won’t it be visible to the user? When looking real close at the screen recording it’s possible to spot the content change right as the scroll start, however it’s only noticeable when actively looking for it. The key for this to work well is that the colours of the items and the background look similar throughout the list, so the jump will be passed off as the scroll just starting and the previously visible items have just been scrolled away.</p><h4>Enough talk, where’s the code?</h4><p>With the end result demoed, time for some code. To be re-usable in multiple places it’s added as an extension function to RecyclerView and looks like this:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/9cd8181a921ea8606a6109c812252628/href">https://medium.com/media/9cd8181a921ea8606a6109c812252628/href</a></iframe><p>What’s happening here?</p><ol><li>First, this is based on using a LinearLayoutManager or one of its subclasses likeGridLayoutManager, so we check for the proper type of layout manager.</li><li>We create the SmoothScroller to use. It’s hardcoded to the index 0 for the top of the list and applies a speedFactor that allows the caller to control the speed of the scroll. This is not strictly needed but is a nice way to be able to fine tune the visual experience for different scenarios.</li><li>We check if the list is scrolled down more than the provided threshold of items. If so we perform a jump to the threshold location and wait a frame for everything to be laid out and measured before continuing.</li><li>Start the smooth scroll with the custom scroller.</li></ol><p>That’s all that’s needed for it to work.</p><h4>LinearLayoutManager vs GridLayoutManager</h4><p>Worth noting is that jumpThreshold will assume that the list is a single column as no special handling for grids have been added to keep it simple, so for a grid just pass in:</p><pre>jumpThreshold = rowsToScroll x columnsPerRow</pre><h4>Why suspend function?</h4><p>Now what’s the reason for using a suspend function? It’s purely based on preference. For the implementation there’s nothing stopping replacing awaitFrame() with doOnPreDraw/doOnNextLayout callbacks and have a regular function. However using coroutines and suspend functions allows for simple and declarative usage like:</p><pre>viewLifecycleOwner.lifecycleScope.launch {<br>    // scroll to top<br>    list.quickScrollToTop()</pre><pre>    // wait for scroll to end<br>    awaitScrollEnd()</pre><pre>    // make changes to the top of the screen<br>    animateHeaderChange()<br>    applyNewFilters()<br>}</pre><h4>Wrapping up</h4><p>That’s all there is to it. Of course this can be improved further, allowing to scroll to the bottom or middle of the list or add better built-in support for grids, but that’s left as an exercise to the reader 😉 Have a good one 👋</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=da15b717f3c4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/flat-pack-tech/quickly-scroll-to-the-top-of-a-recyclerview-da15b717f3c4">Quickly scroll to the top of a RecyclerView</a> was originally published in <a href="https://medium.com/flat-pack-tech">Flat Pack Tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[More ways to add parallax to your RecyclerView]]></title>
            <link>https://medium.com/@patrick.elmquist/more-ways-to-add-parallax-to-your-lists-8eb1c0e461e0?source=rss-560a85bfeb34------2</link>
            <guid isPermaLink="false">https://medium.com/p/8eb1c0e461e0</guid>
            <category><![CDATA[android]]></category>
            <category><![CDATA[animation]]></category>
            <category><![CDATA[parallax]]></category>
            <category><![CDATA[recyclerview]]></category>
            <category><![CDATA[code]]></category>
            <dc:creator><![CDATA[Patrick Elmquist]]></dc:creator>
            <pubDate>Fri, 27 May 2022 08:08:52 GMT</pubDate>
            <atom:updated>2022-08-12T06:48:58.034Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/480/1*8Z5sYim0kt-dq2g6knb5Cw.gif" /></figure><p>A while back I wrote an introduction tutorial on how to add more depth to common lists by adding a parallax effect when scrolling. Now a couple of years later I’m adding a Part 2 too it, showing a more advanced example of what you can achieve by adding parallax to both the foreground and background of the list item.</p><h4>Series</h4><ul><li>Part 1: <a href="https://medium.com/@patrick.elmquist/add-extra-depth-to-your-list-using-parallax-eddb27b369de">Add extra depth to your list using parallax</a></li><li><strong>Part 2: More ways to add parallax to your RecyclerView</strong></li></ul><p>What got me to do this blog post was when I stumbled upon this concept by Hero on Dribbble:</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgiphy.com%2Fembed%2FZ0kKuLYQHZ4rM4TC5r%2Ftwitter%2Fiframe&amp;display_name=Giphy&amp;url=https%3A%2F%2Fmedia.giphy.com%2Fmedia%2FZ0kKuLYQHZ4rM4TC5r%2Fgiphy.gif&amp;image=https%3A%2F%2Fi.giphy.com%2Fmedia%2FZ0kKuLYQHZ4rM4TC5r%2Fgiphy.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=giphy" width="435" height="326" frameborder="0" scrolling="no"><a href="https://medium.com/media/8ce6394d6f9f5a81079a0f5e1c707cd9/href">https://medium.com/media/8ce6394d6f9f5a81079a0f5e1c707cd9/href</a></iframe><p>I really liked the card transition and granted this post is not a 1:1, pixel perfect implementation it’s clear that more than a little inspiration have been drawn from the concept, so kudos to Hero (<a href="https://dribbble.com/shots/5851468-Nat-Geo-Travel-guides-stories-and-videos">link</a>) for the design.</p><h4>Before we begin</h4><p>As this is more of a ‘Part 2’ follow up post, I’d recommend checking out the previous post for a more in depth presentation on how everything works, as this post will mainly showcase new ways to apply the same concept.</p><p><a href="https://medium.com/@patrick.elmquist/add-extra-depth-to-your-list-using-parallax-eddb27b369de">Add extra depth to your list using parallax</a></p><p>With that out of the way, lets begin!</p><h4>Enable parallax</h4><p>Most of the magic will be in the ViewHolder, so to start of let’s define a skeleton that support parallax:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/a238485d332737d265b1872244c479bb/href">https://medium.com/media/a238485d332737d265b1872244c479bb/href</a></iframe><p>We define parallaxOffset as a property, make sure it’s within the allowed range of [-1.0,1.0] and call the applyParallax() function when the value is changed. With this property defined we can now add a ScrollListener to our RecyclerView that calculates the offset for each ViewHolder and update the value:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/94e979acd495fd1410584797e8680144/href">https://medium.com/media/94e979acd495fd1410584797e8680144/href</a></iframe><h4>The card layout</h4><p>Now that the skeleton for parallax is added, it’s time to create our card. This is what the layout looks like:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/d3cc2a840cb69c5d494ba160553eba52/href">https://medium.com/media/d3cc2a840cb69c5d494ba160553eba52/href</a></iframe><h4>Create background parallax</h4><p>To simplify what can easily become a hot mess of factors, scales and translations, we’ll look at the implementations for the background and text separately. First we are gonna focus on the background parallax, this is what we’re aiming for:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/480/1*7-gVBflFz92Ct32OZFhtmw.gif" /><figcaption>Just the background parallax</figcaption></figure><p>Basically as the card is scrolled one way, the background image moves at a slower rate in the opposite direction, which creates a nice, smooth movement.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/aacc20f886aaa7c8f47784c417ad9113/href">https://medium.com/media/aacc20f886aaa7c8f47784c417ad9113/href</a></iframe><p>In bind() we scale the ImageView as we will be translating it and need some extra content, the parent CardView will take care of clipping the image to the correct size when the scale is to large. It may look a bit complicated but all we do is to make sure that the image has the same width as the parent RecyclerView.</p><p>In applyParallax() we separate the parallaxOffset into a direction and an absolute distance. Those values are then used to calculate the alpha and translation. We use a FloatEvaluator to calculate the linear interpolation between alpha values.</p><p>When it comes to the constants there’s really no magic, just change the values to something between 0-1 that you feel looks good.</p><h4>Add text parallax</h4><p>Now let’s move over to the slightly more complicated text parallax. This is the result we’re aiming for:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/480/1*A541T9DEEFLnPrkb_gdzcw.gif" /><figcaption>Just the text parallax</figcaption></figure><p>To showcase what’s possible we’re applying a slightly different movement depending on if the list is scrolling left or right. Anyway here’s the code (the background code have been omitted by … for this):</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/6981d727a2486da1aa126273c3e74d28/href">https://medium.com/media/6981d727a2486da1aa126273c3e74d28/href</a></iframe><p>We now use a new set of constants and a LinearOutSlowInInterpolator to calculate translations for each text field.</p><p>Together with the background parallax you should now have something looking like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/480/1*8Z5sYim0kt-dq2g6knb5Cw.gif" /><figcaption>Result with background and text parallax</figcaption></figure><p>Link to the complete ViewHolder implementation can be found here: <a href="https://github.com/patrick-elmquist/Demo-RecyclerViewParallaxPart2/blob/master/app/src/main/java/com/patrickelm/parallaxdemo/CardViewHolder.kt">CardViewHolder.kt</a></p><h4>Wrapping up</h4><p>That’s all there is to it. It’s a simple and powerful way of adding more life into the user interaction. The full implementation of this demo can be found on my Github.</p><p><strong>Demo code:</strong></p><p><a href="https://github.com/patrick-elmquist/Demo-RecyclerViewParallaxPart2">GitHub - patrick-elmquist/Demo-RecyclerViewParallaxPart2: Demo code for a future blog post</a></p><p>And again, if you haven’t checked out Part 1 in the series, I recommend having a look at it to get some more explanation around how it all works:</p><p><a href="https://medium.com/@patrick.elmquist/add-extra-depth-to-your-list-using-parallax-eddb27b369de">Add extra depth to your list using parallax</a></p><p>That’s all for this time, have a good one!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8eb1c0e461e0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Separate keymap repo for QMK]]></title>
            <link>https://medium.com/@patrick.elmquist/separate-keymap-repo-for-qmk-136ff5a419bd?source=rss-560a85bfeb34------2</link>
            <guid isPermaLink="false">https://medium.com/p/136ff5a419bd</guid>
            <category><![CDATA[keymap]]></category>
            <category><![CDATA[keyboard]]></category>
            <category><![CDATA[git-repo]]></category>
            <category><![CDATA[gitsubmodule]]></category>
            <category><![CDATA[qmk]]></category>
            <dc:creator><![CDATA[Patrick Elmquist]]></dc:creator>
            <pubDate>Thu, 02 Sep 2021 10:46:56 GMT</pubDate>
            <atom:updated>2024-06-28T07:20:56.631Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*r-w-kuNl1m453CA5eHSUQw.jpeg" /><figcaption>Kyria from <a href="https://splitkb.com/products/kyria-pcb-kit">SplitKB</a></figcaption></figure><p>A while back I started getting into custom mechanical keyboards and the QMK firmware, a really fun side project. One issue I’ve faced since starting to create my own keymaps and custom functionality is that all my code have been living inside the massive qmk_firmware repository which means some extra headache when rebasing to get the latest features. This was especially clear recently when trying to rebase on the latest master branch after having cherry-picked a couple of PRs that were now merged. What I would like is to separate my code from the rest of the firmware, since I never change anything outside of my own folders anyway.</p><p>To sort this out I’d seen people use separate repositories for their custom code and symlink it when building. Having no experience whatsoever with that kind of black magic I started googling “QMK keymap separate repo” and to my surprise I couldn’t find any blog posts about it. So now that I’ve finally taken the time to set it up for myself, I thought I’d share the steps (it was a lot easier than I thought). What we’ll be doing is to create a new repository, add QMK as a submodule and create a make file handling the symlinks and build steps.</p><blockquote><strong>Update 2024</strong></blockquote><blockquote>QMK now has support for external userspace<br><a href="https://docs.qmk.fm/newbs_external_userspace">https://docs.qmk.fm/newbs_external_userspace</a></blockquote><blockquote>While I have only tried it out briefly, this is most likely a better way of doing things going forward as you don’t need error prone submodules and symlinks.</blockquote><blockquote>As an example, here is my repo with my common code and the configs for my Ferris Sweep: <a href="https://github.com/patrick-elmquist/qmk_userspace">https://github.com/patrick-elmquist/qmk_userspace</a></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*t1X9qr0YMh7Y5pq8r10oJQ.jpeg" /><figcaption>Lily58 from <a href="https://mechboards.co.uk/shop/kits/lily58-kit/">Mechboards</a></figcaption></figure><p>First off, here’s a link to my ‘keymaps’ repository on Github, it’s what you will end up with when following the steps below:</p><p><a href="https://github.com/patrick-elmquist/qmk-keymaps">GitHub - patrick-elmquist/qmk-keymaps: A user repository with my keymaps for QMK keyboards.</a></p><p>After following a discussion in #keymap-ideas on the <a href="https://splitkb.com/discord">SplitKB Discord</a>, I ended up using the following repo as a template for how to set it up:</p><p><a href="https://github.com/grasegger/kyria-layout">GitHub - grasegger/kyria-layout</a></p><p>Since I have more than one keyboard and a user space folder in qmk_firmware I needed to make some adjustments for it to suit my needs. These are the steps I went through:</p><h4>Create a new Git repository and add QMK as a submodule</h4><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/9a898fb4b1aa5aa9dbbafd6553c9730e/href">https://medium.com/media/9a898fb4b1aa5aa9dbbafd6553c9730e/href</a></iframe><p>We don’t want to commit the build output to the repo, so add a .gitignore file with the following:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/13b72a259e1c1029c08ed41ff7c7cda4/href">https://medium.com/media/13b72a259e1c1029c08ed41ff7c7cda4/href</a></iframe><p><strong>Update March 2023:<br></strong>Do <strong>NOT</strong> name your repository (or at least the folder you clone into) <strong>keymaps</strong>as a recent change to QMK can make the builds fail with the following error:</p><pre>qmk_firmware ❯ qmk compile -kb dz60 -km default<br>Ψ Compiling keymap with gmake --jobs=1 dz60:default<br><br><br>QMK Firmware 0.20.0<br>make: *** No rule to make target &#39;dz60:default&#39;. Stop.<br>|<br>| QMK&#39;s make format is:<br>|     make keyboard_folder:keymap_folder[:target]<br>|<br>| Where `keyboard_folder` is the path to the keyboard relative to<br>| `qmk_firmware/keyboards/`, and `keymap_folder` is the name of the<br>| keymap folder under that board&#39;s `keymaps/` directory.<br>|<br>| Examples:<br>|     keyboards/dz60, keyboards/dz60/keymaps/default<br>|       -&gt; make dz60:default<br>|       -&gt; qmk compile -kb dz60 -km default<br>|     keyboards/planck/rev6, keyboards/planck/keymaps/default<br>|       -&gt; make planck/rev6:default:flash<br>|       -&gt; qmk flash -kb planck/rev6 -km default<br>|</pre><p>QMK look for a folder called keymaps to find the provided keymap to compile and it seems having the repo folder called the same can cause conflicts.</p><h4>Add folder structure</h4><p>Now lets create the folder structure. In the qmk_firware repo I had keymaps for Lily58, Kyria and have common code for both in a user space folder. So this is this is the structure I use in the new repository:</p><pre>keymaps/<br> + kyria/ <br> + lily58/<br> + user/<br> | .gitignore<br> ` .gitmodules </pre><p>Now transfer any files you already have inqmk_firmware/keyboards/&lt;keyboard&gt;/keymaps/ and qmk_firmaware/users/&lt;user&gt; folders to their new location. <strong>Note</strong>: if you’re transferring files to the user folder it assumes that you have already set up your user space with the correct files. If you haven’t, you can find a guide for how to do it <a href="https://beta.docs.qmk.fm/using-qmk/software-features/feature_userspace">here</a>. Fore me the structure looked something like this after:</p><pre>keymaps/<br> + kyria/<br> |  | keymap.c<br> |  | config.h<br> |  `rules.mk<br> | <br> + lily58/<br> |  | keymap.c<br> |  | config.h<br> |  `rules.mk<br> |<br> + user/<br> |  | &lt;user&gt;.c<br> |  | &lt;user&gt;.h<br> |  ` ... more user files<br> |<br> | .gitignore<br> ` .gitmodules</pre><h4>Create Makefile</h4><p>When you’re done there’s only one more thing to do, and that’s to configure a Makefile to setup the symlinks and build the firmware. I should mention that prior to this I’ve never written a make file, so there might be better ways to do this, but I wanted to make it easy to add more boards.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/77b7dffc84124a139586ab81d0f7c63e/href">https://medium.com/media/77b7dffc84124a139586ab81d0f7c63e/href</a></iframe><p>Note: the Makefile assumes that the <a href="https://github.com/qmk/qmk_firmware/blob/master/docs/newbs_getting_started.md#2-prepare-your-build-environment-idset-up-your-environment">QMK CLI</a> is installed.<br>Now we are pretty much done, to build, just go into the main folder and run:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f3d530c43886eebb865dea5137ca0d4f/href">https://medium.com/media/f3d530c43886eebb865dea5137ca0d4f/href</a></iframe><h3>Wrapping up</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wUmEnAqQJ8Hdnu9zZuRxUQ.jpeg" /><figcaption>Kyria from <a href="https://splitkb.com/products/kyria-pcb-kit">SplitKB</a></figcaption></figure><p>That’s all I had to do to set it up. It’s been working great, I love to have the code separate from the main repo and also closer together making it easier to navigate while developing.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=136ff5a419bd" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Add an animated indicator to your BottomNavigationView]]></title>
            <link>https://itnext.io/add-an-animated-indicator-to-your-bottomnavigationview-db04c6aa738f?source=rss-560a85bfeb34------2</link>
            <guid isPermaLink="false">https://medium.com/p/db04c6aa738f</guid>
            <category><![CDATA[bottomnavigationview]]></category>
            <category><![CDATA[material-components]]></category>
            <category><![CDATA[canvas]]></category>
            <category><![CDATA[animation]]></category>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[Patrick Elmquist]]></dc:creator>
            <pubDate>Thu, 23 Apr 2020 09:11:24 GMT</pubDate>
            <atom:updated>2020-06-26T10:32:29.936Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/480/1*XaErZDWsFEKYtjJwM-YF3g.gif" /></figure><p>Do you think the standard BottomNavigationView component is a bit boring and want to spice it up? In this post I’ll show you how easy it is to add a custom indicator and have it animate between positions.</p><h4>What we’ll be doing</h4><ul><li>Extend BottomNavigationView</li><li>Draw an indicator using the Canvas APIs</li><li>Listen to item selection and move the indicator accordingly</li><li>Animate the movement and also add a scale up/down animation</li></ul><p>Before we start, even though all code you need is included below, you can find a sample project over on my Github page:</p><p><a href="https://github.com/patrick-elmquist/Demo-BottomNavigationIndicator">patrick-elmquist/Demo-BottomNavigationIndicator</a></p><h4>Show me the code</h4><p>So let’s first have a look at the class we’re implementing and then go through what it does.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/ab6b6d1a39c0479d58ba2622ea1a2172/href">https://medium.com/media/ab6b6d1a39c0479d58ba2622ea1a2172/href</a></iframe><p>Let’s break it down.</p><h4>Animate the position</h4><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/0086afbe4257fa8b45dc9a0ecf715637/href">https://medium.com/media/0086afbe4257fa8b45dc9a0ecf715637/href</a></iframe><p>This function is the meat and potatoes of the class, this is where we calculate the next position of the indicator and start the animation to move it in place. We start off by cancelling any running animation. Next we make sure that we can find a view for the given id. fromCenterX is stored outside the animation to get a start value used for an interpolation inside the animation update block.</p><p>The animation can be done in several ways. In this example I’ve decided to have it animate between the scale values since there are three of them (current scale &gt; large &gt; default) and interpolate the translation based on the animatedFraction, a value between 0.0-1.0 representing how far into the animation duration we are.</p><p>The code itself if pretty straight forward, we calculate the new position and use it together with the new scale value to update the indicator bounds.</p><p><strong>distanceTravelled vs scale <br></strong>The distance is always measured from the center of the view so that the scale won&#39;t play into it.</p><pre><em>Item1</em>               <em>Item2</em><br>    (=x=)<br>  |---|                <br>         (===x===)<br>  |----------|<br>                 (=x=)<br>  |----------------|</pre><p><strong>Indicator bounds<br></strong>The bounds are defined as positions within the parent, so we use the travelled distance and the new width to define the left and right values.</p><pre><em>distanceTravelled</em><br>       |<br> (=====x=====)<br> |---<em>width</em>---|</pre><pre>left  = dT - width/2<br>right = dT + width/2</pre><p><strong>Duration<br></strong>The animation duration is a calculated value and depends on the distance the indicator has to travel. Why complicate things? It makes sense that an animation between two neighbour items should be faster than the animation between the two items furthest apart, otherwise either the short movement will fell sluggish or the long movement will barely be noticeable.</p><p>So that’s the idea, however the implementation and values are completely arbitrary and mainly depend on what type of animation you’re doing and what interpolator you’re using. In this case, using a LinearOutSlowInInterpolator, having half the max duration be controlled by the distance felt like a good split.</p><pre><em>Item1</em>               <em>Item2</em><br>  O<br>  |-------------------|<br>      <em>distanceToMove</em></pre><pre>base     = 300L<br>variable = 300L<br>duration = base + variable * distanceToMove / parentWidth</pre><h4>Drawing the indicator</h4><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/10703bd1c21486c8695e6c5ac9ac0b91/href">https://medium.com/media/10703bd1c21486c8695e6c5ac9ac0b91/href</a></iframe><p>By default the BottomNavigationView won’t call onDraw, so instead we put the drawing command in dispatchDraw. The command is simple, draw a rounded rect with the bounds of the indicator and with a corner radius that’s half the height of the indicator. This gives the indicator a pill shape while moving and a circle when it’s still.</p><h4>That’s it</h4><p>Just use this new class instead of the BottomNavigationView in your layout and you’re good to go.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/480/1*XaErZDWsFEKYtjJwM-YF3g.gif" /></figure><p>That’s all for this time, have a good one!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=db04c6aa738f" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/add-an-animated-indicator-to-your-bottomnavigationview-db04c6aa738f">Add an animated indicator to your BottomNavigationView</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Enter animation using RecyclerView and LayoutAnimation Part 3: Exclude items]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@patrick.elmquist/enter-animation-using-recyclerview-and-layoutanimation-part-3-exclude-items-cddc0eb8300d?source=rss-560a85bfeb34------2"><img src="https://cdn-images-1.medium.com/max/1920/1*h9XpqBeV_zHtgGIHasu1KQ.png" width="1920"></a></p><p class="medium-feed-snippet">Learn how to exclude items from a LayoutAnimation.</p><p class="medium-feed-link"><a href="https://medium.com/@patrick.elmquist/enter-animation-using-recyclerview-and-layoutanimation-part-3-exclude-items-cddc0eb8300d?source=rss-560a85bfeb34------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://medium.com/@patrick.elmquist/enter-animation-using-recyclerview-and-layoutanimation-part-3-exclude-items-cddc0eb8300d?source=rss-560a85bfeb34------2</link>
            <guid isPermaLink="false">https://medium.com/p/cddc0eb8300d</guid>
            <category><![CDATA[android]]></category>
            <category><![CDATA[animation]]></category>
            <category><![CDATA[recyclerview]]></category>
            <category><![CDATA[layoutanimation]]></category>
            <dc:creator><![CDATA[Patrick Elmquist]]></dc:creator>
            <pubDate>Sat, 23 Nov 2019 19:50:45 GMT</pubDate>
            <atom:updated>2020-06-26T10:29:52.993Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Add extra depth to your list using parallax]]></title>
            <link>https://medium.com/@patrick.elmquist/add-extra-depth-to-your-list-using-parallax-eddb27b369de?source=rss-560a85bfeb34------2</link>
            <guid isPermaLink="false">https://medium.com/p/eddb27b369de</guid>
            <category><![CDATA[animation]]></category>
            <category><![CDATA[recyclerview]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[parallax]]></category>
            <category><![CDATA[lists]]></category>
            <dc:creator><![CDATA[Patrick Elmquist]]></dc:creator>
            <pubDate>Wed, 02 Jan 2019 13:55:43 GMT</pubDate>
            <atom:updated>2020-06-26T10:25:43.761Z</atom:updated>
            <content:encoded><![CDATA[<p>Lists don’t always have to be linear and boring, in this post we’ll see how to add some extra depth to a horizontal list by adding a simple parallax effect.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/480/1*cMEUmk0tb_yL14ig3r-90A.gif" /></figure><p>The base for the example will be a RecyclerView, configured with a horizontal LinearLayoutManager, and we’ll be using a ScrollListener to create the parallax effect.</p><h4>Example code</h4><p><em>All code from the example below can be found at my Github page:</em></p><p><a href="https://github.com/patrick-elmquist/Demo-RecyclerViewParallax">patrick-elmquist/Demo-RecyclerViewParallax</a></p><p><em>and the demo app is available for download here: </em><a href="https://github.com/patrick-elmquist/Demo-RecyclerViewParallax/releases/tag/v1.0"><em>APK@Github</em></a></p><h3>Set up the RecyclerView</h3><p>First, we need a data class to represent the list items. We can represent a TV show with a simple model class, containing a title, description and an image:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/3a79caadc1eda65729b10505d4bcd34b/href">https://medium.com/media/3a79caadc1eda65729b10505d4bcd34b/href</a></iframe><p>Second, we need a list adapter for Show items. Nothing fancy here, the adapter delegates the view binding to the ViewHolder:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/b635f4906f43824130d29695d0a9a85e/href">https://medium.com/media/b635f4906f43824130d29695d0a9a85e/href</a></iframe><p>Now let’s configure our RecyclerView:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/0f2c3d1c0e6193c57f865d31c49c5835/href">https://medium.com/media/0f2c3d1c0e6193c57f865d31c49c5835/href</a></iframe><p>The first two lines are basic — we create and add an adapter and make the list horizontal.</p><p>At the third line, we attach a PagerSnapHelper to the RecyclerView. The PagerSnapHelper is part of the RecyclerView library and makes the list behave like a ViewPager, where one view is always snapped to the center of the screen. So if you scroll halfway between two views and let go, the list will automatically center the closest view.</p><p>The last row calls setupParallaxScrollListener(), a function that configures the parallax effect. However, for it to make sense, we first need a list item…</p><h3>Create a ViewHolder</h3><p>This image shows what the item view will look like. We have a title and description text, and we show the Show image as both a thumbnail and the background.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/480/1*VIyd5LhL6Us6aO_wQjD2YA.png" /></figure><p>To create the item, we define item_show.xml as below. Note that most of the styling has been removed for brevity (the full implementation can be found <a href="https://github.com/patrick-elmquist/Demo-RecyclerViewParallax/blob/master/app/src/main/res/layout/item_show.xml">here</a>).</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/4edb4ac04dfc8b80ccc750793a846ad5/href">https://medium.com/media/4edb4ac04dfc8b80ccc750793a846ad5/href</a></iframe><p>With the layout done, we need a ViewHolder for the RecyclerView. Most of the implementation is pretty basic: we take in a Show item and bind the text and image to the view. We also add an extra property called offset, which is responsible for adding the parallax effect.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/46d36966b93e68252846833a9013389a/href">https://medium.com/media/46d36966b93e68252846833a9013389a/href</a></iframe><p>Let’s step through the offset setter:</p><p><strong>field = v.coerceIn(-1f, 1f)</strong></p><p>The offset is relative to the view width, and needs to be limited to the range of [-1.0..1.0] or else the views will be moved too far off the screen.</p><p><strong>val direction = if (field &lt; 0) -1f else 1f</strong></p><p>The offset sign determines which direction the views will move in. A value less than 0 means that the views will move to the left, and a value greater will move to the right.</p><p><strong>interpolator.getInterpolation(abs(field))</strong></p><p>Here we define how the parallax views should be moved. To understand what this does, we first need to understand what an <a href="https://en.wikipedia.org/wiki/Interpolation">interpolator</a> is. An interpolator is simply a function that takes an input value and transforms it into another value. In the animation below there’s the red ball, which is a linear function, and the purple ball which is an easing function.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/480/1*xS-0bgqaB0xyfZqGCzqWIw.gif" /><figcaption>Credit: Material Design Guidelines Motion chapter (<a href="https://material.io/design/motion/speed.html#easing">link</a>).</figcaption></figure><p>As you can see, they start and end in the same place, but the path in between is different. When a user scroll a view with its finger, the view moves as a linear function (red ball), which means that the view will follow the user’s finger. Adding a parallax effect means that you want some of the views to deviate from the finger position and move at another pace, but end up in the same place as the rest of the views. To achieve this, you can transform the linear scroll input by passing it to an easing function and apply the result to some of the views, making them move like the purple ball, while the rest of the views move like the red one.</p><pre>Input    Easing    Output<br> 0.0       -&gt;       0.0<br> 0.1       -&gt;       0.02<br> 0.2       -&gt;       0.07<br> 0.3       -&gt;       0.14<br> 0.4       -&gt;       0.22<br> 0.5       -&gt;       0.33<br> 0.6       -&gt;       0.44<br> 0.7       -&gt;       0.56<br> 0.8       -&gt;       0.70<br> 0.9       -&gt;       0.84<br> 1.0       -&gt;       1.0</pre><p>The reason for sending abs(field) to the interpolator is that it expects a value between 0 and 1, and will cap any negative value to 0. So we remove the sign of the input and re-apply it as a direction later on.</p><p><strong>direction * interpolatedValue * itemView.measuredWidth</strong></p><p>Let’s break it down:</p><ul><li><strong>direction</strong> - Determines if the view will move to the left or right.</li><li><strong>interpolatedValue</strong> - A value in the range [0.0..1.0] that determines how far the view should be moved, relative to the item view’s width.</li><li><strong>itemView.measuredWidth</strong> - The width of the item root view.</li></ul><p>Now, the only part left is to apply the new translation to the views that should move at a different pace. This is done by changing the translationX properties for the title, description and thumbnail.</p><h3>The ParallaxScrollListener</h3><p>Here’s the meat and potatoes of the parallax effect: let’s first have a look at the code and then step through it.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/7a70b65848d416b29b6658a56c812ad6/href">https://medium.com/media/7a70b65848d416b29b6658a56c812ad6/href</a></iframe><p>The code is based on the fact that our list item is as wide as the RecyclerView. This means that there will be (at most) two visible items at the screen at any given time.</p><p>Here’s what’s happening:</p><ol><li>Get the RecyclerView scroll offset, i.e. the total number of pixels that it’s scrolled from its start position.</li><li>Next we want to calculate an offset factor between [0.0..1.0], indicating how far the current item view has been scrolled (0.0 being not scrolled at all and 1.0 being entirely off screen). To do this we take the scroll offset and use the <a href="https://en.wikipedia.org/wiki/Modulo_operation">modulo operator</a> to get the remainder, which in this case represent how many pixels the current view is scrolled. To get the fraction we then just have to divide the remainder with the width of the RecyclerView.</li><li>Find the first visible ViewHolder in the LayoutManager and apply the offset to the ShowViewHolder.</li><li>Find the last visible ViewHolder in the same way, however, for this case we need to add an extra check. There are two items on the screen while scrolling, however, if the view is snapped to the center, findFirst and findLast will find the same item, causing the offset to be updated twice. To prevent this, just check that the position of the first and last item differ before updating the offset.</li></ol><p>And that’s all there is to it. You now have a list with a fancy parallax effect.</p><h3>Wrapping up</h3><p>So there you have it, the basics for adding some spice to a simple list. Remember, this is just one example of what you can do. To further customize the transition, use the offset provided in this example and play around with other view properties like:</p><ul><li>Translation X,Y,Z</li><li>Scale X,Y</li><li>Alpha</li><li>Tint color (tip: use <a href="https://developer.android.com/reference/android/animation/ArgbEvaluator">ArgbEvaluator</a> to tween colors)</li></ul><p>That’s all for this time, have a good one!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=eddb27b369de" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Randomized, endless animation using TimeAnimator]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@patrick.elmquist/continuous-animation-using-timeanimator-5b8a903603fb?source=rss-560a85bfeb34------2"><img src="https://cdn-images-1.medium.com/max/1920/1*6KOI38ppuPcRgUliBBJ8mQ.png" width="1920"></a></p><p class="medium-feed-snippet">Learn how to create an endless, randomized animation.</p><p class="medium-feed-link"><a href="https://medium.com/@patrick.elmquist/continuous-animation-using-timeanimator-5b8a903603fb?source=rss-560a85bfeb34------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://medium.com/@patrick.elmquist/continuous-animation-using-timeanimator-5b8a903603fb?source=rss-560a85bfeb34------2</link>
            <guid isPermaLink="false">https://medium.com/p/5b8a903603fb</guid>
            <category><![CDATA[timeanimator]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[frame-by-frame]]></category>
            <category><![CDATA[animation]]></category>
            <category><![CDATA[custom-views]]></category>
            <dc:creator><![CDATA[Patrick Elmquist]]></dc:creator>
            <pubDate>Mon, 07 Aug 2017 15:06:52 GMT</pubDate>
            <atom:updated>2020-06-26T09:49:35.877Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Enter animation using RecyclerView and LayoutAnimation Part 2: Grids]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://proandroiddev.com/enter-animation-using-recyclerview-and-layoutanimation-part-2-grids-688829b1d29b?source=rss-560a85bfeb34------2"><img src="https://cdn-images-1.medium.com/max/1920/1*h9XpqBeV_zHtgGIHasu1KQ.png" width="1920"></a></p><p class="medium-feed-snippet">Learn how to populate an empty RecyclerView grid using custom animations.</p><p class="medium-feed-link"><a href="https://proandroiddev.com/enter-animation-using-recyclerview-and-layoutanimation-part-2-grids-688829b1d29b?source=rss-560a85bfeb34------2">Continue reading on ProAndroidDev »</a></p></div>]]></description>
            <link>https://proandroiddev.com/enter-animation-using-recyclerview-and-layoutanimation-part-2-grids-688829b1d29b?source=rss-560a85bfeb34------2</link>
            <guid isPermaLink="false">https://medium.com/p/688829b1d29b</guid>
            <category><![CDATA[gridlayoutanimation]]></category>
            <category><![CDATA[recyclerview]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[animation]]></category>
            <category><![CDATA[layoutanimation]]></category>
            <dc:creator><![CDATA[Patrick Elmquist]]></dc:creator>
            <pubDate>Wed, 26 Jul 2017 18:33:54 GMT</pubDate>
            <atom:updated>2020-06-26T09:40:42.291Z</atom:updated>
        </item>
    </channel>
</rss>