Want to Contribute to us or want to have 15k+ Audience read your Article ? Or Just want to make a strong Backlink?

Creating Tinder like card feature with Android Compose



Introduction

Hi there! On this tutorial I will likely be exhibiting you easy methods to create the cardboard swipe function that apps like Tinder are utilizing. ๐Ÿ˜Ž

The ultimate product will likely be one thing like this: the person is ready to both swipe through drag or press the like/dislike button to do the work for them.




Importing the required libraries

First create an empty android compose undertaking, if you do not know how please discuss with my different tutorials.

Subsequent we have to import a few additional libraries, open up your app’s construct.gradle file and add the next dependencies:

implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"  
implementation "io.coil-kt:coil-compose:2.2.2"
Enter fullscreen mode

Exit fullscreen mode

We will likely be utilizing the ConstraintLayout and coil will likely be wanted so we are able to use AsyncImage. ๐Ÿ‘€




Creating the parts

Subsequent we are going to want two parts, the CardStackController and the precise CardStack, create a brand new bundle referred to as parts and create a brand new CardStackController file.

Within the CardStackController file import the next:

import androidx.compose.animation.core.Animatable  
import androidx.compose.animation.core.AnimationSpec  
import androidx.compose.basis.gestures.detectDragGestures  
import androidx.compose.materials.ExperimentalMaterialApi  
import androidx.compose.materials.SwipeableDefaults  
import androidx.compose.materials.ThresholdConfig  
import androidx.compose.runtime.Composable  
import androidx.compose.runtime.keep in mind  
import androidx.compose.runtime.rememberCoroutineScope  
import androidx.compose.ui.ExperimentalComposeUiApi  
import androidx.compose.ui.Modifier  
import androidx.compose.ui.composed  
import androidx.compose.ui.geometry.Offset  
import androidx.compose.ui.enter.pointer.pointerInput  
import androidx.compose.ui.platform.LocalConfiguration  
import androidx.compose.ui.platform.LocalDensity  
import androidx.compose.ui.unit.Dp  
import androidx.compose.ui.unit.dp  
import kotlinx.coroutines.CoroutineScope  
import kotlinx.coroutines.launch  
import kotlin.math.abs  
import kotlin.math.signal
Enter fullscreen mode

Exit fullscreen mode

Subsequent we have to create the CardStackController composable.

open class CardStackController(
    val scope: CoroutineScope,
    personal val screenWidth: Float,
    inside val animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec
) {
    val proper = Offset(screenWidth, 0f)
    val left = Offset(-screenWidth, 0f)
    val middle = Offset(0f, 0f)

    var threshold = 0.0f

    val offsetX = Animatable(0f)
    val offsetY = Animatable(0f)
    val rotation = Animatable(0f)
    val scale = Animatable(0.8f)

    var onSwipeLeft: () -> Unit = {}
    var onSwipeRight: () -> Unit = {}

    enjoyable swipeLeft() {
        scope.apply {
            launch {
                offsetX.animateTo(-screenWidth, animationSpec)

                onSwipeLeft()

                launch {
                    offsetX.snapTo(middle.x)
                }

                launch {
                    offsetY.snapTo(0f)
                }

                launch {
                    rotation.snapTo(0f)
                }

                launch {
                    scale.snapTo(0.8f)
                }
            }

            launch {
                scale.animateTo(1f, animationSpec)
            }
        }
    }

    enjoyable swipeRight() {
        scope.apply {
            launch {
                offsetX.animateTo(screenWidth, animationSpec)

                onSwipeRight()

                launch {
                    offsetX.snapTo(middle.x)
                }

                launch {
                    offsetY.snapTo(0f)
                }

                launch {
                    scale.snapTo(0.8f)
                }

                launch {
                    rotation.snapTo(0f)
                }
            }

            launch {
                scale.animateTo(1f, animationSpec)
            }
        }
    }

    enjoyable returnCenter() {
        scope.apply {
            launch {
                offsetX.animateTo(middle.x, animationSpec)
            }

            launch {
                offsetY.animateTo(middle.y, animationSpec)
            }

            launch {
                rotation.animateTo(0f, animationSpec)
            }

            launch {
                scale.animateTo(0.8f, animationSpec)
            }
        }
    }
}
Enter fullscreen mode

Exit fullscreen mode

This composable mainly offers with the playing cards animation, we cope with when the person swipes proper/left or returns to middle. If left the cardboard is thrown to the left, if proper the cardboard is thrown to the proper.

Subsequent we have to create a perform to recollect the state of the controller so under the CardStackController composable add the next perform:

@Composable
enjoyable rememberCardStackController(
    animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec
): CardStackController {
    val scope = rememberCoroutineScope()
    val screenWidth = with(LocalDensity.present) {
        LocalConfiguration.present.screenWidthDp.dp.toPx()
    }

    return keep in mind {
        CardStackController(
            scope = scope,
            screenWidth = screenWidth,
            animationSpec = animationSpec
        )
    }
}
Enter fullscreen mode

Exit fullscreen mode

After we have to create a draggable modifier extension in order that we are able to really drag the playing cards:

@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class)
enjoyable Modifier.draggableStack(
    controller: CardStackController,
    thresholdConfig: (Float, Float) -> ThresholdConfig,
    velocityThreshold: Dp = 125.dp
): Modifier = composed {
    val scope = rememberCoroutineScope()
    val density = LocalDensity.present

    val velocityThresholdPx = with(density) {
        velocityThreshold.toPx()
    }

    val thresholds = { a: Float, b: Float ->
        with(thresholdConfig(a, b)) {
            density.computeThreshold(a, b)
        }
    }

    controller.threshold = thresholds(controller.middle.x, controller.proper.x)

    Modifier.pointerInput(Unit) {
        detectDragGestures(
            onDragEnd = {
                if (controller.offsetX.worth <= 0f) {
                    if (controller.offsetX.worth > -controller.threshold) {
                        controller.returnCenter()
                    } else {
                        controller.swipeLeft()
                    }
                } else {
                    if (controller.offsetX.worth < controller.threshold) {
                        controller.returnCenter()
                    } else {
                        controller.swipeRight()
                    }
                }
            },
            onDrag = { change, dragAmount ->
                controller.scope.apply {
                    launch {
                        controller.offsetX.snapTo(controller.offsetX.worth + dragAmount.x)
                        controller.offsetY.snapTo(controller.offsetY.worth + dragAmount.y)

                        val targetRotation = normalize(
                            controller.middle.x,
                            controller.proper.x,
                            abs(controller.offsetX.worth),
                            0f,
                            10f
                        )

                        controller.rotation.snapTo(targetRotation * -controller.offsetX.worth.signal)

                        controller.scale.snapTo(
                            normalize(
                                controller.middle.x,
                                controller.proper.x / 3,
                                abs(controller.offsetX.worth),
                                0.8f
                            )
                        )
                    }
                }
                change.eat()
            }
        )
    }
}
Enter fullscreen mode

Exit fullscreen mode

Right here we detect when the dragging begins and ends and animate/normalize accordinly.

Lastly we have to create the perform to normalize the cardboard when it’s being dragged:

personal enjoyable normalize(
    min: Float,
    max: Float,
    v: Float,
    startRange: Float = 0f,
    endRange: Float = 1f
): Float {
    require(startRange < endRange) {
        "Begin vary is larger than finish vary"
    }

    val worth = v.coerceIn(min, max)

    return (worth - min) / (max - min) * (endRange + startRange) + startRange
}
Enter fullscreen mode

Exit fullscreen mode

Phew! That is the primary element created, subsequent we have to create the element to truly present the playing cards. Subsequent create a brand new file referred to as CardStack and import the wanted dependencies:

import androidx.compose.basis.format.*
import androidx.compose.materials.*
import androidx.compose.materials.icons.Icons
import androidx.compose.materials.icons.crammed.Shut
import androidx.compose.materials.icons.crammed.FavoriteBorder
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shade
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.format.ContentScale
import androidx.compose.ui.format.format
import androidx.compose.ui.textual content.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout
import coil.compose.AsyncImage
import kotlin.math.roundToInt
Enter fullscreen mode

Exit fullscreen mode

First we are going to create the really CardStack composible that can deal with the stack of playing cards:

@OptIn(ExperimentalMaterialApi::class)
@Composable
enjoyable CardStack(
    modifier: Modifier = Modifier,
    gadgets: MutableList<Merchandise>,
    thresholdConfig: (Float, Float) -> ThresholdConfig = { _, _ -> FractionalThreshold(0.2f)},
    velocityThreshold: Dp = 125.dp,
    onSwipeLeft: (merchandise: Merchandise) -> Unit = {},
    onSwipeRight: (merchandise: Merchandise) -> Unit = {},
    onEmptyStack: (lastItem: Merchandise) -> Unit = {}
) {
    var i by keep in mind {
        mutableStateOf(gadgets.dimension - 1)
    }

    if (i == -1) {
        onEmptyStack(gadgets.final())
    }

    val cardStackController = rememberCardStackController()

    cardStackController.onSwipeLeft = {
        onSwipeLeft(gadgets[i])
        i--
    }

    cardStackController.onSwipeRight = {
        onSwipeRight(gadgets[i])
        i--
    }

    ConstraintLayout(
        modifier = modifier
            .fillMaxSize()
            .padding(0.dp)
    ) {
        val stack = createRef()

        Field(
            modifier = modifier
                .constrainAs(stack) {
                    prime.linkTo(guardian.prime)
                }
                .draggableStack(
                    controller = cardStackController,
                    thresholdConfig = thresholdConfig,
                    velocityThreshold = velocityThreshold
                )
                .fillMaxHeight()
        ) {
            gadgets.asReversed().forEachIndexed  index == i -1)
                        .graphicsLayer(
                            rotationZ = if (index == i) cardStackController.rotation.worth else 0f,
                            scaleX = if (index < i) cardStackController.scale.worth else 1f,
                            scaleY = if (index < i) cardStackController.scale.worth else 1f
                         ),
                    merchandise,
                    cardStackController
                )
            
        }
    }
}
Enter fullscreen mode

Exit fullscreen mode

Subsequent we are going to create the precise card to point out the picture and the like/dislike buttons:

@Composable
enjoyable Card(
    modifier: Modifier = Modifier,
    merchandise: com.instance.tinderclone.parts.Merchandise,
    cardStackController: CardStackController
) {
    Field(modifier = modifier) {
        if (merchandise.url != null) {
            AsyncImage(
                mannequin = merchandise.url,
                contentDescription = "",
                contentScale = ContentScale.Crop,
                modifier = modifier.fillMaxSize()
            )
        }

        Column(
            modifier = modifier
                .align(Alignment.BottomStart)
                .padding(10.dp)
        ) {
            Textual content(textual content = merchandise.textual content, shade = Shade.White, fontWeight = FontWeight.Daring, fontSize = 25.sp) 

            Textual content(textual content = merchandise.subText, shade = Shade.White, fontSize = 20.sp)

            Row {
                IconButton(
                    modifier = modifier.padding(50.dp, 0.dp, 0.dp, 0.dp),
                    onClick = { cardStackController.swipeLeft() },
                ) {
                    Icon(
                        Icons.Default.Shut, contentDescription = "", tint = Shade.White, modifier =
                        modifier
                            .peak(50.dp)
                            .width(50.dp)
                    )
                }

                Spacer(modifier = Modifier.weight(1f))

                IconButton(
                    modifier = modifier.padding(0.dp, 0.dp, 50.dp, 0.dp),
                    onClick = { cardStackController.swipeRight() }
                ) {
                    Icon(
                        Icons.Default.FavoriteBorder, contentDescription = "", tint = Shade.White, modifier =
                        modifier.peak(50.dp).width(50.dp)
                    )
                }
            }
        }
    }
}
Enter fullscreen mode

Exit fullscreen mode

Right here we additionally use a spacle the be sure that the hate button is on the left and the like button is on the proper.

Subsequent we are going to create the information class to accommodate the playing cards contents, I additionally embody a title and a subtitle for a easy self introduction:

knowledge class Merchandise(
    val url: String? = null,
    val textual content: String = "",
    val subText: String = ""
)
Enter fullscreen mode

Exit fullscreen mode

Lastly we are going to create two Modifier extensions to deal with motion and card visibility:

enjoyable Modifier.moveTo(
    x: Float,
    y: Float
) = this.then(Modifier.format { measurable, constraints ->
    val placeable = measurable.measure(constraints)

    format(placeable.width, placeable.peak) {
        placeable.placeRelative(x.roundToInt(), y.roundToInt())
    }
})

enjoyable Modifier.seen(
    seen: Boolean = true
) = this.then(Modifier.format{ measurable, constraints ->
    val placeable = measurable.measure(constraints)

    if (seen) {
        format(placeable.width, placeable.peak) {
            placeable.placeRelative(0, 0)
        }
    } else {
        format(0, 0) {}
    }
})
Enter fullscreen mode

Exit fullscreen mode

That is the exhausting work achieved now all now we have to do is definitely use our parts!




Implementing the CardStack

Lastly all now we have to do now’s use our newly created parts within the MainActivity, so take away Greeting and change it with the next:

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalMaterialApi::class)
    override enjoyable onCreate(savedInstanceState: Bundle?) {
        tremendous.onCreate(savedInstanceState)
        setContent {
            TinderCloneTheme {
                val isEmpty = keep in mind {
                    mutableStateOf(false)
                }

                Scaffold {
                    Column(
                        modifier = Modifier
                            .fillMaxSize()
                            .padding(it),
                        horizontalAlignment = Alignment.CenterHorizontally,
                        verticalArrangement = Association.Heart
                    ) {
                        if (!isEmpty.worth) {
                            CardStack(
                                gadgets = accounts,
                                onEmptyStack = {
                                    isEmpty.worth = true
                                }
                            )
                        } else {
                            Textual content(textual content = "No extra playing cards", fontWeight = FontWeight.Daring)
                        }
                    }
                }
            }
        }
    }
}

val accounts = mutableListOf<Merchandise>(
    Merchandise("https://photos.unsplash.com/photo-1668069574922-bca50880fd70?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&match=crop&w=687&q=80", "Musician", "Alice (25)"),
    Merchandise("https://photos.unsplash.com/photo-1618641986557-1ecd230959aa?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&match=crop&w=687&q=80", "Developer", "Chris (33)"),
    Merchandise("https://photos.unsplash.com/photo-1667935764607-73fca1a86555?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&match=crop&w=688&q=80", "Trainer", "Roze (22)")
)
Enter fullscreen mode

Exit fullscreen mode

Utilizing the element is fairly simple, I lengthen the onEmptyStack perform to point out textual content if there’s nomore playing cards left to be proven.

I additionally use some dummy knowledge, the pictures are coutesy of unsplash.

Lastly should you hearth up the emulator/or an precise system you need to see the next:

Cool! ๐Ÿ˜†




Conclusion

On this tutorail I’ve proven easy methods to create a tinder like swipe app.
I hope you loved the tutorial as a lot as I loved making this. I am nonetheless new to Compose so in case you have anyhow to enhance it please let me know. ๐Ÿ˜ƒ

The supply for this undertaking might be discovered through:


Like me work? I put up about quite a lot of matters, if you need to see extra please like and comply with me.
Additionally I really like espresso.

โ€œBuy Me A Coffeeโ€

Add a Comment

Your email address will not be published. Required fields are marked *

Want to Contribute to us or want to have 15k+ Audience read your Article ? Or Just want to make a strong Backlink?