├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── canerkaseler │ │ └── threadscard │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── canerkaseler │ │ │ └── threadscard │ │ │ ├── MainActivity.kt │ │ │ ├── ThreadsInviteCard.kt │ │ │ ├── cards │ │ │ ├── CardBackSide.kt │ │ │ └── CardFrontSide.kt │ │ │ ├── entity │ │ │ └── User.kt │ │ │ └── ui │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_qr_code.png │ │ ├── ic_threads_black.png │ │ └── ic_user_avatar.jpg │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── canerkaseler │ └── threadscard │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | local.properties 7 | 8 | # Log/OS Files 9 | *.log 10 | 11 | # Android Studio generated files and folders 12 | captures/ 13 | .externalNativeBuild/ 14 | .cxx/ 15 | *.apk 16 | output.json 17 | 18 | # IntelliJ 19 | *.iml 20 | .idea/ 21 | misc.xml 22 | deploymentTargetDropDown.xml 23 | render.experimental.xml 24 | 25 | # Keystore files 26 | *.jks 27 | *.keystore 28 | 29 | # Google Services (e.g. APIs or Firebase) 30 | google-services.json 31 | 32 | # Android Profiling 33 | *.hprof 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Caner Kaşeler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![final_400](https://github.com/canerkaseler/jetpack-compose-threads-card/assets/130801186/b4d271c7-74f3-465c-8cd5-a0e58bd74d7f) 2 | 3 | # Threads Invitation Card with Jetpack Compose 4 | 5 | ![threads_medium_ck 2](https://github.com/canerkaseler/jetpack-compose-threads-card/assets/130801186/2652db5e-0092-4ab3-9705-aadc41e0a668) 6 | 7 | 8 | This repository targets to show Threads Card animation with Jetpack Compose in android development. 9 | 10 | This repository has a [Medium Article](https://proandroiddev.com/threads-invitation-card-with-jetpack-compose-2e5b9baede44). You can study this repository commit by commit with the article. 11 | 12 | ## Description 13 | 14 | This article aims to create a animation and UI copy of the Threads Invitation Card with Jetpack Compose in the Android project. This project includes combination of these three (3) different animations; Always Turning Animation, Rotating Card to near Axis-Y Animation after Dragging, Animation Rotating after Quick Dragging. 15 | 16 | ### A) Necessary topics: 17 | 1. User Data Model & Design Images 18 | 2. Front Side of the Card 19 | 3. Back Side of the Card 20 | 21 | ### B) Main Topics: 22 | 1. Card Turning with Dragging 23 | 2. Always Turning Animation & Stop Animation 24 | 3. Rotating Card to near Axis-Y Animation after Dragging 25 | 4. Animation Rotating after Quick Dragging then Infinite Turning Animation 26 | 27 | To continue reading about above parts, please check the [Medium Article](https://proandroiddev.com/threads-invitation-card-with-jetpack-compose-2e5b9baede44). 28 | 29 | 30 | ## Author 31 | 32 | All social media and contact info is [@canerkaseler](https://linktr.ee/canerkaseler) 33 | 34 | Buy Me A Coffee 35 | 36 | ## License 37 | ```xml 38 | MIT License 39 | 40 | Copyright (c) 2023 Caner Kaşeler 41 | 42 | Permission is hereby granted, free of charge, to any person obtaining a copy 43 | of this software and associated documentation files (the "Software"), to deal 44 | in the Software without restriction, including without limitation the rights 45 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 46 | copies of the Software, and to permit persons to whom the Software is 47 | furnished to do so, subject to the following conditions: 48 | 49 | The above copyright notice and this permission notice shall be included in all 50 | copies or substantial portions of the Software. 51 | ``` 52 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | } 5 | 6 | android { 7 | namespace = "com.canerkaseler.threadscard" 8 | compileSdk = 33 9 | 10 | defaultConfig { 11 | applicationId = "com.canerkaseler.threadscard" 12 | minSdk = 24 13 | targetSdk = 33 14 | versionCode = 1 15 | versionName = "1.0" 16 | 17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 18 | vectorDrawables { 19 | useSupportLibrary = true 20 | } 21 | } 22 | 23 | buildTypes { 24 | release { 25 | isMinifyEnabled = false 26 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility = JavaVersion.VERSION_1_8 31 | targetCompatibility = JavaVersion.VERSION_1_8 32 | } 33 | kotlinOptions { 34 | jvmTarget = "1.8" 35 | } 36 | buildFeatures { 37 | compose = true 38 | } 39 | composeOptions { 40 | kotlinCompilerExtensionVersion = "1.4.3" 41 | } 42 | packaging { 43 | resources { 44 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 45 | } 46 | } 47 | } 48 | 49 | dependencies { 50 | 51 | implementation("androidx.core:core-ktx:1.9.0") 52 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") 53 | implementation("androidx.activity:activity-compose:1.7.2") 54 | implementation(platform("androidx.compose:compose-bom:2023.03.00")) 55 | implementation("androidx.compose.ui:ui") 56 | implementation("androidx.compose.ui:ui-graphics") 57 | implementation("androidx.compose.ui:ui-tooling-preview") 58 | implementation("androidx.compose.material3:material3") 59 | testImplementation("junit:junit:4.13.2") 60 | androidTestImplementation("androidx.test.ext:junit:1.1.5") 61 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") 62 | androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00")) 63 | androidTestImplementation("androidx.compose.ui:ui-test-junit4") 64 | debugImplementation("androidx.compose.ui:ui-tooling") 65 | debugImplementation("androidx.compose.ui:ui-test-manifest") 66 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/canerkaseler/threadscard/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.canerkaseler.threadscard 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.canerkaseler.threadscard", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/canerkaseler/threadscard/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.canerkaseler.threadscard 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material3.Surface 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import com.canerkaseler.threadscard.ui.theme.ThreadsCardTheme 13 | 14 | class MainActivity : ComponentActivity() { 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | setContent { 18 | ThreadsCardTheme { 19 | // A surface container using the 'background' color from the theme 20 | Surface(modifier = Modifier.fillMaxSize(), color = Color.Black) { 21 | ThreadsInviteCard() 22 | } 23 | } 24 | } 25 | } 26 | } 27 | 28 | @Preview(showBackground = true) 29 | @Composable 30 | fun GreetingPreview() { 31 | ThreadsCardTheme { 32 | ThreadsInviteCard() 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/canerkaseler/threadscard/ThreadsInviteCard.kt: -------------------------------------------------------------------------------- 1 | package com.canerkaseler.threadscard 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.FastOutLinearInEasing 5 | import androidx.compose.animation.core.FastOutSlowInEasing 6 | import androidx.compose.animation.core.LinearEasing 7 | import androidx.compose.animation.core.infiniteRepeatable 8 | import androidx.compose.animation.core.tween 9 | import androidx.compose.foundation.gestures.detectHorizontalDragGestures 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.material3.Card 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.LaunchedEffect 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.runtime.setValue 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.graphics.graphicsLayer 22 | import androidx.compose.ui.input.pointer.pointerInput 23 | import androidx.compose.ui.unit.dp 24 | import com.canerkaseler.threadscard.cards.CardBackSide 25 | import com.canerkaseler.threadscard.cards.CardFrontSide 26 | import com.canerkaseler.threadscard.entity.User 27 | import kotlin.math.abs 28 | 29 | @Composable 30 | fun ThreadsInviteCard() { 31 | 32 | // This is our custom value of Axis-Y on coordinate system. 33 | var axisY by remember { mutableStateOf(0f) } 34 | 35 | // Manage animations. 36 | var isAutomaticAnimationActive by remember { mutableStateOf(true) } 37 | var isCompletingAnimationActive by remember { mutableStateOf(false) } 38 | var isQuickDragAnimationActive by remember { mutableStateOf(false) } 39 | 40 | // Follow drag amount to manage QuickDragAnimation. 41 | var animationDragAmount by remember { mutableStateOf(0f) } 42 | 43 | ThreadsInviteCardHolder ( 44 | frontSide = { 45 | CardFrontSide(user = User()) 46 | }, 47 | backSide = { 48 | CardBackSide() 49 | }, 50 | positionAxisY = if (isAutomaticAnimationActive) { 51 | val automaticTurningAnimation = remember { Animatable(axisY) } // Auto-turning animation. 52 | 53 | LaunchedEffect(isAutomaticAnimationActive) { 54 | if (isAutomaticAnimationActive) { 55 | automaticTurningAnimation.animateTo( 56 | targetValue = if (axisY >= 0) { 57 | axisY + 360f // Turn right. 58 | } else { 59 | - 360f + axisY // Turn left. 60 | }, 61 | animationSpec = infiniteRepeatable( 62 | tween(7000, easing = FastOutSlowInEasing) 63 | ), 64 | ) 65 | } 66 | } 67 | axisY = automaticTurningAnimation.value // Do not forget to update axis-Y. 68 | automaticTurningAnimation.value // Finally, return animation value. 69 | } 70 | else if (isCompletingAnimationActive) { 71 | val completeTurningAnimation = remember { Animatable(axisY) } 72 | 73 | LaunchedEffect(isCompletingAnimationActive) { 74 | if (isCompletingAnimationActive) { 75 | completeTurningAnimation.animateTo( 76 | targetValue = if(abs(axisY.toInt()) % 360 <= 90) { 77 | 0f 78 | } 79 | else if (abs(axisY.toInt()) % 360 in 91..270) { 80 | if (abs(axisY.toInt()) % 360 <= 270f) { 81 | 82 | if (axisY > 0) 180f else -180f 83 | } 84 | else { 85 | if (axisY > 0) 360f else -360f 86 | } 87 | } 88 | else { 89 | if (axisY > 0) 360f else -360f 90 | }, 91 | animationSpec = tween(500, easing = FastOutLinearInEasing) 92 | ).endState 93 | } 94 | } 95 | axisY = completeTurningAnimation.value 96 | completeTurningAnimation.value 97 | } 98 | else if (isQuickDragAnimationActive) { 99 | val completeQuickDragAnimation = remember { Animatable(axisY) } 100 | 101 | LaunchedEffect(isQuickDragAnimationActive) { 102 | if (isQuickDragAnimationActive) { 103 | 104 | val completeTurningAnimationState = completeQuickDragAnimation.animateTo( 105 | targetValue = if (animationDragAmount > 0) { 106 | 360f * 2 107 | } else { 108 | -360f * 2 109 | }, 110 | animationSpec = tween(1250, easing = LinearEasing) 111 | ).endState 112 | 113 | if (!completeTurningAnimationState.isRunning) { 114 | isQuickDragAnimationActive = false 115 | isAutomaticAnimationActive = true 116 | } 117 | } 118 | } 119 | axisY = completeQuickDragAnimation.value 120 | completeQuickDragAnimation.value 121 | } 122 | else { 123 | axisY 124 | }, 125 | modifier = Modifier 126 | .padding( 127 | horizontal = 48.dp, 128 | vertical = 210.dp 129 | ) 130 | .pointerInput(Unit) { 131 | detectHorizontalDragGestures( 132 | onDragStart = { offset -> 133 | isAutomaticAnimationActive = false // Stop animation. 134 | isCompletingAnimationActive = false // Stop animation. 135 | }, 136 | onDragEnd = { 137 | 138 | // Stop animations. 139 | isAutomaticAnimationActive = false 140 | isCompletingAnimationActive = false 141 | isQuickDragAnimationActive = false 142 | 143 | // If user did not drag enough, just show completing animation. 144 | if (abs(animationDragAmount) > 12f) { 145 | isQuickDragAnimationActive = true 146 | } else { 147 | isCompletingAnimationActive = true 148 | } 149 | }, 150 | onDragCancel = { 151 | 152 | }, 153 | onHorizontalDrag = { change, dragAmount -> 154 | 155 | // Decide to turn card in which side. 156 | axisY = if (dragAmount < 0) { 157 | (axisY - abs(dragAmount)) % 360 // Turn left for negative numbers. 158 | } else { 159 | (axisY + abs(dragAmount)) % 360 // Turn right for positive numbers. 160 | } 161 | 162 | animationDragAmount = dragAmount // Keep updated drag amount. 163 | } 164 | ) 165 | }, 166 | ) 167 | } 168 | 169 | /** 170 | * This holder manages logic to show correct side of the card. 171 | */ 172 | @Composable 173 | fun ThreadsInviteCardHolder( 174 | modifier: Modifier = Modifier, 175 | positionAxisY: Float, 176 | frontSide: @Composable () -> Unit = {}, 177 | backSide: @Composable () -> Unit = {}, 178 | ) { 179 | Card( 180 | modifier = modifier 181 | .graphicsLayer { 182 | rotationY = positionAxisY // Move card according to value of customY. 183 | cameraDistance = 14f * density 184 | }, 185 | ) { 186 | 187 | // Here, logic is about coordinate system such as [0..90], [91..270], [270..360]. 188 | if (abs(positionAxisY.toInt()) % 360 <= 90) { 189 | Box( 190 | Modifier.fillMaxSize() 191 | ) { 192 | frontSide() 193 | } 194 | } else if (abs(positionAxisY.toInt()) % 360 in 91..270) { 195 | Box( 196 | Modifier 197 | .fillMaxSize() 198 | .graphicsLayer { 199 | rotationY = 180f // Important to avoid mirror effect. 200 | }, 201 | ) { 202 | backSide() 203 | } 204 | } else { 205 | Box( 206 | Modifier.fillMaxSize() 207 | ) { 208 | frontSide() 209 | } 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /app/src/main/java/com/canerkaseler/threadscard/cards/CardBackSide.kt: -------------------------------------------------------------------------------- 1 | package com.canerkaseler.threadscard.cards 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.res.painterResource 13 | import androidx.compose.ui.tooling.preview.Preview 14 | import androidx.compose.ui.unit.dp 15 | import com.canerkaseler.threadscard.R 16 | 17 | @Composable 18 | fun CardBackSide() { 19 | 20 | // All card surface. 21 | Box( 22 | modifier = Modifier 23 | .fillMaxSize() 24 | .background(Color.White), 25 | contentAlignment = Alignment.Center, 26 | ) { 27 | 28 | // Top black half circle. 29 | CardBlackHalfCircles(modifier = Modifier.align(alignment = Alignment.TopCenter)) 30 | 31 | Image( 32 | modifier = Modifier.size(size = 160.dp), 33 | painter = painterResource(id = R.drawable.ic_threads_black), 34 | contentDescription ="" 35 | ) 36 | 37 | // Bottom black half circle. 38 | CardBlackHalfCircles(modifier = Modifier.align(alignment = Alignment.BottomCenter)) 39 | } 40 | } 41 | 42 | @Composable 43 | @Preview 44 | fun CardBackSidePreview() { 45 | CardBackSide() 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/canerkaseler/threadscard/cards/CardFrontSide.kt: -------------------------------------------------------------------------------- 1 | package com.canerkaseler.threadscard.cards 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.border 7 | import androidx.compose.foundation.layout.Arrangement 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.Spacer 12 | import androidx.compose.foundation.layout.fillMaxSize 13 | import androidx.compose.foundation.layout.fillMaxWidth 14 | import androidx.compose.foundation.layout.height 15 | import androidx.compose.foundation.layout.padding 16 | import androidx.compose.foundation.layout.size 17 | import androidx.compose.foundation.shape.CircleShape 18 | import androidx.compose.material3.Text 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.draw.clip 23 | import androidx.compose.ui.geometry.Offset 24 | import androidx.compose.ui.graphics.Color 25 | import androidx.compose.ui.graphics.PathEffect 26 | import androidx.compose.ui.layout.ContentScale 27 | import androidx.compose.ui.res.painterResource 28 | import androidx.compose.ui.text.TextStyle 29 | import androidx.compose.ui.text.font.FontFamily 30 | import androidx.compose.ui.text.font.FontWeight 31 | import androidx.compose.ui.tooling.preview.Preview 32 | import androidx.compose.ui.unit.dp 33 | import androidx.compose.ui.unit.sp 34 | import com.canerkaseler.threadscard.R 35 | import com.canerkaseler.threadscard.entity.User 36 | 37 | 38 | private val spaceBetweenItems = 28.dp 39 | private val framePadding = 24.dp 40 | 41 | @Composable 42 | fun CardFrontSide( 43 | user: User 44 | ) { 45 | 46 | // All card surface. 47 | Box( 48 | modifier = Modifier 49 | .fillMaxSize() 50 | .background(Color.White), 51 | contentAlignment = Alignment.Center, 52 | ) { 53 | 54 | // Top black half circle. 55 | CardBlackHalfCircles(modifier = Modifier.align(alignment = Alignment.TopCenter)) 56 | 57 | 58 | // Card content. 59 | CardContent( 60 | date = user.date, 61 | time = user.time, 62 | instagram = user.instagram, 63 | userId = user.userId, 64 | username = user.username, 65 | userImage = user.userImage, 66 | userQrCode = user.userQrCode, 67 | ) 68 | 69 | // Bottom black half circle. 70 | CardBlackHalfCircles(modifier = Modifier.align(alignment = Alignment.BottomCenter)) 71 | } 72 | } 73 | 74 | @Composable 75 | fun CardBlackHalfCircles( 76 | modifier: Modifier 77 | ) { 78 | Canvas( 79 | modifier = modifier 80 | .border(color = Color.Magenta, width = 2.dp) 81 | ) { 82 | drawCircle( 83 | color = Color.Black, 84 | radius = 24.dp.toPx() 85 | ) 86 | } 87 | } 88 | 89 | @Composable 90 | fun CardContent( 91 | date: String, 92 | time: String, 93 | instagram: String, 94 | userId: String, 95 | username: String, 96 | userImage: Int, 97 | userQrCode: Int, 98 | ) { 99 | Column ( 100 | modifier = Modifier 101 | ) { 102 | 103 | Row( 104 | modifier = Modifier.fillMaxWidth(), 105 | horizontalArrangement = Arrangement.SpaceBetween 106 | ) { 107 | 108 | CardTitleText(title = "DATE", info = date) 109 | 110 | CardBrand(modifier = Modifier.align(alignment = Alignment.Bottom)) 111 | } 112 | 113 | Spacer(modifier = Modifier.height(spaceBetweenItems)) 114 | 115 | CardTitleText(title = "TIME", info = time) 116 | 117 | Spacer(modifier = Modifier.height(spaceBetweenItems)) 118 | 119 | CardTitleText(title = "USERNAME", info = username) 120 | 121 | Spacer(modifier = Modifier.height(spaceBetweenItems)) 122 | 123 | CardUserQrCode(userQrCode = userQrCode, modifier = Modifier.align(alignment = Alignment.Start)) 124 | 125 | Spacer(modifier = Modifier.height(spaceBetweenItems)) 126 | 127 | CardDashDivider() 128 | 129 | Spacer(modifier = Modifier.height(spaceBetweenItems)) 130 | 131 | Row( 132 | modifier = Modifier.fillMaxWidth(), 133 | horizontalArrangement = Arrangement.SpaceBetween, 134 | verticalAlignment = Alignment.CenterVertically 135 | ) { 136 | Row ( 137 | verticalAlignment = Alignment.CenterVertically 138 | ) { 139 | 140 | CardUserImage(userImage = userImage) 141 | 142 | CardInstagram(text = instagram) 143 | } 144 | 145 | CardUserId(text = userId) 146 | } 147 | } 148 | } 149 | 150 | @Composable 151 | fun CardTitleText(title: String, info: String) { 152 | Column { 153 | Text( 154 | modifier = Modifier.padding( 155 | horizontal = framePadding 156 | ), 157 | text = title, 158 | color = Color.Black, 159 | fontWeight = FontWeight.ExtraBold, 160 | style = TextStyle( 161 | fontSize = 12.sp, 162 | fontFamily = FontFamily.Monospace, 163 | ) 164 | ) 165 | 166 | Text( 167 | modifier = Modifier.padding( 168 | horizontal = framePadding 169 | ), 170 | text = info, 171 | color = Color.Black, 172 | style = TextStyle( 173 | fontSize = 20.sp, 174 | fontFamily = FontFamily.Monospace 175 | ) 176 | ) 177 | } 178 | } 179 | 180 | @Composable 181 | fun CardBrand( 182 | modifier: Modifier 183 | ) { 184 | Image( 185 | modifier = modifier 186 | .padding( 187 | end = framePadding 188 | ) 189 | .size(size = 42.dp), 190 | painter = painterResource(id = R.drawable.ic_threads_black), 191 | contentDescription ="" 192 | ) 193 | } 194 | 195 | @Composable 196 | fun CardDashDivider() { 197 | Canvas( 198 | Modifier 199 | .fillMaxWidth() 200 | .height(1.dp)) { 201 | drawLine( 202 | color = Color.DarkGray, 203 | start = Offset(0f, 0f), 204 | end = Offset(size.width, 0f), 205 | pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 14f), 0f) 206 | ) 207 | } 208 | } 209 | 210 | @Composable 211 | fun CardUserQrCode(userQrCode: Int, modifier: Modifier) { 212 | Image( 213 | modifier = modifier 214 | .padding(horizontal = framePadding) 215 | .size(size = 56.dp), 216 | painter = painterResource(id = userQrCode), 217 | contentDescription ="" 218 | ) 219 | } 220 | 221 | @Composable 222 | fun CardUserImage(userImage: Int) { 223 | Image( 224 | modifier = Modifier 225 | .padding(start = framePadding) 226 | .size(size = 42.dp) 227 | .clip(CircleShape) , 228 | contentScale = ContentScale.Crop, 229 | painter = painterResource(id = userImage), 230 | contentDescription = "" 231 | ) 232 | } 233 | 234 | @Composable 235 | fun CardInstagram(text: String) { 236 | Text( 237 | modifier = Modifier.padding(start = 16.dp), 238 | text = text, 239 | color = Color.Black, 240 | fontWeight = FontWeight.SemiBold, 241 | style = TextStyle( 242 | fontSize = 12.sp, 243 | fontFamily = FontFamily.Default, 244 | letterSpacing = 0.7.sp, 245 | ) 246 | ) 247 | } 248 | 249 | @Composable 250 | fun CardUserId(text: String) { 251 | Text( 252 | modifier = Modifier.padding(end = framePadding), 253 | text = text, 254 | color = Color.Black, 255 | fontWeight = FontWeight.Light, 256 | style = TextStyle( 257 | fontSize = 14.sp, 258 | fontFamily = FontFamily.Default, 259 | letterSpacing = 1.sp, 260 | ) 261 | ) 262 | } 263 | 264 | @Composable 265 | @Preview 266 | fun CardFrontSidePreview() { 267 | CardFrontSide( 268 | User( 269 | username = "CANERKASELER", 270 | instagram = "canerkaseler", 271 | userId = "071030501", 272 | date = "WED JUL 7", 273 | time = "03:24 P.M.", 274 | userImage = R.drawable.ic_user_avatar, 275 | userQrCode = R.drawable.ic_qr_code, 276 | ) 277 | ) 278 | } -------------------------------------------------------------------------------- /app/src/main/java/com/canerkaseler/threadscard/entity/User.kt: -------------------------------------------------------------------------------- 1 | package com.canerkaseler.threadscard.entity 2 | 3 | import com.canerkaseler.threadscard.R 4 | 5 | data class User( 6 | val username: String = "CANERKASELER", 7 | val instagram: String = "canerkaseler", 8 | val userId: String = "071030501", 9 | val date: String = "WED JUL 7", 10 | val time: String = "03:24 P.M.", 11 | val userImage: Int = R.drawable.ic_user_avatar, 12 | val userQrCode: Int = R.drawable.ic_qr_code, 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/canerkaseler/threadscard/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.canerkaseler.threadscard.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/com/canerkaseler/threadscard/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.canerkaseler.threadscard.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.WindowCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun ThreadsCardTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | 53 | darkTheme -> DarkColorScheme 54 | else -> LightColorScheme 55 | } 56 | val view = LocalView.current 57 | if (!view.isInEditMode) { 58 | SideEffect { 59 | val window = (view.context as Activity).window 60 | window.statusBarColor = colorScheme.primary.toArgb() 61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 62 | } 63 | } 64 | 65 | MaterialTheme( 66 | colorScheme = colorScheme, 67 | typography = Typography, 68 | content = content 69 | ) 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/canerkaseler/threadscard/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.canerkaseler.threadscard.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_qr_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/res/drawable/ic_qr_code.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_threads_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/res/drawable/ic_threads_black.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_user_avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/res/drawable/ic_user_avatar.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canerkaseler/jetpack-compose-threads-card/455551927c57266e291aebd93fc7abb441aebd77/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Threads Card 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |