diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1afd053 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: CI + +on: + push: + branches: [master, main, "cursor/**"] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + - uses: android-actions/setup-android@v3 + - name: Assemble and unit test + run: ./gradlew assembleDebug testDebugUnitTest --stacktrace diff --git a/.gitignore b/.gitignore index 9c4de58..51fb2f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,14 @@ +*.iml .gradle /local.properties -/.idea/workspace.xml -/.idea/libraries +/.idea .DS_Store /build +**/build/ /captures +.externalNativeBuild +.cxx +*.apk +*.ap_ +.kotlin +*.hprof diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 4bb741d..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -TinderView \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 96cc43e..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml deleted file mode 100644 index e7bedf3..0000000 --- a/.idea/copyright/profiles_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index da49c43..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 5d19981..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 0c4afc9..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 7f68460..0000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..62fff32 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + Copyright 2015 Aradh Pillai + Copyright 2026 TinderView contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 654e757..f3fd0d7 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,93 @@ -TinderView -======== +# TinderView -Created A Simple and Beautiful Tinder like card deck & Captain Train like toolbar. +A lightweight Jetpack Compose card-stack for Android. Drag, fling, rewind — the 2026 swipe feel without a 2015 View hierarchy. -This is heavily based on [AndroidSwipeableCardStack](https://github.com/wenchaojiang/AndroidSwipeableCardStack), wenchaojiang has written the library very well but i faced a lot of difficulty integrating it & that's the reason i built this app so that you dont waste time. Use this repo as a base for your app & build on top :). + -API 16 and UP. (Support for API 16 is now live thanks to [#4] (https://github.com/GadgetCheck/TinderView/pull/4)) +The 24s clip walks the sample: pulse reveal, a below-threshold drag that springs back, like / nope / super-like throws, rewind, action-button squash, profile sheet, match burst, a real last-card swipe, empty deck, Explore, and Messages. -In Action -========= +## Usage -[Click here to watch the app in action on youtube!](https://youtu.be/ccnkq9HmyVY) +```kotlin +val state = rememberCardStackState { profiles.size } -![Screen 3](http://s4.postimg.org/x9bamw3yl/image.png)![Screen1](http://s4.postimg.org/7b7oguyh9/image.png) +CardStack( + items = profiles, + key = { it.id }, + state = state, + onSwiped = { profile, direction -> /* persist like / pass */ }, + onTopCardClick = { profile -> /* open sheet */ }, +) { profile -> + ProfileCard(profile) +} -Get it on Google Play +// Same physics as a finger fling +scope.launch { state.swipe(SwipeDirection.Right) } +scope.launch { state.rewind() } +``` -The demo is now available, dont forget to give it 5 stats if it helped you. Oh if you can contribute to development of the library in anyway i would love to see your pull request. +### Install -Happy Coding! +```kotlin +implementation(project(":cardstack")) +``` -Developed By -============ +The sample app (`:app`) is the reference integration. Maven Central publishing is a follow-up. -* Aradh Pillai +### Properties +| Property | Default | Meaning | +| --- | --- | --- | +| `visibleCount` | `3` | Cards composed at once | +| `stackOffset` | `10.dp` | Depth offset for back cards | +| `stackScaleStep` | `0.04` | Scale drop per depth | +| `maxRotationZ` | `14f` | Peak tilt in degrees | +| `thresholdFraction` | `0.35` | Commit distance as a fraction of card width | +| `flingVelocity` | `900.dp` | Velocity (per second) that commits a throw | +| `enabledDirections` | Left, Right, Up | Physical axes that can discard | +| `enableHaptics` | `true` | One tick on threshold, one on commit | +| `enableColorWash` | `true` | Leading-edge tint while dragging | +| `snapSpring` / `throwSpring` | tuned springs | Snap-back vs exit | -License -======= +Override via `CardStackProperties` or `CardStackDefaults.properties()`. - Copyright 2015 Aradh Pillai +### Overlays - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +LIKE / NOPE / SUPER stamps receive **0f..1f** progress. Replace the defaults: - http://www.apache.org/licenses/LICENSE-2.0 +```kotlin +CardStack( + items = profiles, + key = { it.id }, + likeOverlay = { progress -> MyLikeStamp(progress) }, + passOverlay = { progress -> MyNopeStamp(progress) }, + superLikeOverlay = { progress -> MySuperStamp(progress) }, +) { ProfileCard(it) } +``` - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +### State +| Field / API | Role | +| --- | --- | +| `currentIndex` | Top card in the stable list | +| `canSwipe` / `canRewind` | Drive action buttons | +| `isAnimating` | Ignore extra input mid-throw | +| `swipe(direction)` | Programmatic discard | +| `rewind()` | Fly the last card back in | +The list is **not** mutated by the library. Swipe advances `currentIndex`; rewind decrements it. Use `onSwiped` for side effects only. + +## Sample + +`:app` is a Material 3 demo: full-bleed cards, liquid-glass action bar, Super Like, match burst, Discover grid, and Chat from liked profiles. + +``` +./gradlew :app:assembleDebug +``` + +## License + +Copyright 2015 Aradh Pillai +Copyright 2026 TinderView contributors + +Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE). diff --git a/TinderView.iml b/TinderView.iml deleted file mode 100644 index fa96218..0000000 --- a/TinderView.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..a715da2 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,68 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.tinderview" + compileSdk = 36 + + defaultConfig { + applicationId = "com.tinderview" + minSdk = 26 + targetSdk = 36 + versionCode = 2 + versionName = "1.0.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables.useSupportLibrary = true + } + + buildTypes { + release { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + compose = true + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + implementation(project(":cardstack")) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.compose.bom)) + implementation(libs.compose.ui) + implementation(libs.compose.ui.graphics) + implementation(libs.compose.ui.tooling.preview) + implementation(libs.compose.material3) + implementation(libs.compose.animation) + debugImplementation(libs.compose.ui.tooling) + debugImplementation(libs.compose.ui.test.manifest) + androidTestImplementation(platform(libs.compose.bom)) + androidTestImplementation(libs.compose.ui.test.junit4) + androidTestImplementation(libs.androidx.junit) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..d578c68 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1 @@ +# Sample app. Compose and the cardstack consumer rules cover the hot path. diff --git a/app/src/androidTest/java/com/tinderview/CardStackSwipeTest.kt b/app/src/androidTest/java/com/tinderview/CardStackSwipeTest.kt new file mode 100644 index 0000000..f06bdc8 --- /dev/null +++ b/app/src/androidTest/java/com/tinderview/CardStackSwipeTest.kt @@ -0,0 +1,45 @@ +package com.tinderview + +import androidx.compose.material3.Text +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.tinderview.cardstack.CardStack +import com.tinderview.cardstack.CardStackState +import com.tinderview.cardstack.SwipeDirection +import com.tinderview.cardstack.rememberCardStackState +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class CardStackSwipeTest { + + @get:Rule + val composeRule = createComposeRule() + + @Test + fun swipeRightAdvancesCurrentIndex() { + val holder = arrayOfNulls(1) + composeRule.setContent { + val items = listOf("a", "b", "c") + val state = rememberCardStackState { items.size } + holder[0] = state + CardStack( + items = items, + key = { it }, + state = state, + ) { label -> + Text(label) + } + } + composeRule.onNodeWithContentDescription("Top card").assertExists() + composeRule.runOnIdle { + runBlocking { holder[0]!!.swipe(SwipeDirection.Right) } + } + composeRule.waitForIdle() + assertEquals(1, holder[0]!!.currentIndex) + } +} diff --git a/wear/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml similarity index 57% rename from wear/src/main/AndroidManifest.xml rename to app/src/main/AndroidManifest.xml index 7d68a15..c9fbeec 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,23 +1,25 @@ - - + xmlns:tools="http://schemas.android.com/tools"> + android:supportsRtl="true" + android:theme="@style/Theme.TinderView" + android:enableOnBackInvokedCallback="true" + tools:targetApi="33"> + android:exported="true" + android:screenOrientation="portrait" + android:theme="@style/Theme.TinderView" + android:windowSoftInputMode="adjustResize"> - - diff --git a/app/src/main/java/com/tinderview/MainActivity.kt b/app/src/main/java/com/tinderview/MainActivity.kt new file mode 100644 index 0000000..c889168 --- /dev/null +++ b/app/src/main/java/com/tinderview/MainActivity.kt @@ -0,0 +1,20 @@ +package com.tinderview + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import com.tinderview.ui.TinderViewApp +import com.tinderview.ui.theme.TinderViewTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + setContent { + TinderViewTheme { + TinderViewApp() + } + } + } +} diff --git a/app/src/main/java/com/tinderview/data/SampleProfiles.kt b/app/src/main/java/com/tinderview/data/SampleProfiles.kt new file mode 100644 index 0000000..e9bc10d --- /dev/null +++ b/app/src/main/java/com/tinderview/data/SampleProfiles.kt @@ -0,0 +1,90 @@ +package com.tinderview.data + +import androidx.compose.ui.graphics.Color + +data class Profile( + val id: String, + val name: String, + val age: Int, + val bio: String, + val city: String, + val gradient: List, + val interests: List, + val isMatch: Boolean = false, +) + +val SampleProfiles: List = listOf( + Profile( + id = "nova", + name = "Nova", + age = 27, + bio = "Designs typefaces, climbs granite, never skips golden hour.", + city = "Lisbon", + gradient = listOf(Color(0xFFFF7A59), Color(0xFFE85D4C), Color(0xFF3A1D18)), + interests = listOf("Type", "Granite", "Film"), + isMatch = true, + ), + Profile( + id = "julian", + name = "Julian", + age = 31, + bio = "Jazz pianist who makes a serious case for weeknight dancing.", + city = "Brooklyn", + gradient = listOf(Color(0xFF7B8CFF), Color(0xFF5A3D8A), Color(0xFF1A1228)), + interests = listOf("Jazz", "Dance"), + ), + Profile( + id = "mira", + name = "Mira", + age = 24, + bio = "Marine biologist. Will talk about tide pools until the sun comes up.", + city = "San Diego", + gradient = listOf(Color(0xFF3EE0C2), Color(0xFF0A7EA8), Color(0xFF062430)), + interests = listOf("Tide pools", "Coffee"), + ), + Profile( + id = "theo", + name = "Theo", + age = 29, + bio = "Chef. Makes pasta from scratch and playlists that start too loud.", + city = "Austin", + gradient = listOf(Color(0xFFFFC857), Color(0xFFE07A2F), Color(0xFF3A1E0A)), + interests = listOf("Pasta", "Vinyl"), + ), + Profile( + id = "sienna", + name = "Sienna", + age = 26, + bio = "Photo essays, thrifted denim, and a motorcycle named Pearl.", + city = "Portland", + gradient = listOf(Color(0xFFF2B6C6), Color(0xFFC46B7A), Color(0xFF3A1A22)), + interests = listOf("35mm", "Pearl"), + ), + Profile( + id = "kai", + name = "Kai", + age = 28, + bio = "Builds furniture by day, stargazes from rooftops by night.", + city = "Denver", + gradient = listOf(Color(0xFF6DD5ED), Color(0xFF1A6B8A), Color(0xFF0B1E26)), + interests = listOf("Oak", "Stars"), + ), + Profile( + id = "lina", + name = "Lina", + age = 25, + bio = "Runs sunrise 10Ks and still has energy for a second espresso.", + city = "Berlin", + gradient = listOf(Color(0xFFE36AA5), Color(0xFF5B2C6F), Color(0xFF160C22)), + interests = listOf("10K", "Espresso"), + ), + Profile( + id = "omar", + name = "Omar", + age = 33, + bio = "Documentary editor. Best first date is a flea market plus tacos.", + city = "Chicago", + gradient = listOf(Color(0xFF5EEAD4), Color(0xFF0F766E), Color(0xFF06201D)), + interests = listOf("Cuts", "Tacos"), + ), +) diff --git a/app/src/main/java/com/tinderview/ui/TinderViewApp.kt b/app/src/main/java/com/tinderview/ui/TinderViewApp.kt new file mode 100644 index 0000000..f2363db --- /dev/null +++ b/app/src/main/java/com/tinderview/ui/TinderViewApp.kt @@ -0,0 +1,148 @@ +package com.tinderview.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.selected +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import com.tinderview.data.Profile +import com.tinderview.ui.chat.ChatScreen +import com.tinderview.ui.connect.ConnectScreen +import com.tinderview.ui.discover.DiscoverScreen +import com.tinderview.ui.theme.CardsIcon +import com.tinderview.ui.theme.ChatIcon +import com.tinderview.ui.theme.Ink +import com.tinderview.ui.theme.SearchIcon + +private enum class Dest(val label: String) { + Connect("Connect"), + Discover("Explore"), + Chat("Chat"), +} + +@Composable +fun TinderViewApp() { + var selected by remember { mutableIntStateOf(0) } + val liked = remember { mutableStateListOf() } + val destinations = Dest.entries + + Scaffold( + containerColor = Ink.Night, + bottomBar = { + AppNavigationBar( + selected = selected, + destinations = destinations, + onSelect = { selected = it }, + ) + }, + ) { padding -> + val dest = destinations[selected] + Box( + Modifier + .padding(padding) + .fillMaxSize() + .background(Ink.Night), + ) { + Box( + Modifier + .fillMaxSize() + .zIndex(if (dest == Dest.Connect) 1f else 0f) + .graphicsLayer { alpha = if (dest == Dest.Connect) 1f else 0f } + .then(if (dest != Dest.Connect) Modifier.clearAndSetSemantics { } else Modifier), + ) { + ConnectScreen( + onLiked = { profile -> + if (liked.none { it.id == profile.id }) liked.add(0, profile) + }, + ) + } + if (dest == Dest.Discover) { + DiscoverScreen(modifier = Modifier.fillMaxSize()) + } + if (dest == Dest.Chat) { + ChatScreen(modifier = Modifier.fillMaxSize(), liked = liked) + } + } + } +} + +@Composable +private fun AppNavigationBar( + selected: Int, + destinations: List, + onSelect: (Int) -> Unit, +) { + Column( + Modifier + .fillMaxWidth() + .background(Ink.Ink) + .navigationBarsPadding(), + ) { + Box( + Modifier + .fillMaxWidth() + .height(1.dp) + .background(Ink.Hairline), + ) + Row( + modifier = Modifier + .fillMaxWidth() + .height(64.dp) + .padding(horizontal = 8.dp), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + ) { + destinations.forEachIndexed { index, dest -> + val active = selected == index + val tint = if (active) Ink.Coral else Ink.CreamMuted + Column( + modifier = Modifier + .weight(1f) + .clickable( + interactionSource = remember(index) { MutableInteractionSource() }, + indication = null, + onClick = { onSelect(index) }, + ) + .semantics { this.selected = active } + .padding(vertical = 6.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + when (dest) { + Dest.Connect -> CardsIcon(tint, size = 20.dp) + Dest.Discover -> SearchIcon(tint, size = 20.dp) + Dest.Chat -> ChatIcon(tint, size = 20.dp) + } + Text( + text = dest.label, + color = tint, + style = androidx.compose.material3.MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(top = 4.dp), + ) + } + } + } + } +} diff --git a/app/src/main/java/com/tinderview/ui/chat/ChatScreen.kt b/app/src/main/java/com/tinderview/ui/chat/ChatScreen.kt new file mode 100644 index 0000000..a00f7da --- /dev/null +++ b/app/src/main/java/com/tinderview/ui/chat/ChatScreen.kt @@ -0,0 +1,110 @@ +package com.tinderview.ui.chat + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.tinderview.data.Profile +import com.tinderview.ui.theme.Ink + +@Composable +fun ChatScreen( + liked: List, + modifier: Modifier = Modifier, +) { + Column( + modifier + .fillMaxSize() + .background(Ink.Night), + ) { + Text( + text = "Messages", + style = MaterialTheme.typography.headlineMedium, + color = Ink.Cream, + modifier = Modifier.padding(start = 22.dp, end = 22.dp, top = 18.dp), + ) + Text( + text = if (liked.isEmpty()) "Likes become conversations" else "${liked.size} open threads", + color = Ink.CreamMuted, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(start = 22.dp, end = 22.dp, top = 4.dp, bottom = 10.dp), + ) + if (liked.isEmpty()) { + Text( + text = "Like someone on Connect to start a conversation.", + color = Ink.CreamMuted, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(horizontal = 22.dp, vertical = 12.dp), + ) + } else { + LazyColumn( + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + items(liked, key = { it.id }) { profile -> + val brush = remember(profile.id) { Brush.linearGradient(profile.gradient) } + val shape = RoundedCornerShape(20.dp) + Row( + modifier = Modifier + .fillMaxWidth() + .clip(shape) + .background(Ink.Raised) + .border(1.dp, Ink.Hairline, shape) + .padding(horizontal = 14.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + Modifier + .size(52.dp) + .clip(CircleShape) + .background(brush) + .border( + width = if (profile.isMatch) 1.5.dp else 0.dp, + color = if (profile.isMatch) Ink.Coral else Color.Transparent, + shape = CircleShape, + ), + ) + Column(Modifier.padding(start = 14.dp).weight(1f)) { + Text( + text = profile.name, + style = MaterialTheme.typography.titleMedium, + color = Ink.Cream, + ) + Text( + text = if (profile.isMatch) { + "You matched · say hello" + } else { + "You liked ${profile.name}" + }, + color = Ink.CreamMuted, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 2.dp), + ) + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/tinderview/ui/connect/ActionBar.kt b/app/src/main/java/com/tinderview/ui/connect/ActionBar.kt new file mode 100644 index 0000000..760eefd --- /dev/null +++ b/app/src/main/java/com/tinderview/ui/connect/ActionBar.kt @@ -0,0 +1,117 @@ +package com.tinderview.ui.connect + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tinderview.ui.theme.CloseIcon +import com.tinderview.ui.theme.HeartIcon +import com.tinderview.ui.theme.Ink +import com.tinderview.ui.theme.RewindIcon +import com.tinderview.ui.theme.StarIcon + +@Composable +fun GlassActionBar( + canRewind: Boolean, + canSwipe: Boolean, + onRewind: () -> Unit, + onPass: () -> Unit, + onSuperLike: () -> Unit, + onLike: () -> Unit, + modifier: Modifier = Modifier, +) { + val pill = RoundedCornerShape(percent = 50) + Box( + modifier = modifier + .fillMaxWidth() + .shadow(22.dp, pill, ambientColor = Color.Black.copy(alpha = 0.45f), spotColor = Color.Black.copy(alpha = 0.55f)) + .clip(pill) + .background( + Brush.verticalGradient( + 0f to Color(0xCC2A2724), + 1f to Color(0xE6141210), + ), + ) + .border(1.dp, Color.White.copy(alpha = 0.16f), pill) + .padding(horizontal = 10.dp, vertical = 8.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(68.dp), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + ) { + ActionButton(enabled = canRewind, tint = Ink.Gold, size = 50.dp, onClick = onRewind) { + RewindIcon(Ink.Gold.copy(alpha = if (canRewind) 1f else 0.35f), size = 20.dp) + } + ActionButton(enabled = canSwipe, tint = Ink.Nope, size = 60.dp, onClick = onPass) { + CloseIcon(Ink.Nope.copy(alpha = if (canSwipe) 1f else 0.35f), size = 22.dp) + } + ActionButton(enabled = canSwipe, tint = Ink.Ice, size = 50.dp, onClick = onSuperLike) { + StarIcon(Ink.Ice.copy(alpha = if (canSwipe) 1f else 0.35f), size = 20.dp) + } + ActionButton(enabled = canSwipe, tint = Ink.Mint, size = 60.dp, onClick = onLike) { + HeartIcon(Ink.Mint.copy(alpha = if (canSwipe) 1f else 0.35f), size = 22.dp) + } + } + } +} + +@Composable +private fun ActionButton( + enabled: Boolean, + tint: Color, + size: Dp, + onClick: () -> Unit, + icon: @Composable () -> Unit, +) { + val interaction = remember { MutableInteractionSource() } + val pressed by interaction.collectIsPressedAsState() + val scale by animateFloatAsState( + targetValue = if (pressed) 0.88f else 1f, + animationSpec = spring(stiffness = 700f, dampingRatio = 0.55f), + label = "action-scale", + ) + Box( + modifier = Modifier + .size(size) + .scale(scale) + .clip(CircleShape) + .background(tint.copy(alpha = if (enabled) 0.16f else 0.06f)) + .border(1.25.dp, tint.copy(alpha = if (enabled) 0.85f else 0.22f), CircleShape) + .clickable( + enabled = enabled, + interactionSource = interaction, + indication = null, + onClick = onClick, + ), + contentAlignment = Alignment.Center, + ) { + icon() + } +} diff --git a/app/src/main/java/com/tinderview/ui/connect/ConnectScreen.kt b/app/src/main/java/com/tinderview/ui/connect/ConnectScreen.kt new file mode 100644 index 0000000..5cb4ac4 --- /dev/null +++ b/app/src/main/java/com/tinderview/ui/connect/ConnectScreen.kt @@ -0,0 +1,235 @@ +package com.tinderview.ui.connect + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.tinderview.cardstack.CardStack +import com.tinderview.cardstack.SwipeDirection +import com.tinderview.cardstack.rememberCardStackState +import com.tinderview.data.Profile +import com.tinderview.data.SampleProfiles +import com.tinderview.ui.theme.Ink +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ConnectScreen( + onLiked: (Profile) -> Unit, + modifier: Modifier = Modifier, +) { + val profiles = remember { SampleProfiles } + val state = rememberCardStackState { profiles.size } + val scope = rememberCoroutineScope() + var ready by remember { mutableStateOf(false) } + var selected by remember { mutableStateOf(null) } + var match by remember { mutableStateOf(null) } + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + LaunchedEffect(Unit) { + delay(1100) + ready = true + } + + Box(modifier.fillMaxSize().background(Ink.Night)) { + if (!ready) { + PulseLoader() + } + + AnimatedVisibility( + visible = ready, + enter = fadeIn() + scaleIn(initialScale = 0.975f), + exit = fadeOut(), + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Wordmark() + Box( + modifier = Modifier + .weight(1f) + .widthIn(max = 480.dp) + .padding(horizontal = 16.dp, vertical = 6.dp), + ) { + if (state.currentIndex >= profiles.size) { + EmptyDeck( + canRewind = state.canRewind, + onRewind = { scope.launch { state.rewind() } }, + ) + } else { + CardStack( + items = profiles, + key = { it.id }, + state = state, + modifier = Modifier.fillMaxSize(), + onSwiped = { profile, direction -> + if (direction != SwipeDirection.Left) { + onLiked(profile) + if (profile.isMatch && direction == SwipeDirection.Right) { + match = profile + } + } + }, + onTopCardClick = { selected = it }, + ) { profile -> + ProfileCard(profile) + } + } + } + GlassActionBar( + canRewind = state.canRewind, + canSwipe = state.canSwipe, + onRewind = { scope.launch { state.rewind() } }, + onPass = { scope.launch { state.swipe(SwipeDirection.Left) } }, + onSuperLike = { scope.launch { state.swipe(SwipeDirection.Up) } }, + onLike = { scope.launch { state.swipe(SwipeDirection.Right) } }, + modifier = Modifier + .padding(horizontal = 28.dp) + .padding(bottom = 16.dp, top = 8.dp), + ) + } + } + + match?.let { profile -> + MatchBurst(profile = profile, onDismiss = { match = null }) + } + } + + selected?.let { profile -> + ModalBottomSheet( + onDismissRequest = { selected = null }, + sheetState = sheetState, + containerColor = Ink.Raised, + contentColor = Ink.Cream, + ) { + Column( + Modifier + .fillMaxWidth() + .padding(horizontal = 22.dp, vertical = 8.dp), + ) { + ProfileCard( + profile = profile, + showBio = false, + modifier = Modifier + .fillMaxWidth() + .height(300.dp), + ) + Text( + text = profile.name, + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(top = 20.dp), + ) + Text( + text = "${profile.age} · ${profile.city}", + color = Ink.CreamMuted, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 4.dp), + ) + Row(Modifier.padding(top = 14.dp)) { + profile.interests.forEach { interest -> + InfoChip(interest) + Spacer(Modifier.width(8.dp)) + } + } + Text( + text = profile.bio, + style = MaterialTheme.typography.bodyLarge, + color = Ink.Cream.copy(alpha = 0.9f), + modifier = Modifier.padding(top = 16.dp), + ) + TextButton( + onClick = { selected = null }, + modifier = Modifier.align(Alignment.End), + ) { + Text("Close", color = Ink.Coral, fontWeight = FontWeight.SemiBold) + } + Spacer(Modifier.height(20.dp)) + } + } + } +} + +@Composable +private fun Wordmark() { + Row( + modifier = Modifier.padding(top = 6.dp, bottom = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + Modifier + .padding(end = 8.dp) + .size(7.dp) + .clip(CircleShape) + .background(Ink.Coral), + ) + Text( + text = "tinderview", + color = Ink.Cream, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + ) + } +} + +@Composable +private fun EmptyDeck( + canRewind: Boolean, + onRewind: () -> Unit, +) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(Modifier.weight(1f)) + Text( + text = "That's everyone", + style = MaterialTheme.typography.displayMedium, + color = Ink.Cream, + ) + Text( + text = "Rewind the last card, or see who's nearby.", + modifier = Modifier.padding(top = 10.dp, bottom = 18.dp), + color = Ink.CreamMuted, + style = MaterialTheme.typography.bodyLarge, + ) + if (canRewind) { + TextButton(onClick = onRewind) { + Text("Rewind last swipe", color = Ink.Coral, fontWeight = FontWeight.SemiBold) + } + } + Spacer(Modifier.weight(1f)) + } +} diff --git a/app/src/main/java/com/tinderview/ui/connect/MatchBurst.kt b/app/src/main/java/com/tinderview/ui/connect/MatchBurst.kt new file mode 100644 index 0000000..aa20eac --- /dev/null +++ b/app/src/main/java/com/tinderview/ui/connect/MatchBurst.kt @@ -0,0 +1,104 @@ +package com.tinderview.ui.connect + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.unit.dp +import com.tinderview.data.Profile +import com.tinderview.ui.theme.Ink +import kotlinx.coroutines.delay + +@Composable +fun MatchBurst( + profile: Profile, + onDismiss: () -> Unit, +) { + var visible by remember { mutableStateOf(false) } + LaunchedEffect(profile.id) { + visible = true + delay(2300) + onDismiss() + } + val scale by animateFloatAsState( + targetValue = if (visible) 1f else 0.72f, + animationSpec = spring(stiffness = 220f, dampingRatio = 0.68f), + label = "match-scale", + ) + val alpha by animateFloatAsState( + targetValue = if (visible) 1f else 0f, + label = "match-alpha", + ) + Box( + modifier = Modifier + .fillMaxSize() + .background(Ink.Wash.copy(alpha = 0.92f * alpha)) + .clickable(onClick = onDismiss), + contentAlignment = Alignment.Center, + ) { + Box( + Modifier + .size(340.dp) + .scale(scale) + .background( + Brush.radialGradient(listOf(Ink.Coral.copy(alpha = 0.28f * alpha), Color.Transparent)), + CircleShape, + ), + ) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.scale(scale), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + Modifier + .size(72.dp) + .offset(x = 10.dp) + .clip(CircleShape) + .background(Brush.linearGradient(listOf(Ink.Coral, Ink.CoralDeep))), + ) + Box( + Modifier + .size(72.dp) + .offset(x = (-10).dp) + .clip(CircleShape) + .background(Brush.linearGradient(profile.gradient)), + ) + } + Text( + text = "It's a match", + color = Ink.Cream, + style = MaterialTheme.typography.displayMedium, + modifier = Modifier.padding(top = 22.dp), + ) + Text( + text = "You and ${profile.name} liked each other", + color = Ink.CreamMuted, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 8.dp), + ) + } + } +} diff --git a/app/src/main/java/com/tinderview/ui/connect/ProfileCard.kt b/app/src/main/java/com/tinderview/ui/connect/ProfileCard.kt new file mode 100644 index 0000000..0ffac80 --- /dev/null +++ b/app/src/main/java/com/tinderview/ui/connect/ProfileCard.kt @@ -0,0 +1,118 @@ +package com.tinderview.ui.connect + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tinderview.data.Profile +import com.tinderview.ui.theme.Ink + +@Composable +fun ProfileCard( + profile: Profile, + modifier: Modifier = Modifier, + showBio: Boolean = true, +) { + val shape = remember { RoundedCornerShape(28.dp) } + val photo = remember(profile.id) { Brush.linearGradient(profile.gradient) } + val scrim = remember { + Brush.verticalGradient( + 0f to Color.Transparent, + 0.42f to Color.Transparent, + 0.72f to Color(0x66000000), + 1f to Color(0xCC000000), + ) + } + val sheen = remember { + Brush.linearGradient( + 0f to Color.White.copy(alpha = 0.16f), + 0.38f to Color.Transparent, + ) + } + Box( + modifier = modifier + .fillMaxSize() + .clip(shape) + .background(photo) + .border(1.dp, Color.White.copy(alpha = 0.12f), shape), + ) { + Box(Modifier.fillMaxSize().background(sheen)) + Box(Modifier.fillMaxSize().background(scrim)) + Column( + modifier = Modifier + .align(Alignment.BottomStart) + .fillMaxWidth() + .padding(horizontal = 22.dp, vertical = 20.dp), + ) { + Row(verticalAlignment = Alignment.Bottom) { + Text( + text = profile.name, + color = Ink.Cream, + style = MaterialTheme.typography.displayMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, fill = false), + ) + Text( + text = " ${profile.age}", + color = Ink.Cream.copy(alpha = 0.78f), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(bottom = 3.dp), + ) + } + Row( + modifier = Modifier.padding(top = 10.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + InfoChip(profile.city) + profile.interests.take(2).forEach { InfoChip(it) } + } + if (showBio) { + Text( + text = profile.bio, + color = Ink.Cream.copy(alpha = 0.88f), + style = MaterialTheme.typography.bodyMedium, + fontSize = 15.sp, + lineHeight = 21.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 12.dp), + ) + } + } + } +} + +@Composable +internal fun InfoChip(label: String) { + Text( + text = label, + color = Ink.Cream, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier + .clip(RoundedCornerShape(50)) + .background(Color.White.copy(alpha = 0.14f)) + .border(1.dp, Color.White.copy(alpha = 0.10f), RoundedCornerShape(50)) + .padding(horizontal = 10.dp, vertical = 5.dp), + ) +} diff --git a/app/src/main/java/com/tinderview/ui/connect/PulseLoader.kt b/app/src/main/java/com/tinderview/ui/connect/PulseLoader.kt new file mode 100644 index 0000000..bfca9df --- /dev/null +++ b/app/src/main/java/com/tinderview/ui/connect/PulseLoader.kt @@ -0,0 +1,62 @@ +package com.tinderview.ui.connect + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.unit.dp +import com.tinderview.ui.theme.Ink + +@Composable +fun PulseLoader(modifier: Modifier = Modifier) { + val transition = rememberInfiniteTransition(label = "pulse") + val t by transition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(1600, easing = LinearEasing), + repeatMode = RepeatMode.Restart, + ), + label = "pulse-t", + ) + Box(modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Canvas(Modifier.size(200.dp)) { + val center = Offset(size.width / 2f, size.height / 2f) + val maxR = size.minDimension / 2f + repeat(3) { i -> + val phase = ((t + i / 3f) % 1f) + drawCircle( + color = Ink.Coral.copy(alpha = (1f - phase) * 0.55f), + radius = maxR * (0.18f + phase * 0.78f), + center = center, + style = Stroke(width = 2.5.dp.toPx()), + ) + } + drawCircle(color = Ink.Coral, radius = maxR * 0.12f, center = center) + } + Text( + text = "tinderview", + color = Ink.Cream.copy(alpha = 0.72f), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(top = 8.dp), + ) + } + } +} diff --git a/app/src/main/java/com/tinderview/ui/discover/DiscoverScreen.kt b/app/src/main/java/com/tinderview/ui/discover/DiscoverScreen.kt new file mode 100644 index 0000000..856eb4b --- /dev/null +++ b/app/src/main/java/com/tinderview/ui/discover/DiscoverScreen.kt @@ -0,0 +1,92 @@ +package com.tinderview.ui.discover + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.tinderview.data.SampleProfiles +import com.tinderview.ui.theme.Ink + +@Composable +fun DiscoverScreen(modifier: Modifier = Modifier) { + Column( + modifier + .fillMaxSize() + .background(Ink.Night), + ) { + Text( + text = "Nearby", + style = MaterialTheme.typography.headlineMedium, + color = Ink.Cream, + modifier = Modifier.padding(start = 22.dp, end = 22.dp, top = 18.dp), + ) + Text( + text = "People around you tonight", + color = Ink.CreamMuted, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(start = 22.dp, end = 22.dp, top = 4.dp, bottom = 8.dp), + ) + LazyVerticalGrid( + columns = GridCells.Fixed(2), + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + items(SampleProfiles, key = { it.id }) { profile -> + val brush = remember(profile.id) { Brush.linearGradient(profile.gradient) } + val scrim = remember { + Brush.verticalGradient( + 0.45f to Color.Transparent, + 1f to Color(0xB3000000), + ) + } + val shape = RoundedCornerShape(22.dp) + Box( + modifier = Modifier + .aspectRatio(0.78f) + .clip(shape) + .background(brush) + .border(1.dp, Color.White.copy(alpha = 0.10f), shape), + ) { + Box(Modifier.fillMaxSize().background(scrim)) + Column( + modifier = Modifier + .align(Alignment.BottomStart) + .padding(14.dp), + ) { + Text( + text = "${profile.name}, ${profile.age}", + color = Ink.Cream, + style = MaterialTheme.typography.titleMedium, + ) + Text( + text = profile.city, + color = Ink.Cream.copy(alpha = 0.75f), + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(top = 2.dp), + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/tinderview/ui/theme/Color.kt b/app/src/main/java/com/tinderview/ui/theme/Color.kt new file mode 100644 index 0000000..8e6ecce --- /dev/null +++ b/app/src/main/java/com/tinderview/ui/theme/Color.kt @@ -0,0 +1,19 @@ +package com.tinderview.ui.theme + +import androidx.compose.ui.graphics.Color + +internal object Ink { + val Night = Color(0xFF0B0A09) + val Ink = Color(0xFF121110) + val Raised = Color(0xFF1C1A18) + val Hairline = Color(0x33F4EFE6) + val Cream = Color(0xFFF4EFE6) + val CreamMuted = Color(0xFFB8B1A6) + val Coral = Color(0xFFFF385C) + val CoralDeep = Color(0xFFE31C5F) + val Mint = Color(0xFF3DDC97) + val Ice = Color(0xFF6BB4FF) + val Gold = Color(0xFFE8B86D) + val Nope = Color(0xFFFF6B6B) + val Wash = Color(0xCC0B0A09) +} diff --git a/app/src/main/java/com/tinderview/ui/theme/Icons.kt b/app/src/main/java/com/tinderview/ui/theme/Icons.kt new file mode 100644 index 0000000..21301fc --- /dev/null +++ b/app/src/main/java/com/tinderview/ui/theme/Icons.kt @@ -0,0 +1,175 @@ +package com.tinderview.ui.theme + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.math.cos +import kotlin.math.sin + +private fun DrawScope.stroke(width: Float = size.minDimension * 0.09f) = Stroke( + width = width, + cap = StrokeCap.Round, + join = StrokeJoin.Round, +) + +@Composable +internal fun HeartIcon( + color: Color, + modifier: Modifier = Modifier, + size: Dp = 22.dp, + filled: Boolean = true, +) { + Canvas(modifier.size(size)) { + val w = this.size.width + val h = this.size.height + val path = Path().apply { + moveTo(w * 0.50f, h * 0.84f) + cubicTo(w * 0.22f, h * 0.66f, w * 0.10f, h * 0.50f, w * 0.18f, h * 0.32f) + cubicTo(w * 0.24f, h * 0.18f, w * 0.40f, h * 0.16f, w * 0.50f, h * 0.30f) + cubicTo(w * 0.60f, h * 0.16f, w * 0.76f, h * 0.18f, w * 0.82f, h * 0.32f) + cubicTo(w * 0.90f, h * 0.50f, w * 0.78f, h * 0.66f, w * 0.50f, h * 0.84f) + close() + } + drawPath(path, color, style = if (filled) Fill else stroke()) + } +} + +@Composable +internal fun CloseIcon(color: Color, modifier: Modifier = Modifier, size: Dp = 20.dp) { + Canvas(modifier.size(size)) { + val pad = this.size.minDimension * 0.24f + val s = stroke() + drawLine(color, Offset(pad, pad), Offset(this.size.width - pad, this.size.height - pad), s.width, s.cap) + drawLine(color, Offset(this.size.width - pad, pad), Offset(pad, this.size.height - pad), s.width, s.cap) + } +} + +@Composable +internal fun StarIcon(color: Color, modifier: Modifier = Modifier, size: Dp = 20.dp) { + Canvas(modifier.size(size)) { + val cx = this.size.width / 2f + val cy = this.size.height / 2f + val outer = this.size.minDimension * 0.46f + val inner = outer * 0.44f + val path = Path() + repeat(10) { i -> + val angle = Math.toRadians(-90.0 + i * 36.0) + val r = if (i % 2 == 0) outer else inner + val x = cx + (cos(angle) * r).toFloat() + val y = cy + (sin(angle) * r).toFloat() + if (i == 0) path.moveTo(x, y) else path.lineTo(x, y) + } + path.close() + drawPath(path, color) + } +} + +@Composable +internal fun RewindIcon(color: Color, modifier: Modifier = Modifier, size: Dp = 20.dp) { + Canvas(modifier.size(size)) { + val s = stroke() + val m = this.size.minDimension + val c = Offset(this.size.width * 0.56f, this.size.height * 0.52f) + val r = m * 0.30f + drawArc( + color = color, + startAngle = 50f, + sweepAngle = 250f, + useCenter = false, + topLeft = Offset(c.x - r, c.y - r), + size = Size(r * 2, r * 2), + style = s, + ) + val tip = Offset(this.size.width * 0.24f, this.size.height * 0.26f) + drawPath( + Path().apply { + moveTo(tip.x - m * 0.02f, tip.y - m * 0.12f) + lineTo(tip.x - m * 0.14f, tip.y + m * 0.04f) + lineTo(tip.x + m * 0.10f, tip.y + m * 0.06f) + close() + }, + color, + ) + } +} + +@Composable +internal fun SearchIcon(color: Color, modifier: Modifier = Modifier, size: Dp = 18.dp) { + Canvas(modifier.size(size)) { + val s = stroke() + val m = this.size.minDimension + val r = m * 0.28f + val c = Offset(m * 0.42f, m * 0.42f) + drawCircle(color, r, c, style = s) + drawLine( + color, + Offset(c.x + r * 0.72f, c.y + r * 0.72f), + Offset(m * 0.82f, m * 0.82f), + s.width, + StrokeCap.Round, + ) + } +} + +@Composable +internal fun CardsIcon(color: Color, modifier: Modifier = Modifier, size: Dp = 18.dp) { + Canvas(modifier.size(size)) { + val s = stroke(this.size.minDimension * 0.085f) + val m = this.size.minDimension + drawRoundRect( + color = color, + topLeft = Offset(m * 0.28f, m * 0.16f), + size = Size(m * 0.52f, m * 0.64f), + cornerRadius = androidx.compose.ui.geometry.CornerRadius(m * 0.10f), + style = s, + ) + drawRoundRect( + color = color, + topLeft = Offset(m * 0.16f, m * 0.26f), + size = Size(m * 0.52f, m * 0.64f), + cornerRadius = androidx.compose.ui.geometry.CornerRadius(m * 0.10f), + style = s, + ) + } +} + +@Composable +internal fun ChatIcon(color: Color, modifier: Modifier = Modifier, size: Dp = 18.dp) { + Canvas(modifier.size(size)) { + val s = stroke() + val m = this.size.minDimension + drawRoundRect( + color = color, + topLeft = Offset(m * 0.14f, m * 0.16f), + size = Size(m * 0.72f, m * 0.50f), + cornerRadius = androidx.compose.ui.geometry.CornerRadius(m * 0.16f), + style = s, + ) + drawLine( + color, + Offset(m * 0.32f, m * 0.64f), + Offset(m * 0.24f, m * 0.84f), + s.width, + StrokeCap.Round, + ) + drawLine( + color, + Offset(m * 0.24f, m * 0.84f), + Offset(m * 0.48f, m * 0.64f), + s.width, + StrokeCap.Round, + ) + } +} diff --git a/app/src/main/java/com/tinderview/ui/theme/Theme.kt b/app/src/main/java/com/tinderview/ui/theme/Theme.kt new file mode 100644 index 0000000..e9f9623 --- /dev/null +++ b/app/src/main/java/com/tinderview/ui/theme/Theme.kt @@ -0,0 +1,28 @@ +package com.tinderview.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable + +private val BrandDark = darkColorScheme( + primary = Ink.Coral, + onPrimary = Ink.Cream, + secondary = Ink.Mint, + tertiary = Ink.Ice, + background = Ink.Night, + surface = Ink.Ink, + surfaceVariant = Ink.Raised, + onBackground = Ink.Cream, + onSurface = Ink.Cream, + onSurfaceVariant = Ink.CreamMuted, + outline = Ink.Hairline, +) + +@Composable +fun TinderViewTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = BrandDark, + typography = TinderViewTypography, + content = content, + ) +} diff --git a/app/src/main/java/com/tinderview/ui/theme/Type.kt b/app/src/main/java/com/tinderview/ui/theme/Type.kt new file mode 100644 index 0000000..8226467 --- /dev/null +++ b/app/src/main/java/com/tinderview/ui/theme/Type.kt @@ -0,0 +1,107 @@ +package com.tinderview.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontVariation +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import com.tinderview.R + +@OptIn(ExperimentalTextApi::class) +internal val Outfit = FontFamily( + Font( + resId = R.font.outfit, + weight = FontWeight.Normal, + variationSettings = FontVariation.Settings(FontVariation.weight(400)), + ), + Font( + resId = R.font.outfit, + weight = FontWeight.Medium, + variationSettings = FontVariation.Settings(FontVariation.weight(500)), + ), + Font( + resId = R.font.outfit, + weight = FontWeight.SemiBold, + variationSettings = FontVariation.Settings(FontVariation.weight(600)), + ), + Font( + resId = R.font.outfit, + weight = FontWeight.Bold, + variationSettings = FontVariation.Settings(FontVariation.weight(700)), + ), + Font( + resId = R.font.outfit, + weight = FontWeight.ExtraBold, + variationSettings = FontVariation.Settings(FontVariation.weight(800)), + ), +) + +internal val TinderViewTypography = Typography( + displayLarge = TextStyle( + fontFamily = Outfit, + fontWeight = FontWeight.ExtraBold, + fontSize = 40.sp, + lineHeight = 46.sp, + letterSpacing = (-0.8).sp, + ), + displayMedium = TextStyle( + fontFamily = Outfit, + fontWeight = FontWeight.Bold, + fontSize = 34.sp, + lineHeight = 40.sp, + letterSpacing = (-0.5).sp, + ), + headlineMedium = TextStyle( + fontFamily = Outfit, + fontWeight = FontWeight.Bold, + fontSize = 28.sp, + lineHeight = 34.sp, + letterSpacing = (-0.4).sp, + ), + titleLarge = TextStyle( + fontFamily = Outfit, + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + lineHeight = 26.sp, + letterSpacing = (-0.2).sp, + ), + titleMedium = TextStyle( + fontFamily = Outfit, + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + lineHeight = 22.sp, + ), + bodyLarge = TextStyle( + fontFamily = Outfit, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + ), + bodyMedium = TextStyle( + fontFamily = Outfit, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + ), + labelLarge = TextStyle( + fontFamily = Outfit, + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp, + letterSpacing = 0.1.sp, + ), + labelMedium = TextStyle( + fontFamily = Outfit, + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + letterSpacing = 0.15.sp, + ), + labelSmall = TextStyle( + fontFamily = Outfit, + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + letterSpacing = 0.4.sp, + ), +) diff --git a/app/src/main/res/font/outfit.ttf b/app/src/main/res/font/outfit.ttf new file mode 100644 index 0000000..466d624 Binary files /dev/null and b/app/src/main/res/font/outfit.ttf differ diff --git a/mobile/src/main/res/mipmap-hdpi/lv_ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from mobile/src/main/res/mipmap-hdpi/lv_ic_launcher.png rename to app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/mobile/src/main/res/mipmap-mdpi/lv_ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from mobile/src/main/res/mipmap-mdpi/lv_ic_launcher.png rename to app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/mobile/src/main/res/mipmap-xhdpi/lv_ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from mobile/src/main/res/mipmap-xhdpi/lv_ic_launcher.png rename to app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/mobile/src/main/res/mipmap-xxhdpi/lv_ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from mobile/src/main/res/mipmap-xxhdpi/lv_ic_launcher.png rename to app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..40b0ca0 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + TinderView + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..5daca9f --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,8 @@ + + + + diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 1b7886d..0000000 --- a/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - repositories { - jcenter() - } - dependencies { - classpath 'com.android.tools.build:gradle:1.3.0' - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} - -allprojects { - repositories { - jcenter() - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..81f6a88 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,6 @@ +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.compose) apply false +} diff --git a/cardstack/build.gradle.kts b/cardstack/build.gradle.kts new file mode 100644 index 0000000..85ad29a --- /dev/null +++ b/cardstack/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.tinderview.cardstack" + compileSdk = 36 + + defaultConfig { + minSdk = 26 + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + compose = true + } +} + +kotlin { + explicitApi() +} + +dependencies { + implementation(platform(libs.compose.bom)) + implementation(libs.compose.ui) + implementation(libs.compose.foundation) + implementation(libs.compose.animation) + implementation(libs.compose.material3) + testImplementation(libs.junit) +} diff --git a/cardstack/consumer-rules.pro b/cardstack/consumer-rules.pro new file mode 100644 index 0000000..21a32bc --- /dev/null +++ b/cardstack/consumer-rules.pro @@ -0,0 +1,2 @@ +# CardStack is Compose-only; keep public API types if a consumer shrinks this module. +-keep class com.tinderview.cardstack.** { *; } diff --git a/cardstack/proguard-rules.pro b/cardstack/proguard-rules.pro new file mode 100644 index 0000000..3763157 --- /dev/null +++ b/cardstack/proguard-rules.pro @@ -0,0 +1 @@ +# Library-side rules are intentionally empty; consumers use consumer-rules.pro. diff --git a/cardstack/src/main/AndroidManifest.xml b/cardstack/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/cardstack/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/cardstack/src/main/java/com/tinderview/cardstack/CardStack.kt b/cardstack/src/main/java/com/tinderview/cardstack/CardStack.kt new file mode 100644 index 0000000..64ea378 --- /dev/null +++ b/cardstack/src/main/java/com/tinderview/cardstack/CardStack.kt @@ -0,0 +1,316 @@ +package com.tinderview.cardstack + +import android.provider.Settings +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key as composeKey +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.util.VelocityTracker +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.semantics.CustomAccessibilityAction +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.customActions +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed +import androidx.compose.ui.input.pointer.positionChange +import kotlin.math.min +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.launch + +@Composable +public fun CardStack( + items: List, + key: (T) -> Any, + modifier: Modifier = Modifier, + state: CardStackState = rememberCardStackState { items.size }, + properties: CardStackProperties = CardStackDefaults.properties(), + onSwiped: (T, SwipeDirection) -> Unit = { _, _ -> }, + onEmpty: () -> Unit = {}, + onTopCardClick: (T) -> Unit = {}, + likeOverlay: @Composable (Float) -> Unit = { CardStackDefaults.LikeStamp(it) }, + passOverlay: @Composable (Float) -> Unit = { CardStackDefaults.PassStamp(it) }, + superLikeOverlay: @Composable (Float) -> Unit = { CardStackDefaults.SuperLikeStamp(it) }, + content: @Composable (T) -> Unit, +) { + val context = LocalContext.current + val density = LocalDensity.current + val haptics = LocalHapticFeedback.current + val scope = rememberCoroutineScope() + val cardShape = remember { RoundedCornerShape(28.dp) } + + val onSwipedState = rememberUpdatedState(onSwiped) + val onEmptyState = rememberUpdatedState(onEmpty) + val onClickState = rememberUpdatedState(onTopCardClick) + val itemsState = rememberUpdatedState(items) + + SideEffect { + state.properties = properties + state.itemCountProvider = { items.size } + state.onSettledSwipe = { index, direction -> + itemsState.value.getOrNull(index)?.let { onSwipedState.value(it, direction) } + } + } + state.reduceMotion = remember(context) { + Settings.Global.getFloat( + context.contentResolver, + Settings.Global.ANIMATOR_DURATION_SCALE, + 1f, + ) == 0f + } + + var notifiedEmpty by remember { mutableStateOf(false) } + LaunchedEffect(state.currentIndex, items.size) { + if (state.currentIndex >= items.size && items.isNotEmpty() && !notifiedEmpty) { + notifiedEmpty = true + onEmptyState.value() + } + if (state.currentIndex < items.size) { + notifiedEmpty = false + } + } + + BoxWithConstraints(modifier = modifier) { + val widthPx = constraints.maxWidth.toFloat() + val heightPx = constraints.maxHeight.toFloat() + SideEffect { + state.updateCardSize(Size(widthPx, heightPx)) + } + + val start = state.currentIndex + val lastVisible = min(start + properties.visibleCount - 1, items.lastIndex) + val stackOffsetPx = with(density) { properties.stackOffset.toPx() } + + if (items.isEmpty() || start > items.lastIndex) { + return@BoxWithConstraints + } + + for (index in lastVisible downTo start) { + val item = items[index] + val depth = index - start + val isTop = depth == 0 + composeKey(key(item)) { + Box( + modifier = Modifier + .fillMaxSize() + .zIndex((properties.visibleCount - depth).toFloat()) + .graphicsLayer { + val progress = state.progressToward( + properties.enabledDirections, + properties.thresholdFraction, + ).second + val rise = if (isTop) 0f else progress + val scale = 1f - depth * properties.stackScaleStep + rise * properties.stackScaleStep + val y = stackOffsetPx * depth * (1f - rise) + translationY = y + scaleX = scale + scaleY = scale + if (isTop) { + translationX = state.dragOffset.x + translationY += state.dragOffset.y + rotationZ = CardStackMath.rotationZ( + translationX = state.dragOffset.x, + divisor = properties.rotationDivisor, + maxRotation = properties.maxRotationZ, + ) + shadowElevation = CardStackMath.elevationDp(progress) + shape = cardShape + clip = false + } else { + shadowElevation = 2f + shape = cardShape + clip = false + } + } + .then( + if (isTop) { + Modifier + .semantics { + contentDescription = "Top card" + customActions = listOf( + CustomAccessibilityAction("Like") { + scope.launch { state.swipe(SwipeDirection.Right) } + true + }, + CustomAccessibilityAction("Pass") { + scope.launch { state.swipe(SwipeDirection.Left) } + true + }, + CustomAccessibilityAction("Super like") { + scope.launch { state.swipe(SwipeDirection.Up) } + true + }, + CustomAccessibilityAction("Rewind") { + scope.launch { state.rewind() } + true + }, + ) + } + .pointerInput(properties, items.size, state.currentIndex) { + awaitEachGesture { + val down = awaitFirstDown(requireUnconsumed = false) + if (state.isAnimating) return@awaitEachGesture + val tracker = VelocityTracker() + tracker.addPosition(down.uptimeMillis, down.position) + var total = Offset.Zero + var dragging = false + var crossed = false + try { + while (true) { + val event = awaitPointerEvent() + val change = event.changes.firstOrNull { it.id == down.id } + if (change == null) { + if (dragging) scope.launch { state.snapBack() } + break + } + if (!change.pressed) { + if (change.changedToUpIgnoreConsumed()) { + if (!dragging) { + itemsState.value.getOrNull(state.currentIndex)?.let { + onClickState.value(it) + } + } else { + val velocity = tracker.calculateVelocity() + val (dir, progress) = state.progressToward( + properties.enabledDirections, + properties.thresholdFraction, + ) + val commit = CardStackMath.shouldCommit( + direction = dir, + progress = progress, + velocityX = velocity.x, + velocityY = velocity.y, + flingVelocityPx = state.flingVelocityPx(properties, density), + enabled = properties.enabledDirections, + ) + if (commit != null) { + if (properties.enableHaptics) { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + } + scope.launch { + state.throwOff(commit, Offset(velocity.x, velocity.y)) + } + } else { + scope.launch { state.snapBack() } + } + } + } else if (dragging) { + scope.launch { state.snapBack() } + } + break + } + val delta = change.positionChange() + total += delta + tracker.addPosition(change.uptimeMillis, change.position) + if (!dragging && total.getDistance() > viewConfiguration.touchSlop) { + dragging = true + } + if (dragging) { + change.consume() + val (cx, cy) = CardStackMath.constrainDrag( + total.x, + total.y, + properties.enabledDirections, + ) + state.dragTo(Offset(cx, cy)) + val progress = state.progressToward( + properties.enabledDirections, + properties.thresholdFraction, + ).second + if (properties.enableHaptics && !crossed && progress >= 1f) { + crossed = true + haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove) + } + if (progress < 1f) crossed = false + } + } + } catch (_: CancellationException) { + scope.launch { state.snapBack() } + } + } + } + } else { + Modifier + }, + ), + ) { + content(item) + if (isTop) { + OverlayLayer( + state = state, + properties = properties, + likeOverlay = likeOverlay, + passOverlay = passOverlay, + superLikeOverlay = superLikeOverlay, + ) + } + } + } + } + } +} + +@Composable +private fun OverlayLayer( + state: CardStackState, + properties: CardStackProperties, + likeOverlay: @Composable (Float) -> Unit, + passOverlay: @Composable (Float) -> Unit, + superLikeOverlay: @Composable (Float) -> Unit, +) { + val (direction, progress) = state.progressToward( + properties.enabledDirections, + properties.thresholdFraction, + ) + if (properties.enableColorWash && direction != null && progress > 0f) { + val wash = when (direction) { + SwipeDirection.Right -> Color(0xFF2ECC71) + SwipeDirection.Left -> Color(0xFFE74C3C) + SwipeDirection.Up -> Color(0xFF3498DB) + } + Box( + Modifier + .fillMaxSize() + .graphicsLayer { alpha = progress * 0.12f } + .background( + Brush.horizontalGradient( + listOf( + if (direction == SwipeDirection.Left) wash else Color.Transparent, + if (direction == SwipeDirection.Up) wash else Color.Transparent, + if (direction == SwipeDirection.Right) wash else Color.Transparent, + ), + ), + ), + ) + } + val likeProgress = if (direction == SwipeDirection.Right) progress else 0f + val passProgress = if (direction == SwipeDirection.Left) progress else 0f + val superProgress = if (direction == SwipeDirection.Up) progress else 0f + if (likeProgress > 0f) likeOverlay(likeProgress) + if (passProgress > 0f) passOverlay(passProgress) + if (superProgress > 0f) superLikeOverlay(superProgress) +} diff --git a/cardstack/src/main/java/com/tinderview/cardstack/CardStackDefaults.kt b/cardstack/src/main/java/com/tinderview/cardstack/CardStackDefaults.kt new file mode 100644 index 0000000..f811752 --- /dev/null +++ b/cardstack/src/main/java/com/tinderview/cardstack/CardStackDefaults.kt @@ -0,0 +1,89 @@ +package com.tinderview.cardstack + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +public object CardStackDefaults { + + public fun properties(): CardStackProperties = CardStackProperties() + + @Composable + public fun LikeStamp(progress: Float) { + Stamp( + label = "LIKE", + color = Color(0xFF2ECC71), + rotation = -18f, + progress = progress, + alignment = Alignment.TopStart, + ) + } + + @Composable + public fun PassStamp(progress: Float) { + Stamp( + label = "NOPE", + color = Color(0xFFE74C3C), + rotation = 18f, + progress = progress, + alignment = Alignment.TopEnd, + ) + } + + @Composable + public fun SuperLikeStamp(progress: Float) { + Stamp( + label = "SUPER", + color = Color(0xFF3498DB), + rotation = 0f, + progress = progress, + alignment = Alignment.TopCenter, + ) + } +} + +@Composable +private fun Stamp( + label: String, + color: Color, + rotation: Float, + progress: Float, + alignment: Alignment, +) { + val shape = remember { RoundedCornerShape(6.dp) } + Box( + modifier = Modifier + .fillMaxSize() + .clearAndSetSemantics { } + .padding(20.dp), + contentAlignment = alignment, + ) { + val alpha = progress.coerceIn(0f, 1f) + Text( + text = label, + color = color.copy(alpha = alpha), + fontSize = 28.sp, + fontWeight = FontWeight.Black, + letterSpacing = 3.sp, + modifier = Modifier + .rotate(rotation) + .background(color.copy(alpha = alpha * 0.12f), shape) + .border(2.dp, color.copy(alpha = alpha), shape) + .padding(horizontal = 12.dp, vertical = 5.dp), + ) + } +} diff --git a/cardstack/src/main/java/com/tinderview/cardstack/CardStackMath.kt b/cardstack/src/main/java/com/tinderview/cardstack/CardStackMath.kt new file mode 100644 index 0000000..6091b0a --- /dev/null +++ b/cardstack/src/main/java/com/tinderview/cardstack/CardStackMath.kt @@ -0,0 +1,129 @@ +package com.tinderview.cardstack + +import kotlin.math.abs +import kotlin.math.max + +/** + * Pure geometry for the deck. Kept allocation-free so unit tests and the + * gesture path share one implementation. + */ +public object CardStackMath { + + public fun rotationZ( + translationX: Float, + divisor: Float, + maxRotation: Float, + ): Float { + if (divisor == 0f) return 0f + return (translationX / divisor).coerceIn(-maxRotation, maxRotation) + } + + public fun dominantDirection(offsetX: Float, offsetY: Float): SwipeDirection? { + if (offsetX == 0f && offsetY == 0f) return null + val ax = abs(offsetX) + val ay = abs(offsetY) + return when { + ay > ax && offsetY < 0f -> SwipeDirection.Up + offsetX > 0f -> SwipeDirection.Right + offsetX < 0f -> SwipeDirection.Left + offsetY < 0f -> SwipeDirection.Up + else -> null + } + } + + public fun axisDistance( + offsetX: Float, + offsetY: Float, + direction: SwipeDirection, + ): Float { + val raw = when (direction) { + SwipeDirection.Left -> -offsetX + SwipeDirection.Right -> offsetX + SwipeDirection.Up -> -offsetY + } + return raw.coerceAtLeast(0f) + } + + public fun progressTowardCommit( + offsetX: Float, + offsetY: Float, + cardWidth: Float, + thresholdFraction: Float, + enabled: Set, + ): Pair { + val direction = dominantDirection(offsetX, offsetY) ?: return null to 0f + if (direction !in enabled) return direction to 0f + val thresholdPx = cardWidth * thresholdFraction + if (thresholdPx <= 0f) return direction to 1f + val distance = axisDistance(offsetX, offsetY, direction) + return direction to (distance / thresholdPx).coerceIn(0f, 1f) + } + + public fun shouldCommit( + direction: SwipeDirection?, + progress: Float, + velocityX: Float, + velocityY: Float, + flingVelocityPx: Float, + enabled: Set, + ): SwipeDirection? { + if (direction == null || direction !in enabled) return null + val flingSpeed = when (direction) { + SwipeDirection.Left -> -velocityX + SwipeDirection.Right -> velocityX + SwipeDirection.Up -> -velocityY + } + return if (progress >= 1f || flingSpeed >= flingVelocityPx) direction else null + } + + public fun nextIndex(current: Int, count: Int): Int { + val next = current + 1 + return if (next <= count) next else current + } + + public fun previousIndex(current: Int): Int = if (current > 0) current - 1 else 0 + + public fun rubberBand( + value: Float, + allow: Boolean, + coefficient: Float = 0.28f, + ): Float { + if (allow) return value + return value * coefficient / (1f + abs(value) * 0.002f) + } + + public fun constrainDrag( + offsetX: Float, + offsetY: Float, + enabled: Set, + ): Pair { + val xAllowed = + (offsetX > 0f && SwipeDirection.Right in enabled) || + (offsetX < 0f && SwipeDirection.Left in enabled) + val yAllowed = offsetY < 0f && SwipeDirection.Up in enabled + return rubberBand(offsetX, xAllowed) to rubberBand(offsetY, yAllowed) + } + + public fun resolvedDimension(value: Float, fallback: Float): Float { + return if (value < 8f) fallback else value + } + + public fun exitTarget( + direction: SwipeDirection, + cardWidth: Float, + cardHeight: Float, + currentX: Float, + currentY: Float, + ): Pair { + val extra = max(cardWidth, cardHeight) * 1.15f + return when (direction) { + SwipeDirection.Right -> (cardWidth + extra) to (currentY * 1.2f) + SwipeDirection.Left -> -(cardWidth + extra) to (currentY * 1.2f) + SwipeDirection.Up -> currentX to -(cardHeight + extra) + } + } + + public fun elevationDp(progress: Float, minDp: Float = 2f, maxDp: Float = 10f): Float { + return minDp + (maxDp - minDp) * progress.coerceIn(0f, 1f) + } +} diff --git a/cardstack/src/main/java/com/tinderview/cardstack/CardStackProperties.kt b/cardstack/src/main/java/com/tinderview/cardstack/CardStackProperties.kt new file mode 100644 index 0000000..9537b6a --- /dev/null +++ b/cardstack/src/main/java/com/tinderview/cardstack/CardStackProperties.kt @@ -0,0 +1,42 @@ +package com.tinderview.cardstack + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.SpringSpec +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Immutable +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Tunables for stack depth, commit threshold, and motion. + * Threshold is a fraction of card width — never a raw pixel value. + */ +@Immutable +public data class CardStackProperties( + public val visibleCount: Int = 3, + public val stackOffset: Dp = 12.dp, + public val stackScaleStep: Float = 0.05f, + public val maxRotationZ: Float = 14f, + public val rotationDivisor: Float = 25f, + public val thresholdFraction: Float = 0.35f, + public val flingVelocity: Dp = 900.dp, + public val enabledDirections: Set = setOf( + SwipeDirection.Left, + SwipeDirection.Right, + SwipeDirection.Up, + ), + public val enableHaptics: Boolean = true, + public val enableColorWash: Boolean = true, + public val snapSpring: SpringSpec = spring( + dampingRatio = 0.72f, + stiffness = 400f, + ), + public val throwSpring: SpringSpec = spring( + dampingRatio = 0.86f, + stiffness = 260f, + ), + public val rewindSpring: SpringSpec = spring( + dampingRatio = Spring.DampingRatioLowBouncy, + stiffness = 380f, + ), +) diff --git a/cardstack/src/main/java/com/tinderview/cardstack/CardStackState.kt b/cardstack/src/main/java/com/tinderview/cardstack/CardStackState.kt new file mode 100644 index 0000000..711bf60 --- /dev/null +++ b/cardstack/src/main/java/com/tinderview/cardstack/CardStackState.kt @@ -0,0 +1,196 @@ +package com.tinderview.cardstack + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.SpringSpec +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.unit.Density + +@Composable +public fun rememberCardStackState( + initialIndex: Int = 0, + itemCount: () -> Int, +): CardStackState { + val state = remember { + CardStackState(initialIndex = initialIndex, itemCount = itemCount) + } + state.itemCountProvider = itemCount + return state +} + +@Stable +public class CardStackState internal constructor( + initialIndex: Int, + itemCount: () -> Int, +) { + internal var itemCountProvider: () -> Int = itemCount + + public var currentIndex: Int by mutableIntStateOf(initialIndex.coerceAtLeast(0)) + internal set + + internal val offset = Animatable(Offset.Zero, Offset.VectorConverter) + + internal var fingerOffset: Offset by mutableStateOf(Offset.Zero) + + internal var cardSize: Size by mutableStateOf(Size.Zero) + + internal var lastDirection: SwipeDirection? by mutableStateOf(null) + + internal var lastExit: Offset by mutableStateOf(Offset.Zero) + + internal var properties: CardStackProperties = CardStackProperties() + + internal var reduceMotion: Boolean = false + + internal var onSettledSwipe: ((swipedIndex: Int, direction: SwipeDirection) -> Unit)? = null + + internal var animating: Boolean by mutableStateOf(false) + + public val isAnimating: Boolean + get() = animating || offset.isRunning + + public val canRewind: Boolean + get() = lastDirection != null && currentIndex > 0 && !isAnimating + + public val canSwipe: Boolean + get() = currentIndex < itemCountProvider() && !isAnimating + + public val dragOffset: Offset + get() = if (offset.isRunning || animating) offset.value else fingerOffset + + public val swipeProgress: Float + get() = progressToward(properties.enabledDirections, properties.thresholdFraction).second + + internal fun resolvedCardSize(): Size { + val width = CardStackMath.resolvedDimension(cardSize.width, 1080f) + val height = CardStackMath.resolvedDimension(cardSize.height, 1920f) + return Size(width, height) + } + + internal fun progressToward( + enabled: Set, + thresholdFraction: Float = 0.35f, + ): Pair { + val current = dragOffset + val size = resolvedCardSize() + return CardStackMath.progressTowardCommit( + offsetX = current.x, + offsetY = current.y, + cardWidth = size.width, + thresholdFraction = thresholdFraction, + enabled = enabled, + ) + } + + internal fun updateCardSize(size: Size) { + if (size != cardSize && size.width > 0f && size.height > 0f) { + cardSize = size + } + } + + public suspend fun swipe(direction: SwipeDirection) { + if (!canSwipe) return + throwOff(direction, initialVelocity = Offset.Zero) + } + + public suspend fun rewind() { + if (!canRewind) return + val exit = lastExit + // Place the returning card off-screen *before* the index moves or + // [animating] flips, otherwise one frame draws it at rest in the center. + fingerOffset = exit + currentIndex = CardStackMath.previousIndex(currentIndex) + offset.snapTo(exit) + animating = true + try { + if (reduceMotion) { + offset.snapTo(Offset.Zero) + } else { + offset.animateTo(Offset.Zero, offsetSpring(properties.rewindSpring)) + } + fingerOffset = Offset.Zero + lastDirection = null + lastExit = Offset.Zero + } finally { + animating = false + } + } + + internal fun dragTo(target: Offset) { + fingerOffset = target + } + + internal suspend fun snapBack() { + animating = true + try { + offset.snapTo(fingerOffset) + if (reduceMotion) { + offset.snapTo(Offset.Zero) + } else { + offset.animateTo(Offset.Zero, offsetSpring(properties.snapSpring)) + } + fingerOffset = Offset.Zero + } finally { + animating = false + } + } + + internal suspend fun throwOff( + direction: SwipeDirection, + initialVelocity: Offset, + ) { + if (currentIndex >= itemCountProvider()) return + val swipedIndex = currentIndex + animating = true + try { + val size = resolvedCardSize() + val (tx, ty) = CardStackMath.exitTarget( + direction = direction, + cardWidth = size.width, + cardHeight = size.height, + currentX = dragOffset.x, + currentY = dragOffset.y, + ) + val target = Offset(tx, ty) + offset.snapTo(fingerOffset) + if (reduceMotion) { + offset.snapTo(target) + } else { + offset.animateTo( + targetValue = target, + animationSpec = offsetSpring(properties.throwSpring), + initialVelocity = initialVelocity, + ) + } + fingerOffset = Offset.Zero + lastDirection = direction + lastExit = target + currentIndex = CardStackMath.nextIndex(currentIndex, itemCountProvider()) + offset.snapTo(Offset.Zero) + onSettledSwipe?.invoke(swipedIndex, direction) + } finally { + animating = false + } + } + + internal fun flingVelocityPx(properties: CardStackProperties, density: Density): Float { + return with(density) { properties.flingVelocity.toPx() } + } +} + +internal fun offsetSpring(spec: SpringSpec): SpringSpec { + return spring( + dampingRatio = spec.dampingRatio, + stiffness = spec.stiffness, + visibilityThreshold = Offset(1f, 1f), + ) +} diff --git a/cardstack/src/main/java/com/tinderview/cardstack/SwipeDirection.kt b/cardstack/src/main/java/com/tinderview/cardstack/SwipeDirection.kt new file mode 100644 index 0000000..e774203 --- /dev/null +++ b/cardstack/src/main/java/com/tinderview/cardstack/SwipeDirection.kt @@ -0,0 +1,11 @@ +package com.tinderview.cardstack + +/** + * Physical swipe axes the deck can commit along. + * Apps map these to product actions (Right = like, Left = pass, Up = super like). + */ +public enum class SwipeDirection { + Left, + Right, + Up, +} diff --git a/cardstack/src/test/java/com/tinderview/cardstack/CardStackMathTest.kt b/cardstack/src/test/java/com/tinderview/cardstack/CardStackMathTest.kt new file mode 100644 index 0000000..4a93c58 --- /dev/null +++ b/cardstack/src/test/java/com/tinderview/cardstack/CardStackMathTest.kt @@ -0,0 +1,146 @@ +package com.tinderview.cardstack + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +public class CardStackMathTest { + + private val allDirections = setOf( + SwipeDirection.Left, + SwipeDirection.Right, + SwipeDirection.Up, + ) + + @Test + public fun rotationIsClamped() { + val rotation = CardStackMath.rotationZ(1000f, divisor = 25f, maxRotation = 14f) + assertEquals(14f, rotation, 0.01f) + } + + @Test + public fun dominantDirectionPrefersVerticalWhenUp() { + assertEquals(SwipeDirection.Up, CardStackMath.dominantDirection(10f, -80f)) + assertEquals(SwipeDirection.Right, CardStackMath.dominantDirection(80f, -10f)) + assertEquals(SwipeDirection.Left, CardStackMath.dominantDirection(-80f, 4f)) + } + + @Test + public fun progressReachesOneAtThreshold() { + val (_, progress) = CardStackMath.progressTowardCommit( + offsetX = 140f, + offsetY = 0f, + cardWidth = 400f, + thresholdFraction = 0.35f, + enabled = allDirections, + ) + assertEquals(1f, progress, 0.01f) + } + + @Test + public fun progressIsZeroWhenDirectionDisabled() { + val (_, progress) = CardStackMath.progressTowardCommit( + offsetX = 0f, + offsetY = -200f, + cardWidth = 400f, + thresholdFraction = 0.35f, + enabled = setOf(SwipeDirection.Left, SwipeDirection.Right), + ) + assertEquals(0f, progress, 0.01f) + } + + @Test + public fun flingCommitsEvenBelowDistanceThreshold() { + val commit = CardStackMath.shouldCommit( + direction = SwipeDirection.Right, + progress = 0.2f, + velocityX = 1200f, + velocityY = 10f, + flingVelocityPx = 900f, + enabled = allDirections, + ) + assertEquals(SwipeDirection.Right, commit) + } + + @Test + public fun weakFlingDoesNotCommit() { + val commit = CardStackMath.shouldCommit( + direction = SwipeDirection.Right, + progress = 0.2f, + velocityX = 200f, + velocityY = 0f, + flingVelocityPx = 900f, + enabled = allDirections, + ) + assertNull(commit) + } + + @Test + public fun indexAdvancesAndRewinds() { + assertEquals(1, CardStackMath.nextIndex(0, 5)) + assertEquals(5, CardStackMath.nextIndex(5, 5)) + assertEquals(0, CardStackMath.previousIndex(0)) + assertEquals(2, CardStackMath.previousIndex(3)) + } + + @Test + public fun rubberBandReducesDisabledAxis() { + val raw = 200f + val banded = CardStackMath.rubberBand(raw, allow = false) + assertTrue(banded < raw && banded > 0f) + } + + @Test + public fun distanceThresholdCommitsWithoutFling() { + val commit = CardStackMath.shouldCommit( + direction = SwipeDirection.Left, + progress = 1f, + velocityX = 0f, + velocityY = 0f, + flingVelocityPx = 900f, + enabled = allDirections, + ) + assertEquals(SwipeDirection.Left, commit) + } + + @Test + public fun constrainDragRubberBandsDisabledUp() { + val (x, y) = CardStackMath.constrainDrag( + offsetX = 80f, + offsetY = -200f, + enabled = setOf(SwipeDirection.Left, SwipeDirection.Right), + ) + assertEquals(80f, x, 0.01f) + assertTrue(kotlin.math.abs(y) < 200f) + } + + @Test + public fun exitTargetLeavesTheScreen() { + val (rightX, _) = CardStackMath.exitTarget(SwipeDirection.Right, 400f, 600f, 20f, 10f) + val (leftX, _) = CardStackMath.exitTarget(SwipeDirection.Left, 400f, 600f, -20f, 10f) + val (_, upY) = CardStackMath.exitTarget(SwipeDirection.Up, 400f, 600f, 0f, -20f) + assertTrue(rightX > 400f) + assertTrue(leftX < -400f) + assertTrue(upY < -600f) + } + + @Test + public fun resolvedDimensionFallsBackWhenUnmeasured() { + assertEquals(1080f, CardStackMath.resolvedDimension(0f, 1080f), 0.01f) + assertEquals(1080f, CardStackMath.resolvedDimension(1f, 1080f), 0.01f) + assertEquals(400f, CardStackMath.resolvedDimension(400f, 1080f), 0.01f) + } + + @Test + public fun progressIsHalfAtMidThreshold() { + val (_, progress) = CardStackMath.progressTowardCommit( + offsetX = 70f, + offsetY = 0f, + cardWidth = 400f, + thresholdFraction = 0.35f, + enabled = allDirections, + ) + assertEquals(0.5f, progress, 0.02f) + } +} diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/demo.mp4 b/docs/demo.mp4 new file mode 100644 index 0000000..27eef7c Binary files /dev/null and b/docs/demo.mp4 differ diff --git a/gradle.properties b/gradle.properties index 1d3591c..f065224 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,18 +1,9 @@ -# Project-wide Gradle settings. - -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. - -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html - -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -# Default value: -Xmx10248m -XX:MaxPermSize=256m -# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 - -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true \ No newline at end of file +org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=true +android.useAndroidX=true +android.nonTransitiveRClass=true +kotlin.code.style=official +kotlin.incremental=true +android.suppressUnsupportedCompileSdk=36 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..97e442a --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,35 @@ +[versions] +agp = "8.7.3" +kotlin = "2.0.21" +compose-bom = "2024.12.01" +activity-compose = "1.9.3" +core-ktx = "1.15.0" +lifecycle-runtime = "2.8.7" +junit = "4.13.2" +androidx-junit = "1.2.1" +espresso = "3.6.1" +compose-ui-test = "1.7.6" + +[libraries] +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } +androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime" } +compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } +compose-ui = { group = "androidx.compose.ui", name = "ui" } +compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +compose-material3 = { group = "androidx.compose.material3", name = "material3" } +compose-animation = { group = "androidx.compose.animation", name = "animation" } +compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-junit" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 8c0fb64..a4b76b9 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a08963a..e2847c8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Wed Sep 30 14:06:07 IST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip diff --git a/gradlew b/gradlew index 91a7e26..f5feea6 100755 --- a/gradlew +++ b/gradlew @@ -1,79 +1,130 @@ -#!/usr/bin/env bash +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum -warn ( ) { +warn () { echo "$*" -} +} >&2 -die ( ) { +die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -82,83 +133,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec9973..9b42019 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,22 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -8,26 +26,30 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -35,54 +57,36 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/mobile/.gitignore b/mobile/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/mobile/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/mobile/build.gradle b/mobile/build.gradle deleted file mode 100644 index 061664e..0000000 --- a/mobile/build.gradle +++ /dev/null @@ -1,35 +0,0 @@ -apply plugin: 'com.android.application' - -android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" - - defaultConfig { - applicationId "com.tinderview" - minSdkVersion 19 - targetSdkVersion 23 - versionCode 1 - versionName "0.1" - } - buildTypes { - release { - - minifyEnabled true - shrinkResources true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } -} - -/*repositories { - flatDir { - dirs 'libs' - } -}*/ - -dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - wearApp project(':wear') - compile 'com.android.support:appcompat-v7:23.0.1' - -} diff --git a/mobile/mobile.iml b/mobile/mobile.iml deleted file mode 100644 index 2f50646..0000000 --- a/mobile/mobile.iml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mobile/proguard-rules.pro b/mobile/proguard-rules.pro deleted file mode 100644 index b89bfd7..0000000 --- a/mobile/proguard-rules.pro +++ /dev/null @@ -1,17 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in /Users/Aradh/Library/Android/sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} diff --git a/mobile/src/androidTest/java/com/tinderview/ApplicationTest.java b/mobile/src/androidTest/java/com/tinderview/ApplicationTest.java deleted file mode 100644 index e30f53d..0000000 --- a/mobile/src/androidTest/java/com/tinderview/ApplicationTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.tinderview; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * Testing Fundamentals - */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); - } -} \ No newline at end of file diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml deleted file mode 100644 index 508decc..0000000 --- a/mobile/src/main/AndroidManifest.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/src/main/java/com/tinderview/activities/MainScreenActivity.java b/mobile/src/main/java/com/tinderview/activities/MainScreenActivity.java deleted file mode 100755 index 3718e7a..0000000 --- a/mobile/src/main/java/com/tinderview/activities/MainScreenActivity.java +++ /dev/null @@ -1,226 +0,0 @@ -package com.tinderview.activities; - - -import android.content.Context; -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentStatePagerAdapter; -import android.support.v4.view.ViewPager; -import android.support.v7.app.ActionBar; -import android.support.v7.widget.Toolbar; -import android.support.v7.app.AppCompatActivity; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; - -import com.tinderview.R; -import com.tinderview.fragments.Connect; -import com.tinderview.fragments.Discover; -import com.tinderview.fragments.Chat; - -import com.tinderview.tabbarview.TabBarView; -import com.tinderview.util.CustomViewPager; - -/* created by Aradh Pillai on 05/10/15 -* -* this class is our home Screen in which all tabs has define -* Toolbar I have used -* Customized ViewPager -* CardStack -*/ -public class MainScreenActivity extends AppCompatActivity { - - //this context using in - public static Context context; - public static CustomViewPager mViewPager; - /*Toolbar use and set the tab dynamically - setting custom design to Toolbar - */ - Toolbar toolbar; - private String TAG = "MainScreenActivity"; - private TabBarView mTabBarView; - private MainScreenPagerAdapter mMainScreenPagerAdapter; - //Data - private int PAGE_COUNT = 3; - - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - Log.i(TAG, "MainScreenActivity: onCreate()"); - context = this; - //using sharedPreference to check whether this is using first time on not... - //even this app is starting first time than from here it will call the OnBoardActivity and updated the value - //even this app is already started than this screen will work... - - //from here it's started - - //setting the layout - setContentView(R.layout.activity_home); - - //checking sdk version and giving shadow effect - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { - findViewById(R.id.shadow_prelollipop).setVisibility(View.GONE); - } else { - findViewById(R.id.shadow_prelollipop).setVisibility(View.VISIBLE); - } - - - //Setting Custom Toolbar - setToolBar(); - - //Setting all 3 Customized Tabs - setUpCustomTabs(); - - //Setting the pagerListener - setPagerListener(); - - } - - //using to set custom design , actions and tabView into toolbar - private void setToolBar() { - toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - - LayoutInflater mLayoutInflater = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - //setting TabBarView - View customTabView = mLayoutInflater.inflate(R.layout.custom_tab_view, null); - mTabBarView = (TabBarView) customTabView.findViewById(R.id.customTabBar); - mTabBarView.setStripHeight(7); - - mTabBarView.setStripColor(getResources().getColor(R.color.white)); - - //setting the properties of ActionBar - - getSupportActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); - getSupportActionBar().setDisplayHomeAsUpEnabled(false); - getSupportActionBar().setHomeButtonEnabled(false); - - //Setting the Customized Toolbar into toolbar object - toolbar.addView(customTabView); - } - - - private void setUpCustomTabs() { - - //setting of ViewPager - mMainScreenPagerAdapter = new MainScreenPagerAdapter(getSupportFragmentManager()); - mViewPager = (CustomViewPager) findViewById(R.id.pager); - mViewPager.setAdapter(mMainScreenPagerAdapter); - - //Setting the CustomizedViewPager into Toolbar for tabOption - mTabBarView.setViewPager(mViewPager); - - - } - - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_main, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar card_stack_item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - if (id == R.id.action_settings) { - return true; - } - - return super.onOptionsItemSelected(item); - } - - - private void setPagerListener() { - mViewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { - @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - mTabBarView.onPageScrolled(position, positionOffset, positionOffsetPixels); - } - - @Override - public void onPageSelected(int position) { - Log.i(TAG, "Page: " + position); - mTabBarView.onPageSelected(position); - } - - @Override - public void onPageScrollStateChanged(int state) { - mTabBarView.onPageScrollStateChanged(state); - } - }); - } - - - // class is implemented with IconTabProvider Interface as well as extends with FragmentStateAdapter for ViewPager - //with different tabIcons - public class MainScreenPagerAdapter extends FragmentStatePagerAdapter implements TabBarView.IconTabProvider { - - - //Defining the array for Tab icons..which is going to call dynamically and load it into tabBar of toolbar - private int[] tab_icons = { - R.drawable.ic_connect, - R.drawable.ic_list, - R.drawable.ic_chat - }; - - public MainScreenPagerAdapter(FragmentManager fm) { - super(fm); - } - - - //this method is returning the ref of our fragments - @Override - public Fragment getItem(int pos) { - switch (pos) { - case 0: - return new Connect(); - case 1: - return new Discover(); - case 2: - return new Chat(); - default: - return null; - } - } - - //returning the number of pages - @Override - public int getCount() { - return PAGE_COUNT; - } - - - //this is TabBarView.IconTabProvider's method to return the position of icon to load into tabBar of Toolbar - @Override - public int getPageIconResId(int position) { - return tab_icons[position]; - } - - @Override - public CharSequence getPageTitle(int position) { - switch (position) { - case 0: - return "Connect"; - case 1: - return "Discover"; - case 2: - return "Chat"; - } - return null; - } - } -} \ No newline at end of file diff --git a/mobile/src/main/java/com/tinderview/adapters/CardStackAdapter.java b/mobile/src/main/java/com/tinderview/adapters/CardStackAdapter.java deleted file mode 100644 index 3126a55..0000000 --- a/mobile/src/main/java/com/tinderview/adapters/CardStackAdapter.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.tinderview.adapters; - -import android.content.Context; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.TextView; - -import com.tinderview.R; - - -/** - * Created by Aradh Pillaion 07/10/15. - */ - -/* -* cardStackAdapter which is going to hold list of information and setting it into CardStack -* -* */ -public class CardStackAdapter extends ArrayAdapter { - - - public CardStackAdapter(Context context, int resource) { - - super(context, 0); - } - - @Override - public View getView(int position, final View contentView, ViewGroup parent) { - //supply the layout for your card - TextView v = (TextView) (contentView.findViewById(R.id.helloText)); - v.setText(getItem(position)); - return contentView; - } - -} diff --git a/mobile/src/main/java/com/tinderview/animation/RippleAnimation.java b/mobile/src/main/java/com/tinderview/animation/RippleAnimation.java deleted file mode 100755 index 9a44dc2..0000000 --- a/mobile/src/main/java/com/tinderview/animation/RippleAnimation.java +++ /dev/null @@ -1,207 +0,0 @@ -package com.tinderview.animation; - -import android.animation.Animator; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.util.AttributeSet; -import android.view.View; -import android.view.animation.AccelerateDecelerateInterpolator; -import android.widget.RelativeLayout; - -import com.tinderview.R; - -import java.util.ArrayList; - -/** - * Created by Aradh Pillai on 14/10/15. - *

- * This class is creating for Background Ripple Animation - */ - -public class RippleAnimation extends RelativeLayout { - - private static final int DEFAULT_RIPPLE_COUNT = 6; - private static final int DEFAULT_DURATION_TIME = 3000; - private static final float DEFAULT_SCALE = 6.0f; - private static final int DEFAULT_FILL_TYPE = 0; - - private int rippleColor; - private float rippleStrokeWidth; - private float rippleRadius; - private int rippleDurationTime; - private int rippleAmount; - private int rippleDelay; - private float rippleScale; - private int rippleType; - private Paint paint; - private boolean animationRunning = false; - private AnimatorSet animatorSet; - private ArrayList animatorList; - private LayoutParams rippleParams; - private ArrayList rippleViewList = new ArrayList(); - - - //constructor with single parameter - public RippleAnimation(Context context) { - super(context); - } - - public RippleAnimation(Context context, AttributeSet attrs) { - super(context, attrs); - init(context, attrs); - } - - //Parameter Constructor - public RippleAnimation(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(context, attrs); - } - - //initialized and sets all the properties of RippleAnimation - private void init(final Context context, final AttributeSet attrs) { - if (isInEditMode()) - return; - - if (null == attrs) { - throw new IllegalArgumentException("Attributes should be provided to this view,"); - } - - //created typedArray and setting all properties to typedArray object - final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RippleAnimation); - - //it will return ripple color even we have specified into xml or else it will get the default ripple Color - rippleColor = typedArray.getColor(R.styleable.RippleAnimation_rb_color, getResources().getColor(R.color.rippleColor)); - - //getting rippleStrokeWidth from typedArray object and putting it into rippleStrokeWidth variable - rippleStrokeWidth = typedArray.getDimension(R.styleable.RippleAnimation_rb_strokeWidth, getResources().getDimension(R.dimen.rippleStrokeWidth)); - - //rippleRadius contains the default the specified radius even we specified or else it takes default radius...which is 64dp - rippleRadius = typedArray.getDimension(R.styleable.RippleAnimation_rb_radius, getResources().getDimension(R.dimen.rippleRadius)); - - //setting rippleAnimation duration is specified or else it takes default Duration Time. - rippleDurationTime = typedArray.getInt(R.styleable.RippleAnimation_rb_duration, DEFAULT_DURATION_TIME); - - //number of ripple if we have specified into design part or else default value it will take. - rippleAmount = typedArray.getInt(R.styleable.RippleAnimation_rb_rippleAmount, DEFAULT_RIPPLE_COUNT); - - //setting scale of ripple - rippleScale = typedArray.getFloat(R.styleable.RippleAnimation_rb_scale, DEFAULT_SCALE); - - //its getting the ripple type and putting it into rippleType variable. - rippleType = typedArray.getInt(R.styleable.RippleAnimation_rb_type, DEFAULT_FILL_TYPE); - - /** - * Recycles the TypedArray, to be re-used by a later caller. After calling - * this function you must not ever touch the typed array again. - * - * @throws RuntimeException if the TypedArray has already been recycled. - */ - typedArray.recycle(); - - rippleDelay = rippleDurationTime / rippleAmount; - - paint = new Paint();//creating an object of paint to make ripple based on the ripple type - paint.setAntiAlias(true); - if (rippleType == DEFAULT_FILL_TYPE) { - rippleStrokeWidth = 0; - paint.setStyle(Paint.Style.FILL); - } else - paint.setStyle(Paint.Style.STROKE); - paint.setColor(rippleColor); - - rippleParams = new LayoutParams((int) (2 * (rippleRadius + rippleStrokeWidth)), (int) (2 * (rippleRadius + rippleStrokeWidth))); - rippleParams.addRule(CENTER_IN_PARENT, TRUE);//setting ripple into center - - animatorSet = new AnimatorSet();// creates animationSet object - animatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); - animatorList = new ArrayList(); - - - /*based on the number of ripple this for loop create ripple View and put three different animation for this view.. - * - * */ - for (int i = 0; i < rippleAmount; i++) { - RippleView rippleView = new RippleView(getContext());//created ripple - addView(rippleView, rippleParams);//adding view - rippleViewList.add(rippleView);//putting this view into rippleViewList object - - - //creating objectAnimator object and setting some X Scale properties to object and putting it into animatorList - final ObjectAnimator scaleXAnimator = ObjectAnimator.ofFloat(rippleView, "ScaleX", 1.0f, rippleScale); - scaleXAnimator.setRepeatCount(ObjectAnimator.INFINITE); - scaleXAnimator.setRepeatMode(ObjectAnimator.RESTART); - scaleXAnimator.setStartDelay(i * rippleDelay); - scaleXAnimator.setDuration(rippleDurationTime); - animatorList.add(scaleXAnimator); - - //creating objectAnimator object and setting some Y Scale properties to object and putting it into animatorList - final ObjectAnimator scaleYAnimator = ObjectAnimator.ofFloat(rippleView, "ScaleY", 1.0f, rippleScale); - scaleYAnimator.setRepeatCount(ObjectAnimator.INFINITE); - scaleYAnimator.setRepeatMode(ObjectAnimator.RESTART); - scaleYAnimator.setStartDelay(i * rippleDelay); - scaleYAnimator.setDuration(rippleDurationTime); - animatorList.add(scaleYAnimator); - - //setting alpha of the animation and putting it into animatorList - final ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(rippleView, "Alpha", 1.0f, 0f); - alphaAnimator.setRepeatCount(ObjectAnimator.INFINITE); - alphaAnimator.setRepeatMode(ObjectAnimator.RESTART); - alphaAnimator.setStartDelay(i * rippleDelay); - alphaAnimator.setDuration(rippleDurationTime); - animatorList.add(alphaAnimator); - } - - //played animator - animatorSet.playTogether(animatorList); - } - - //used to start ripple Animation - public void startRippleAnimation() { - if (!isRippleAnimationRunning()) { - for (RippleView rippleView : rippleViewList) { - rippleView.setVisibility(VISIBLE); - } - animatorSet.start();//animation started - animationRunning = true;//setting value as true... this will support to check isRippleAnimation is running or not. - } - } - - public void stopRippleAnimation() { - if (isRippleAnimationRunning()) { - animatorSet.end(); - animationRunning = false; - } - } - - //stopping ripple Animation using end() method and setting boolean value false to animationRunning - - /*call this method to check whether animation is running or not. - * then basically it return the animationRunning boolean value. - * - * even we have stopped animation then it will return false value. - * or even we have not stopped animation then it will return true value. - */ - public boolean isRippleAnimationRunning() { - return animationRunning; - } - - //this class is using to draw the circle - private class RippleView extends View { - - public RippleView(Context context) { - super(context); - this.setVisibility(View.INVISIBLE); - } - - //it's drawing ripple based on radius value. - @Override - protected void onDraw(Canvas canvas) { - int radius = (Math.min(getWidth(), getHeight())) / 2; - canvas.drawCircle(radius, radius, radius - rippleStrokeWidth, paint); - } - } -} diff --git a/mobile/src/main/java/com/tinderview/cardstatck/animation/RelativeLayoutParamsEvaluator.java b/mobile/src/main/java/com/tinderview/cardstatck/animation/RelativeLayoutParamsEvaluator.java deleted file mode 100755 index 50c8cf5..0000000 --- a/mobile/src/main/java/com/tinderview/cardstatck/animation/RelativeLayoutParamsEvaluator.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.tinderview.cardstatck.animation; - -import android.animation.TypeEvaluator; -import android.widget.RelativeLayout.LayoutParams; - -/* -* when card discard or reverse then this animation class is calling -* and this class is overloading the evaluate method of TypeEvaluator class -* */ -public class RelativeLayoutParamsEvaluator implements TypeEvaluator { - - - @Override - public LayoutParams evaluate(float fraction, LayoutParams start, - LayoutParams end) { - - LayoutParams result = new LayoutParams(start); - result.leftMargin += ((end.leftMargin - start.leftMargin) * fraction); - result.rightMargin += ((end.rightMargin - start.rightMargin) * fraction); - result.topMargin += ((end.topMargin - start.topMargin) * fraction); - result.bottomMargin += ((end.bottomMargin - start.bottomMargin) * fraction); - return result; - } - -} diff --git a/mobile/src/main/java/com/tinderview/cardstatck/cardstack/CardAnimator.java b/mobile/src/main/java/com/tinderview/cardstatck/cardstack/CardAnimator.java deleted file mode 100755 index 4c21ef7..0000000 --- a/mobile/src/main/java/com/tinderview/cardstatck/cardstack/CardAnimator.java +++ /dev/null @@ -1,301 +0,0 @@ -package com.tinderview.cardstatck.cardstack; - -import android.animation.Animator; -import android.animation.Animator.AnimatorListener; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ValueAnimator; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.RelativeLayout; -import android.widget.RelativeLayout.LayoutParams; - -import com.tinderview.R; -import com.tinderview.activities.MainScreenActivity; -import com.tinderview.cardstatck.animation.RelativeLayoutParamsEvaluator; - -import java.util.ArrayList; -import java.util.HashMap; - - -/* -* CardAnimator class is used to give animation on Card eg: dislike and like animation of card -* dragging and dropping animation . -* setting view of Card and arrange it in the form of stack. -* shadow effect -* curve effect of card -* stack view -* */ - -public class CardAnimator { - private static final String DEBUG_TAG = "CardAnimator"; - private static final int REMOTE_DISTANCE = 1000; - public ArrayList mCardCollection; - private float mRotation; //dislike rotation value - private HashMap mLayoutsMap;//used to store the list of card's view - private RelativeLayout.LayoutParams[] mRemoteLayouts = new RelativeLayout.LayoutParams[4];//creating the four list of stack - private RelativeLayout.LayoutParams baseLayout; - private int mStackMargin = 21;//set the margin of cardstack - - - //passing list of cardStack view to mCardCollection. and jumping to setup method - public CardAnimator(ArrayList viewCollection) { - mCardCollection = viewCollection; - setup(); - - } - - - //calling to setup the each card based on view - private void setup() { - mLayoutsMap = new HashMap<>(); - - for (View v : mCardCollection) { - //setup basic card layout - RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) v.getLayoutParams(); - params.addRule(RelativeLayout.ALIGN_PARENT_TOP); - params.width = LayoutParams.MATCH_PARENT; - params.height = LayoutParams.MATCH_PARENT; - - v.setLayoutParams(params); - - } - - baseLayout = (RelativeLayout.LayoutParams) mCardCollection.get(0).getLayoutParams(); - baseLayout = new RelativeLayout.LayoutParams(baseLayout); - - initLayout();//calling this method to arrange all the card based on StackView - - for (View v : mCardCollection) { - RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) v.getLayoutParams(); - RelativeLayout.LayoutParams paramsCopy = new RelativeLayout.LayoutParams(params); - mLayoutsMap.put(v, paramsCopy); - } - - setupRemotes(); - - } - - /* finding the index of view and creating layoutParams using baseLayout object and those height and width setting to view - * using method of v.setLayoutParams - * and setting card scale size to give curve shape to card - * and moving card toward top of others card, and gives look of stack. - * */ - public void initLayout() { - int size = mCardCollection.size(); - for (View v : mCardCollection) { - int index = mCardCollection.indexOf(v); - if (index != 0) { - index -= 1; - } - LayoutParams params = new LayoutParams(baseLayout); - v.setLayoutParams(params); - - //calling scale method of CardUtils class to set the margin and than put layout into v - CardUtils.scale(v, -(size - index - 1) * 8); - - /*calling move method of CardUtils class which is going to move the card one by on based on the mStackMargin - * first index would be 0 so it won't move card and the based on the index it will multiply with the given margin - * and move card to down. - */ - CardUtils.move(v, index * mStackMargin, 0); - - v.setRotation(0);//setting the rotation of card as 0 - - //here calling 3 different shadow layout for each Card - //which will give shadow effect dynamically ... - if (index == 0)// setting 0 used to add different layout shadow... - v.setBackground(MainScreenActivity.context.getResources().getDrawable(R.drawable.card_stack_bg_shadow3)); - else if (index == 1) - v.setBackground(MainScreenActivity.context.getResources().getDrawable(R.drawable.card_stack_bg_shadow2)); - else if (index == 2) - v.setBackground(MainScreenActivity.context.getResources().getDrawable(R.drawable.card_stack_bg_shadow)); - } - } - - //setting the each card margins and putting view into array of mRemoteLayouts - private void setupRemotes() { - View topView = getTopView(); - mRemoteLayouts[0] = CardUtils.getMoveParams(topView, REMOTE_DISTANCE, -REMOTE_DISTANCE); - mRemoteLayouts[1] = CardUtils.getMoveParams(topView, REMOTE_DISTANCE, REMOTE_DISTANCE); - mRemoteLayouts[2] = CardUtils.getMoveParams(topView, -REMOTE_DISTANCE, -REMOTE_DISTANCE); - mRemoteLayouts[3] = CardUtils.getMoveParams(topView, -REMOTE_DISTANCE, REMOTE_DISTANCE); - } - - - //return the top view of stack. - private View getTopView() { - return mCardCollection.get(mCardCollection.size() - 1); - } - - - private void moveToBack(View child) { - final ViewGroup parent = (ViewGroup) child.getParent(); - if (null != parent) { - parent.removeView(child); - parent.addView(child, 0); - } - } - - //rearrange the mCardCollection list... - // - private void reorder() { - View temp = getTopView(); - moveToBack(temp); - - for (int i = (mCardCollection.size() - 1); i > 0; i--) { - - View current = mCardCollection.get(i - 1); - //current replace next - mCardCollection.set(i, current); - - - } - mCardCollection.set(0, temp); - - // temp = getTopView(); - - } - - //using to set the discard animation of CardStack into ArrayList - public void discard(int direction, final AnimatorListener al) { - AnimatorSet as = new AnimatorSet(); - ArrayList aCollection = new ArrayList<>(); - - - final View topView = getTopView(); - RelativeLayout.LayoutParams topParams = (RelativeLayout.LayoutParams) topView.getLayoutParams(); - RelativeLayout.LayoutParams layout = new RelativeLayout.LayoutParams(topParams); - ValueAnimator discardAnim = ValueAnimator.ofObject(new RelativeLayoutParamsEvaluator(), layout, mRemoteLayouts[direction]); - - discardAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator value) { - topView.setLayoutParams((LayoutParams) value.getAnimatedValue()); - } - }); - - discardAnim.setDuration(100);//setting the discard removed animation time in ms. - aCollection.add(discardAnim);//putting the discardAnimation to animation ArrayLIst - - for (int i = 0; i < mCardCollection.size(); i++) { - final View v = mCardCollection.get(i); - - if (v == topView) continue; - final View nv = mCardCollection.get(i + 1); - RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) v.getLayoutParams(); - RelativeLayout.LayoutParams endLayout = new RelativeLayout.LayoutParams(layoutParams); - ValueAnimator layoutAnim = ValueAnimator.ofObject(new RelativeLayoutParamsEvaluator(), endLayout, mLayoutsMap.get(nv)); - layoutAnim.setDuration(100); - layoutAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator value) { - v.setLayoutParams((LayoutParams) value.getAnimatedValue()); - } - }); - aCollection.add(layoutAnim); - } - - as.addListener(new AnimatorListenerAdapter() { - - - @Override - public void onAnimationEnd(Animator animation) { - reorder(); - if (al != null) { - al.onAnimationEnd(animation); - } - mLayoutsMap = new HashMap(); - for (View v : mCardCollection) { - RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) v.getLayoutParams(); - RelativeLayout.LayoutParams paramsCopy = new RelativeLayout.LayoutParams(params); - mLayoutsMap.put(v, paramsCopy); - } - - } - - }); - - - as.playTogether(aCollection); - as.start(); - - - } - - - //even card is dragged and not discarded by the user then again this card will come on the top of the stack.. - //this method will give the animation of reverse along with the reverse animation duration - // time in ms... which is I defined as 250 - - public void reverse(MotionEvent e1, MotionEvent e2) { - final View topView = getTopView(); - ValueAnimator rotationAnim = ValueAnimator.ofFloat(mRotation, 0f); - rotationAnim.setDuration(250); - rotationAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator v) { - topView.setRotation(((Float) (v.getAnimatedValue()))); - } - }); - - rotationAnim.start();//start the animation - - for (final View v : mCardCollection) { - RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) v.getLayoutParams(); - RelativeLayout.LayoutParams endLayout = new RelativeLayout.LayoutParams(layoutParams); - ValueAnimator layoutAnim = ValueAnimator.ofObject(new RelativeLayoutParamsEvaluator(), endLayout, mLayoutsMap.get(v)); - layoutAnim.setDuration(250); - layoutAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator value) { - v.setLayoutParams((LayoutParams) value.getAnimatedValue()); - } - }); - layoutAnim.start(); - } - - } - - - //dragging method...it's gives the position of object and does the secondary animation of CardStack - public void drag(MotionEvent e1, MotionEvent e2, float distanceX, - float distanceY) { - - View topView = getTopView(); - - float rotation_coefficient = 20f;// - - RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) topView.getLayoutParams(); - RelativeLayout.LayoutParams topViewLayouts = mLayoutsMap.get(topView); - int x_diff = (int) ((e2.getRawX() - e1.getRawX())); - int y_diff = (int) ((e2.getRawY() - e1.getRawY())); - - layoutParams.leftMargin = topViewLayouts.leftMargin + x_diff; - layoutParams.rightMargin = topViewLayouts.rightMargin - x_diff; - layoutParams.topMargin = topViewLayouts.topMargin + y_diff; - layoutParams.bottomMargin = topViewLayouts.bottomMargin - y_diff; - - mRotation = (x_diff / rotation_coefficient); - topView.setRotation(mRotation); - topView.setLayoutParams(layoutParams); - - //animate secondary views. - for (View v : mCardCollection) { - int index = mCardCollection.indexOf(v); - if (v != getTopView() && index != 0) { - LayoutParams l = CardUtils.scaleFrom(v, mLayoutsMap.get(v), (int) (Math.abs(x_diff) * 0.05)); - CardUtils.moveFrom(v, l, 0, (int) (Math.abs(x_diff) * 0.1)); - } - } - } - - //using to set the margin into cardStack - public void setStackMargin(int margin) { - mStackMargin = margin;//passing the value to mStackMargin - initLayout(); //and then calling this method to set the margin into view. - } - - -} diff --git a/mobile/src/main/java/com/tinderview/cardstatck/cardstack/CardStack.java b/mobile/src/main/java/com/tinderview/cardstatck/cardstack/CardStack.java deleted file mode 100755 index 3dabc03..0000000 --- a/mobile/src/main/java/com/tinderview/cardstatck/cardstack/CardStack.java +++ /dev/null @@ -1,297 +0,0 @@ -package com.tinderview.cardstatck.cardstack; - -import java.util.ArrayList; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.content.Context; -import android.util.AttributeSet; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.FrameLayout; -import android.widget.RelativeLayout; -import android.database.DataSetObserver; - - -public class CardStack extends RelativeLayout { - ArrayList viewCollection = new ArrayList(); - private int mIndex = 0; - private int mNumVisible = 4; - private ArrayAdapter mAdapter; - private OnTouchListener mOnTouchListener; - //private Queue mIdleStack = new Queue; - private CardAnimator mCardAnimator; - private CardEventListener mEventListener = new DefaultStackEventListener(10); - private int mContentResource = 0; - - //method used to descard the top view based on direction...which is going to use by other widget - //eg: using button we can call this method and we can pass the direction as parameter eg: 0 or 2 to discard the card... - private DataSetObserver mOb = new DataSetObserver() { - @Override - public void onChanged() { - reset(false); - } - }; - - - //return the current index of card - - //only necessary when I need the attrs from xml, this will be used when inflating layout - public CardStack(Context context, AttributeSet attrs) { - super(context, attrs); - - - //get attrs assign minVisiableNum - for (int i = 0; i < mNumVisible; i++) { - addContainerViews(); - } - setupAnimation(); - } - - public CardStack(Context context) { - super(context); - } - - public void discardTop(final int direction) { - mCardAnimator.discard(direction, new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator arg0) { - mCardAnimator.initLayout(); - mIndex++; - mEventListener.discarded(mIndex, direction); - - //mIndex = mIndex%mAdapter.getCount(); - loadLast(); - - viewCollection.get(0).setOnTouchListener(null); - viewCollection.get(viewCollection.size() - 1) - .setOnTouchListener(mOnTouchListener); - } - }); - } - - public int getCurrIndex() { - //sync? - return mIndex; - } - - private void addContainerViews() { - FrameLayout v = new FrameLayout(getContext()); - viewCollection.add(v); - addView(v); - } - - public void setStackMargin(int margin) { - mCardAnimator.setStackMargin(margin); - mCardAnimator.initLayout(); - } - - public void setContentResource(int res) { - mContentResource = res; - } - - //when need to reset the card into stack - public void reset(boolean resetIndex) { - if (resetIndex) mIndex = 0; - removeAllViews(); - viewCollection.clear(); - for (int i = 0; i < mNumVisible; i++) { - addContainerViews(); - } - setupAnimation(); - loadData(); - } - - public void setVisibleCardNum(int visiableNum) { - mNumVisible = visiableNum; - reset(false); - } - - - //setting the card animator and putting GestureDetectorListener and overriding the methods of DragListener interface - //using this method can get to know the dragged card location - //see the animation effect on that selected card - - public void setThreshold(int t) { - mEventListener = new DefaultStackEventListener(t); - } - - public void setListener(CardEventListener cel) { - mEventListener = cel; - } - - - //ArrayList - - private void setupAnimation() { - final View cardView = viewCollection.get(viewCollection.size() - 1); - mCardAnimator = new CardAnimator(viewCollection); //creating an object of cardAnimator - mCardAnimator.initLayout(); //initialize the cardAnimator using object - - final DragGestureDetector dd = new DragGestureDetector(CardStack.this.getContext(), new DragGestureDetector.DragListener() { - - - //when drag of card will start this method invoke first - @Override - public boolean onDragStart(MotionEvent e1, MotionEvent e2, - float distanceX, float distanceY) { - mCardAnimator.drag(e1, e2, distanceX, distanceY); - return true; - } - - //user dragging the card continue then this method will invoke - @Override - public boolean onDragContinue(MotionEvent e1, MotionEvent e2, - float distanceX, float distanceY) { - float x1 = e1.getRawX(); - float y1 = e1.getRawY(); - float x2 = e2.getRawX(); - float y2 = e2.getRawY(); - //float distance = CardUtils.distance(x1,y1,x2,y2); - final int direction = CardUtils.direction(x1, y1, x2, y2); - mCardAnimator.drag(e1, e2, distanceX, distanceY); - mEventListener.swipeContinue(direction, Math.abs(x2 - x1), Math.abs(y2 - y1)); - return true; - } - - /*when user dum the card then this method will invoke - * here it will check the distance between the direction and the distance of the card - *and based on that this method discard or reverse the card - */ - @Override - public boolean onDragEnd(MotionEvent e1, MotionEvent e2) { - //reverse(e1,e2); - float x1 = e1.getRawX(); - float y1 = e1.getRawY(); - float x2 = e2.getRawX(); - float y2 = e2.getRawY(); - Log.d("onDragEndMethod", "dis:s"); - float distance = CardUtils.distance(x1, y1, x2, y2); - final int direction = CardUtils.direction(x1, y1, x2, y2); - Log.d("onDragEndMethod", "dis: " + distance + " direction: " + direction); - boolean discard = mEventListener.swipeEnd(direction, distance); - if (discard) { - Log.d("onDragEndMethod", "deleted"); - mCardAnimator.discard(direction, new AnimatorListenerAdapter() { - - @Override - public void onAnimationEnd(Animator arg0) { - mCardAnimator.initLayout(); - mIndex++; - mEventListener.discarded(mIndex, direction); - - //mIndex = mIndex%mAdapter.getCount(); - loadLast(); - - viewCollection.get(0).setOnTouchListener(null); - viewCollection.get(viewCollection.size() - 1) - .setOnTouchListener(mOnTouchListener); - } - - }); - } else { - Log.d("onDragEndMethod", "reverse"); - mCardAnimator.reverse(e1, e2); - } - return true; - } - - @Override - public boolean onTapUp() { - mEventListener.topCardTapped(); - return true; - } - } - ); - - mOnTouchListener = new OnTouchListener() { - private static final String DEBUG_TAG = "MotionEvents"; - - @Override - public boolean onTouch(View arg0, MotionEvent event) { - dd.onTouchEvent(event); - return true; - } - }; - cardView.setOnTouchListener(mOnTouchListener); - } - - //used to set Adapter - public void setAdapter(final ArrayAdapter adapter) { - if (mAdapter != null) { - mAdapter.unregisterDataSetObserver(mOb); - } - mAdapter = adapter; - adapter.registerDataSetObserver(mOb); - - loadData(); - } - - //call to load data based on index - //this method will put Gone or visible property to view... - private void loadData() { - for (int i = mNumVisible - 1; i >= 0; i--) { - ViewGroup parent = (ViewGroup) viewCollection.get(i); - int index = (mIndex + mNumVisible - 1) - i; - if (index > mAdapter.getCount() - 1) { - parent.setVisibility(View.GONE); - } else { - View child = mAdapter.getView(index, getContentView(), this); - parent.addView(child); - parent.setVisibility(View.VISIBLE); - } - } - } - - //returning a new view even - private View getContentView() { - View contentView = null; - if (mContentResource != 0) { - LayoutInflater lf = LayoutInflater.from(getContext()); - contentView = lf.inflate(mContentResource, null); - } - return contentView; - - } - - private void loadLast() { - ViewGroup parent = (ViewGroup) viewCollection.get(0); - - int lastIndex = (mNumVisible - 1) + mIndex; - if (lastIndex > mAdapter.getCount() - 1) { - parent.setVisibility(View.GONE); //hiding the top view and returning - return; - } - - View child = mAdapter.getView(lastIndex, getContentView(), parent); - parent.removeAllViews();// remove all previous view - parent.addView(child); // and then adding new - } - - //return the size of stack - public int getStackSize() { - return mNumVisible; - } - - public interface CardEventListener { - //section - // 0 | 1 - //-------- - // 2 | 3 - // swipe distance, most likely be used with height and width of a view ; - - boolean swipeEnd(int section, float distance); - - boolean swipeStart(int section, float distance); - - boolean swipeContinue(int section, float distanceX, float distanceY); - - void discarded(int mIndex, int direction); - - void topCardTapped(); - } -} diff --git a/mobile/src/main/java/com/tinderview/cardstatck/cardstack/CardUtils.java b/mobile/src/main/java/com/tinderview/cardstatck/cardstack/CardUtils.java deleted file mode 100755 index 1dd5fb1..0000000 --- a/mobile/src/main/java/com/tinderview/cardstatck/cardstack/CardUtils.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.tinderview.cardstatck.cardstack; - -import android.util.Log; -import android.view.View; -import android.widget.RelativeLayout; -import android.widget.RelativeLayout.LayoutParams; - - -/* -* CardUtils is using for set the card scale , get the selected layout param, scaleFrom, moveFrom and direction of card. -* All these method are static which means it is going to access from some others classes and it returns the value based on the -* passed parameter. -* -* */ -public class CardUtils { - final static int DIRECTION_TOP_LEFT = 0; - final static int DIRECTION_TOP_RIGHT = 1; - final static int DIRECTION_BOTTOM_LEFT = 2; - final static int DIRECTION_BOTTOM_RIGHT = 3; - - //it remove some pixels from the each sides of given view and then set that layout to view using setLayoutParams method. - public static void scale(View v, int pixel) { - RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) v.getLayoutParams(); - params.leftMargin -= pixel; - params.rightMargin -= pixel; - params.topMargin -= pixel; - params.bottomMargin -= pixel; - v.setLayoutParams(params); - } - - - //setting the layout margin dynamically to make it as stack view and returning the layoutParams object. - public static LayoutParams getMoveParams(View v, int upDown, int leftRight) { - RelativeLayout.LayoutParams original = (RelativeLayout.LayoutParams) v.getLayoutParams(); - //RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(original); - RelativeLayout.LayoutParams params = cloneParams(original); - params.leftMargin += leftRight; - params.rightMargin -= leftRight; - Log.d("moving...", params.leftMargin + ""); - - params.topMargin -= upDown; - params.bottomMargin += upDown; - return params; - } - - //used to move the card based on parameter, from this method again it's calling the getMoveParams(v,upDown,leftRight) methods - public static void move(View v, int upDown, int leftRight) { - RelativeLayout.LayoutParams params = getMoveParams(v, upDown, leftRight); - v.setLayoutParams(params); - } - - - //setting the scale of card dynamically based on the parameters - public static LayoutParams scaleFrom(View v, LayoutParams params, int pixel) { - Log.d("pixel", "onScroll: " + pixel); - params = cloneParams(params); - params.leftMargin -= pixel; - params.rightMargin -= pixel; - params.topMargin -= pixel; - params.bottomMargin -= pixel; - Log.d("pixel", "onScroll: " + pixel); - v.setLayoutParams(params); - - return params; - } - - - //setting the card margin based on the values using leftRight and Updown variables - //this method is calling for secondary view - - public static LayoutParams moveFrom(View v, LayoutParams params, int leftRight, int upDown) { - params = cloneParams(params); - params.leftMargin += leftRight; - params.rightMargin -= leftRight; - params.topMargin -= upDown; - params.bottomMargin += upDown; - v.setLayoutParams(params); - - return params; - } - - //a copy method for RelativeLayout.LayoutParams for backward compatibility - public static RelativeLayout.LayoutParams cloneParams(RelativeLayout.LayoutParams params) { - RelativeLayout.LayoutParams copy = new RelativeLayout.LayoutParams(params.width, params.height); - copy.leftMargin = params.leftMargin; - copy.topMargin = params.topMargin; - copy.rightMargin = params.rightMargin; - copy.bottomMargin = params.bottomMargin; - int[] rules = params.getRules(); - for (int i = 0; i < rules.length; i++) { - copy.addRule(i, rules[i]); - } - copy.setMarginStart(params.getMarginStart()); - copy.setMarginEnd(params.getMarginEnd()); - - return copy; - } - - - //giving the distance in between the cards - public static float distance(float x1, float y1, float x2, float y2) { - Log.d("Data: ", "" + Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))); - return (float) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); - } - - - // in which direction user dum card, that location this method will return ... - // we can get to know that whether user has liked or dislike the card... - //eg: - // 0 | 1 - //-------- - // 2 | 3 - // dislike value : even this method return the 0 or 2 - // like value: 1 or 3 - - public static int direction(float x1, float y1, float x2, float y2) { - if (x2 > x1) {//RIGHT - if (y2 > y1) {//BOTTOM - return DIRECTION_BOTTOM_RIGHT; - } else {//TOP - return DIRECTION_TOP_RIGHT; - } - } else {//LEFT - if (y2 > y1) {//BOTTOM - return DIRECTION_BOTTOM_LEFT; - } else {//TOP - return DIRECTION_TOP_LEFT; - } - } - } - - -} diff --git a/mobile/src/main/java/com/tinderview/cardstatck/cardstack/DefaultStackEventListener.java b/mobile/src/main/java/com/tinderview/cardstatck/cardstack/DefaultStackEventListener.java deleted file mode 100755 index 270472a..0000000 --- a/mobile/src/main/java/com/tinderview/cardstatck/cardstack/DefaultStackEventListener.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.tinderview.cardstatck.cardstack; - - -/*this is using to put the listener event into cardStack - - */ -public class DefaultStackEventListener implements CardStack.CardEventListener { - - private float mThreshold = 0; - - public DefaultStackEventListener(int i) { - mThreshold = i; - } - - @Override - public boolean swipeEnd(int section, float distance) { - return distance > mThreshold; - } - - @Override - public boolean swipeStart(int section, float distance) { - - return true; - } - - @Override - public boolean swipeContinue(int section, float distanceX, float distanceY) { - return true; - } - - @Override - public void discarded(int mIndex, int direction) { - - } - - @Override - public void topCardTapped() { - - } - - -} diff --git a/mobile/src/main/java/com/tinderview/cardstatck/cardstack/DragGestureDetector.java b/mobile/src/main/java/com/tinderview/cardstatck/cardstack/DragGestureDetector.java deleted file mode 100755 index ab23864..0000000 --- a/mobile/src/main/java/com/tinderview/cardstatck/cardstack/DragGestureDetector.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.tinderview.cardstatck.cardstack; - -import android.content.Context; -import android.support.v4.view.GestureDetectorCompat; -import android.support.v4.view.MotionEventCompat; -import android.util.Log; -import android.view.GestureDetector; -import android.view.MotionEvent; - - -//detect both tap and drag based on the gesture -/* -* in this class DragListener interface is defined which is going to use to return the boolean value based on the scope of CardStack -* whether card is start, dragging , dum, etc. -* -* -* -* */ - - -public class DragGestureDetector { - public static String DEBUG_TAG = "DragGestureDetector"; - GestureDetectorCompat mGestrueDetector; - DragListener mListener; - private boolean mStarted = false; - private boolean mDown = false; - - private MotionEvent mOriginalEvent; - - public DragGestureDetector(Context context, DragListener myDragListener) { - mGestrueDetector = new GestureDetectorCompat(context, new MyGestureListener()); - mListener = myDragListener; - } - - public void onTouchEvent(MotionEvent event) { - mGestrueDetector.onTouchEvent(event); - int action = MotionEventCompat.getActionMasked(event); - switch (action) { - case (MotionEvent.ACTION_UP): - Log.d(DEBUG_TAG, "Action was UP " + mStarted); - if (mStarted) { - mListener.onDragEnd(mOriginalEvent, event); - mStarted = false; - } - break; - - case (MotionEvent.ACTION_DOWN): - Log.d(DEBUG_TAG, "Action was down " + mStarted); - - - if (mDown) { - mListener.onDragEnd(mOriginalEvent, event); - mDown = false; - - } - break; - case (MotionEvent.ACTION_SCROLL): - Log.d(DEBUG_TAG, "Scroll " + mStarted); - if (mStarted) { - mListener.onDragEnd(mOriginalEvent, event); - - } - // break; - - //need to set this, quick tap will not generate drap event, so the - //originalEvent may be null for case action_up - //which lead to null pointer - mOriginalEvent = event; - - } - } - - public interface DragListener { - boolean onDragStart(MotionEvent e1, MotionEvent e2, float distanceX, - float distanceY); - - boolean onDragContinue(MotionEvent e1, MotionEvent e2, float distanceX, - float distanceY); - - boolean onDragEnd(MotionEvent e1, MotionEvent e2); - - boolean onTapUp(); - } - - class MyGestureListener extends GestureDetector.SimpleOnGestureListener { - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, - float distanceY) { - if (mListener == null) { - Log.d(DEBUG_TAG, "NULL"); - return true; - } - if (mStarted == false) { - Log.d(DEBUG_TAG, "onDragStart " + mStarted); - mListener.onDragStart(e1, e2, distanceX, distanceY); - mStarted = true; - } else { - Log.d(DEBUG_TAG, "onDragContinue " + mStarted); - mListener.onDragContinue(e1, e2, distanceX, distanceY); - //mOriginalEvent = e1; - return true; - } - mOriginalEvent = e1; - return true; - } - - @Override - public boolean onSingleTapUp(MotionEvent e) { - Log.d(DEBUG_TAG, "OnSingTapUp" + mStarted); - return mListener.onTapUp(); - } - } - - -} diff --git a/mobile/src/main/java/com/tinderview/fragments/Chat.java b/mobile/src/main/java/com/tinderview/fragments/Chat.java deleted file mode 100755 index 1a35cbb..0000000 --- a/mobile/src/main/java/com/tinderview/fragments/Chat.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.tinderview.fragments;// Created by Sanat Dutta on 2/17/2015. - -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.tinderview.R; - -/** - * Created by Aradh Pillai on 1/10/15. - */ -/* -* this fragment is using for chat tab option -* */ -public class Chat extends Fragment { - - private String TAG = "fragment_chat"; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_chat, container, false); - - Log.i(TAG, "fragment_chat: onCreateView"); - - return view; - } -} diff --git a/mobile/src/main/java/com/tinderview/fragments/Connect.java b/mobile/src/main/java/com/tinderview/fragments/Connect.java deleted file mode 100755 index 390f1fc..0000000 --- a/mobile/src/main/java/com/tinderview/fragments/Connect.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.tinderview.fragments; - -import android.os.Bundle; -import android.os.Handler; -import android.support.v4.app.Fragment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.tinderview.R; -import com.tinderview.adapters.CardStackAdapter; -import com.tinderview.animation.RippleAnimation; -import com.tinderview.cardstatck.cardstack.CardStack; -import com.tinderview.cardstatck.cardstack.DefaultStackEventListener; - -/** - * Created by Aradh Pillai on 2/10/15. - */ - -/* -* this fragment class is using to show the list of connection into stackCard based on users location -* and user will have option to discard and like the connection. -* -* -* */ -public class Connect extends Fragment { - RippleAnimation rippleBackground1, rippleBackground2; - - CardStack cardStack; - //this class is using for swipe the AdapterView - CardStackAdapter mCardAdapter; - - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_connect, container, false); - - init(view);// to initialize widgets - doRippleBackground(); //start ripple background work.. - return view; - } - - private void init(View view) { - //root ripple background initialization - rippleBackground1 = (RippleAnimation) view.findViewById(R.id.content); - - //child ripple background initialization - // rippleBackground2 = (RippleBackground) view.findViewById(R.id.content2); - - //cardStack initialization - cardStack = (CardStack) view.findViewById(R.id.frame); - - //at begin setting rippleBackground visibility as VISIBLE and setting CardStack visibility as GONE - rippleBackground1.setVisibility(View.VISIBLE); - cardStack.setVisibility(View.GONE); - - //creating adapter - mCardAdapter = new CardStackAdapter(getActivity().getApplicationContext(), 0); - } - - public void doRippleBackground() { - - - //start ripple background animations - startAnimation(); - - //handler created to handle cardStack as well as timer... - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - - callCardStack(); - } - }, 8000); - - } - - //start the background ripple animation... - private void startAnimation() { - //if it's not running - if (!rippleBackground1.isRippleAnimationRunning()) { - rippleBackground1.startRippleAnimation();//start root ripple animation - // rippleBackground2.startRippleAnimation();//start child ripple animation - } - } - - //this method will stop background ripple animation. if it's running. - private void stopAnimation() { - if (rippleBackground1.isRippleAnimationRunning()) { - rippleBackground1.stopRippleAnimation(); - // rippleBackground2.stopRippleAnimation(); - } - } - - //cardStack view will set it as visible and load the information into stack. - public void callCardStack() { - - cardStack.setVisibility(View.VISIBLE); - rippleBackground1.setVisibility(View.GONE); - - stopAnimation();//start the ripple background animation. - - //Setting Resource of CardStack - cardStack.setContentResource(R.layout.card_stack_item); - - //Adding 30 dummy info for CardStack - for (int i = 0; i <= 30; i++) - mCardAdapter.add("" + i); - cardStack.setAdapter(mCardAdapter); - - //Setting Listener and passing distance as a parameter , - //based on the distance card will discard - //if dragging card distance would be more than specified distance(100) then card will discard or else card will reverse on same position. - cardStack.setListener(new DefaultStackEventListener(300)); - - } -} diff --git a/mobile/src/main/java/com/tinderview/fragments/Discover.java b/mobile/src/main/java/com/tinderview/fragments/Discover.java deleted file mode 100755 index df269b3..0000000 --- a/mobile/src/main/java/com/tinderview/fragments/Discover.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.tinderview.fragments;// Created by Sanat Dutta on 2/17/2015. - -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.tinderview.R; - -/** - * Created by Aradh Pillai on 1/10/15. - */ -public class Discover extends Fragment { - - private String TAG = "fragment_discover"; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_discover, container, false); - - Log.i(TAG, "fragment_discover: onCreateView"); - - return view; - } -} diff --git a/mobile/src/main/java/com/tinderview/tabbarview/CheatSheet.java b/mobile/src/main/java/com/tinderview/tabbarview/CheatSheet.java deleted file mode 100755 index f7dcf04..0000000 --- a/mobile/src/main/java/com/tinderview/tabbarview/CheatSheet.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.tinderview.tabbarview; - -import android.content.Context; -import android.graphics.Rect; -import android.text.TextUtils; -import android.view.Gravity; -import android.view.View; -import android.widget.Toast; - -/** - * Helper class for showing cheat sheets (tooltips) for icon-only UI elements on long-press. This is - * already default platform behavior for icon-only {@link android.app.ActionBar} items and tabs. - * This class provides this behavior for any other such UI element. - */ -public class CheatSheet { - /** - * The estimated height of a toast, in dips (density-independent pixels). This is used to - * determine whether or not the toast should appear above or below the UI element. - */ - private static final int ESTIMATED_TOAST_HEIGHT_DIPS = 48; - - /** - * Sets up a cheat sheet (tooltip) for the given view by setting its {@link - * View.OnLongClickListener}. When the view is long-pressed, a {@link Toast} with - * the view's {@link View#getContentDescription() content description} will be - * shown either above (default) or below the view (if there isn't room above it). - * - * @param view The view to add a cheat sheet for. - */ - public static void setup(View view) { - view.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View view) { - return showCheatSheet(view, view.getContentDescription()); - } - }); - } - - /** - * Sets up a cheat sheet (tooltip) for the given view by setting its {@link - * View.OnLongClickListener}. When the view is long-pressed, a {@link Toast} with - * the given text will be shown either above (default) or below the view (if there isn't room - * above it). - * - * @param view The view to add a cheat sheet for. - * @param textResId The string resource containing the text to show on long-press. - */ - public static void setup(View view, final int textResId) { - view.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View view) { - return showCheatSheet(view, view.getContext().getString(textResId)); - } - }); - } - - /** - * Sets up a cheat sheet (tooltip) for the given view by setting its {@link - * View.OnLongClickListener}. When the view is long-pressed, a {@link Toast} with - * the given text will be shown either above (default) or below the view (if there isn't room - * above it). - * - * @param view The view to add a cheat sheet for. - * @param text The text to show on long-press. - */ - public static void setup(View view, final CharSequence text) { - view.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View view) { - return showCheatSheet(view, text); - } - }); - } - - /** - * Removes the cheat sheet for the given view by removing the view's {@link - * View.OnLongClickListener}. - * - * @param view The view whose cheat sheet should be removed. - */ - public static void remove(final View view) { - view.setOnLongClickListener(null); - } - - /** - * Internal helper method to show the cheat sheet toast. - */ - private static boolean showCheatSheet(View view, CharSequence text) { - if (TextUtils.isEmpty(text)) { - return false; - } - - final int[] screenPos = new int[2]; // origin is device display - final Rect displayFrame = new Rect(); // includes decorations (e.g. status bar) - view.getLocationOnScreen(screenPos); - view.getWindowVisibleDisplayFrame(displayFrame); - - final Context context = view.getContext(); - final int viewWidth = view.getWidth(); - final int viewHeight = view.getHeight(); - final int viewCenterX = screenPos[0] + viewWidth / 2; - final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; - final int estimatedToastHeight = (int) (ESTIMATED_TOAST_HEIGHT_DIPS - * context.getResources().getDisplayMetrics().density); - - Toast cheatSheet = Toast.makeText(context, text, Toast.LENGTH_SHORT); - boolean showBelow = screenPos[1] < estimatedToastHeight; - if (showBelow) { - // Show below - // Offsets are after decorations (e.g. status bar) are factored in - cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, - viewCenterX - screenWidth / 2, - screenPos[1] - displayFrame.top + viewHeight); - } else { - // Show above - // Offsets are after decorations (e.g. status bar) are factored in - // NOTE: We can't use Gravity.BOTTOM because when the keyboard is up - // its height isn't factored in. - cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, - viewCenterX - screenWidth / 2, - screenPos[1] - displayFrame.top - estimatedToastHeight); - } - - cheatSheet.show(); - return true; - } -} diff --git a/mobile/src/main/java/com/tinderview/tabbarview/TabBarView.java b/mobile/src/main/java/com/tinderview/tabbarview/TabBarView.java deleted file mode 100755 index 1f34ddf..0000000 --- a/mobile/src/main/java/com/tinderview/tabbarview/TabBarView.java +++ /dev/null @@ -1,297 +0,0 @@ -package com.tinderview.tabbarview; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.support.v4.view.ViewPager; -import android.support.v4.view.ViewPager.OnPageChangeListener; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewTreeObserver.OnGlobalLayoutListener; -import android.widget.LinearLayout; - -import com.tinderview.R; - -/* -* created by Aradh Pillai on 1.10.15 -* this class is inheriting features of LinearLayout -* using this it's creating TabBarView dynamically -* -* -* */ - -public class TabBarView extends LinearLayout { - //strip height has defined here - private static final int STRIP_HEIGHT = 6; - public static int mSelectedTab = 0; - public static int tabCount;//hold the number of tabs - public static int a; - public final Paint mPaint; - //this pager will hold the instance of viewPager - //and rearrange the pages using the object - public ViewPager pager; - public OnPageChangeListener delegatePageListener; - // TabView class objects - private TabView tabView1, tabView2, tabView3; - private int mStripHeight; - private float mOffset = 0f; - private View child; - - private View nextChild; - - public TabBarView(Context context) { - this(context, null); - } - - public TabBarView(Context context, AttributeSet attrs) { - this(context, attrs, R.attr.actionBarTabBarStyle); - } - - public TabBarView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.MATCH_PARENT)); - - setWillNotDraw(false); - - mPaint = new Paint(); - mPaint.setColor(Color.WHITE); - - mPaint.setAntiAlias(false); - - mStripHeight = (int) (STRIP_HEIGHT * getResources().getDisplayMetrics().density + .5f); - } - - //use to set StripColor - public void setStripColor(int color) { - if (mPaint.getColor() != color) { - mPaint.setColor(color); - invalidate(); - } - } - - //Use to set the height of strip - public void setStripHeight(int height) { - if (mStripHeight != height) { - mStripHeight = height; - invalidate(); - } - } - - //using to set the tab based on Tabindex which is need to pass as parameter. - public void setSelectedTab(int tabIndex) { - if (tabIndex < 0) { - tabIndex = 0; - } - final int childCount = getChildCount(); - if (tabIndex >= childCount) { - tabIndex = childCount - 1; - } - if (mSelectedTab != tabIndex) { - mSelectedTab = tabIndex; - invalidate(); - } - } - - public void setOffset(int position, float offset) { - if (mOffset != offset) { - mOffset = offset; - invalidate(); - } - } - - //this method is using to draw the strip - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - // Draw the strip manually - child = getChildAt(mSelectedTab); - int height = getHeight(); - if (child != null) { - float left = child.getLeft(); - float right = child.getRight(); - if (mOffset > 0f && mSelectedTab < tabCount - 1) { - nextChild = getChildAt(mSelectedTab + 1); - if (nextChild != null) { - final float nextTabLeft = nextChild.getLeft(); - final float nextTabRight = nextChild.getRight(); - left = (mOffset * nextTabLeft + (1f - mOffset) * left); - right = (mOffset * nextTabRight + (1f - mOffset) * right); - } - } - canvas.drawRect(left, height - mStripHeight, right, height, mPaint); - } - } - - //this method is used to set the pager reference to this.pager object and call the notifyDataSetChanged() method - // - public void setViewPager(ViewPager pager) { - this.pager = pager; - - if (pager.getAdapter() == null) { - throw new IllegalStateException("ViewPager does not have adapter instance."); - } - - notifyDataSetChanged(); - } - - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - - mSelectedTab = position; - mOffset = positionOffset; - - invalidate(); - - if (delegatePageListener != null) { - delegatePageListener.onPageScrolled(position, positionOffset, positionOffsetPixels); - } - - //based on the position setting tab alpha as 1 if it's selected or others set is as 0.5f - if (position == 0) { - tabView1.setAlpha(1.0f); - tabView2.setAlpha(0.5f); - tabView3.setAlpha(0.5f); - - } else if (position == 1) { - tabView1.setAlpha(0.5f); - tabView2.setAlpha(1.0f); - tabView3.setAlpha(0.5f); - - } else if (position == 2) { - tabView1.setAlpha(0.5f); - tabView2.setAlpha(0.5f); - tabView3.setAlpha(1.0f); - - } - } - - public void onPageScrollStateChanged(int state) { - if (state == ViewPager.SCROLL_STATE_IDLE) { - - } - - if (delegatePageListener != null) { - delegatePageListener.onPageScrollStateChanged(state); - } - } - - public void onPageSelected(int position) { - if (delegatePageListener != null) { - delegatePageListener.onPageSelected(position); - } - } - - // it will remove all previous views and creating new view for tab - public void notifyDataSetChanged() { - - int resId; - - this.removeAllViews(); - - tabCount = pager.getAdapter().getCount(); - - for (int i = 0; i < tabCount; i++) { - - resId = ((IconTabProvider) pager.getAdapter()).getPageIconResId(i); - - if (getResources().getConfiguration().orientation == 1) - addTabViewP(i, pager.getAdapter().getPageTitle(i).toString(), resId); - else - addTabViewL(i, pager.getAdapter().getPageTitle(i).toString(), resId); - } - - //removing swiping animation effect from viewPager - - - getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { - - @SuppressLint("NewApi") - @Override - public void onGlobalLayout() { - - getViewTreeObserver().removeOnGlobalLayoutListener(this); - - mSelectedTab = pager.getCurrentItem(); - - } - }); - - } - - //used to set the properties for landscape screen like icon,text and click-event into tab and then add this tab into View using addView(tab) method - private void addTabViewL(final int i, String string, int pageIconResId) { - // TODO Auto-generated method stub - TabView tab = new TabView(getContext()); -// tab.setIcon(pageIconResId); - tab.setText(string, pageIconResId); - tab.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - pager.setCurrentItem(i, false); - } - }); - - this.addView(tab); - } - - private void addTabViewP(final int i, final String string, int pageIconResId) { - // TODO Auto-generated method stub - - if (i == 0) { - tabView1 = new TabView(getContext()); - - tabView1.setIcon(pageIconResId); - tabView1.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - pager.setCurrentItem(i); - - - } - }); - CheatSheet.setup(tabView1, string); - this.addView(tabView1); - } else if (i == 1) { - tabView2 = new TabView(getContext()); - tabView2.setIcon(pageIconResId); - tabView2.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - pager.setCurrentItem(i); - - } - }); - CheatSheet.setup(tabView2, string); - this.addView(tabView2); - } else if (i == 2) { - tabView3 = new TabView(getContext()); - tabView3.setIcon(pageIconResId); - tabView3.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - pager.setCurrentItem(i); - - } - }); - CheatSheet.setup(tabView3, string); - this.addView(tabView3); - } - - - } - - //used to set the properties for portrait screen like icon and click-event into tabView objects - // and then add this tab object into View using addView(tabView1) method - //here we setup the tabs properties - - public void setOnPageChangeListener(OnPageChangeListener listener) { - this.delegatePageListener = listener; - } - - public interface IconTabProvider { - int getPageIconResId(int position); - } -} \ No newline at end of file diff --git a/mobile/src/main/java/com/tinderview/tabbarview/TabView.java b/mobile/src/main/java/com/tinderview/tabbarview/TabView.java deleted file mode 100755 index 557a802..0000000 --- a/mobile/src/main/java/com/tinderview/tabbarview/TabView.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.tinderview.tabbarview; - -import android.content.Context; -import android.graphics.Color; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.util.TypedValue; -import android.view.Gravity; -import android.view.View; -import android.widget.ImageView; -import android.widget.ImageView.ScaleType; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.tinderview.R; - -/* -* created by Aradh Pillai on 1.10.15 -* This TabView class using to create tab and setting ImageView and Text into TabView -* and adding ripple animation into View. -* -* -* */ - - -public class TabView extends LinearLayout { - - private ImageView mImageView; - private TextView mTextView; - - public TabView(Context context) { - this(context, null); - } - - public TabView(Context context, AttributeSet attrs) { - this(context, attrs, android.R.attr.actionBarTabStyle); - } - - public TabView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - TypedValue outValue = new TypedValue(); - context.getTheme().resolveAttribute(android.R.attr.actionBarTabTextStyle, outValue, true); - - int txtstyle = outValue.data; - - int pad = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources() - .getDisplayMetrics()); - - - mImageView = new ImageView(context);//dynamically create an object of imageView and setting param into it... - mImageView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - mImageView.setScaleType(ScaleType.CENTER_INSIDE); - - mTextView = new TextView(context); //dynamically create a text view and setting these properties into it... - mTextView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - mTextView.setGravity(Gravity.CENTER); - mTextView.setCompoundDrawablePadding(pad); - mTextView.setTextAppearance(context, txtstyle); - - - this.addView(mImageView);// adding the imageview object into view - this.addView(mTextView);// adding textview object into view - this.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));//setting param into view - - this.setBackgroundResource(R.drawable.list_selector_white);//setting ripple animation into this View. - - - } - - public void setIcon(int resId) { - setIcon(getContext().getResources().getDrawable(resId)); - } - - public void setIcon(Drawable icon) { - if (icon != null) { - mImageView.setVisibility(View.VISIBLE); - mImageView.setImageDrawable(icon); - } else { - mImageView.setImageResource(View.GONE); - } - } - - public void setText(int resId, int ico) { - setText(getContext().getString(resId), ico); - } - - public void setText(CharSequence text, int ico) { - mTextView.setText(text); - mTextView.setCompoundDrawablesWithIntrinsicBounds(ico, 0, 0, 0); - } - -} \ No newline at end of file diff --git a/mobile/src/main/java/com/tinderview/util/CircularImageView.java b/mobile/src/main/java/com/tinderview/util/CircularImageView.java deleted file mode 100644 index ea6ebef..0000000 --- a/mobile/src/main/java/com/tinderview/util/CircularImageView.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.tinderview.util; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; - -import android.graphics.drawable.Drawable; -import android.support.v4.graphics.drawable.RoundedBitmapDrawable; -import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; - -/** - * Created by Aradh Pillaion 07/10/15. - *

- * setting the context and image resource from drawable and converting it into bitmap - */ -public class CircularImageView { - - public static Drawable makeCircleFromResources(Context context, int resource) { - Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resource); - - RoundedBitmapDrawable roundedBitmapDrawable = RoundedBitmapDrawableFactory.create( - context.getResources(), bitmapToSquare(bitmap)); - roundedBitmapDrawable.setCornerRadius(bitmap.getWidth() / 2); - roundedBitmapDrawable.setAntiAlias(true); - return roundedBitmapDrawable; - } - - - // - private static Bitmap bitmapToSquare(Bitmap bitmap) { - if (bitmap.getWidth() >= bitmap.getHeight()) { - bitmap = Bitmap.createBitmap(bitmap, - bitmap.getWidth() / 2 - bitmap.getHeight() / 2, 0, - bitmap.getHeight(), bitmap.getHeight()); - } else { - bitmap = Bitmap.createBitmap(bitmap, 0, bitmap.getHeight() / 2 - - bitmap.getWidth() / 2, bitmap.getWidth(), - bitmap.getWidth()); - } - - return bitmap; - } - -} diff --git a/mobile/src/main/java/com/tinderview/util/CustomViewPager.java b/mobile/src/main/java/com/tinderview/util/CustomViewPager.java deleted file mode 100644 index 754fd82..0000000 --- a/mobile/src/main/java/com/tinderview/util/CustomViewPager.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.tinderview.util; - -import android.content.Context; -import android.support.v4.view.ViewPager; -import android.util.AttributeSet; -import android.view.MotionEvent; - -/** - * Created by Aradh Pillai on 09/10/15. - *

- *

- * this class is created to avoid the swipe gesture from ViewPager - * this is customized ViewPager class to override the specific methods of ViewPager to avoid swipe feature. - */ - - -public class CustomViewPager extends ViewPager { - - public CustomViewPager(Context context) { - super(context); - } - - - public CustomViewPager(Context context, AttributeSet attrs) { - super(context, attrs); - } - - - /* when user will touch viewPager in intercept motion then this method will - * invoke and it will return "false" it means it won't work so using this method we can avoid this feather from ViewPager - * - */ - @Override - public boolean onInterceptTouchEvent(MotionEvent event) { - // Never allow swiping to switch between pages - return false; - } - - /* even user will touch viewPager scope then this method will invoke and it will return false value ... - * so using this method we can avoid touch event of ViewPager. - */ - - @Override - public boolean onTouchEvent(MotionEvent event) { - // Never allow swiping to switch between pages - return false; - } -} \ No newline at end of file diff --git a/mobile/src/main/res/drawable-hdpi/ic_chat.png b/mobile/src/main/res/drawable-hdpi/ic_chat.png deleted file mode 100755 index cc71515..0000000 Binary files a/mobile/src/main/res/drawable-hdpi/ic_chat.png and /dev/null differ diff --git a/mobile/src/main/res/drawable-hdpi/ic_connect.png b/mobile/src/main/res/drawable-hdpi/ic_connect.png deleted file mode 100755 index 4452783..0000000 Binary files a/mobile/src/main/res/drawable-hdpi/ic_connect.png and /dev/null differ diff --git a/mobile/src/main/res/drawable-hdpi/ic_list.png b/mobile/src/main/res/drawable-hdpi/ic_list.png deleted file mode 100755 index cbe6016..0000000 Binary files a/mobile/src/main/res/drawable-hdpi/ic_list.png and /dev/null differ diff --git a/mobile/src/main/res/drawable-mdpi/ic_chat.png b/mobile/src/main/res/drawable-mdpi/ic_chat.png deleted file mode 100755 index 735280c..0000000 Binary files a/mobile/src/main/res/drawable-mdpi/ic_chat.png and /dev/null differ diff --git a/mobile/src/main/res/drawable-mdpi/ic_connect.png b/mobile/src/main/res/drawable-mdpi/ic_connect.png deleted file mode 100755 index 0afed6e..0000000 Binary files a/mobile/src/main/res/drawable-mdpi/ic_connect.png and /dev/null differ diff --git a/mobile/src/main/res/drawable-mdpi/ic_list.png b/mobile/src/main/res/drawable-mdpi/ic_list.png deleted file mode 100755 index 970105c..0000000 Binary files a/mobile/src/main/res/drawable-mdpi/ic_list.png and /dev/null differ diff --git a/mobile/src/main/res/drawable-v21/list_selector_white.xml b/mobile/src/main/res/drawable-v21/list_selector_white.xml deleted file mode 100755 index b7cc05a..0000000 --- a/mobile/src/main/res/drawable-v21/list_selector_white.xml +++ /dev/null @@ -1,3 +0,0 @@ - - \ No newline at end of file diff --git a/mobile/src/main/res/drawable-xhdpi/ic_chat.png b/mobile/src/main/res/drawable-xhdpi/ic_chat.png deleted file mode 100755 index ae28ab2..0000000 Binary files a/mobile/src/main/res/drawable-xhdpi/ic_chat.png and /dev/null differ diff --git a/mobile/src/main/res/drawable-xhdpi/ic_connect.png b/mobile/src/main/res/drawable-xhdpi/ic_connect.png deleted file mode 100755 index b357171..0000000 Binary files a/mobile/src/main/res/drawable-xhdpi/ic_connect.png and /dev/null differ diff --git a/mobile/src/main/res/drawable-xhdpi/ic_list.png b/mobile/src/main/res/drawable-xhdpi/ic_list.png deleted file mode 100755 index a3d744f..0000000 Binary files a/mobile/src/main/res/drawable-xhdpi/ic_list.png and /dev/null differ diff --git a/mobile/src/main/res/drawable-xhdpi/phone2.png b/mobile/src/main/res/drawable-xhdpi/phone2.png deleted file mode 100755 index c7efb42..0000000 Binary files a/mobile/src/main/res/drawable-xhdpi/phone2.png and /dev/null differ diff --git a/mobile/src/main/res/drawable-xxhdpi/ic_chat.png b/mobile/src/main/res/drawable-xxhdpi/ic_chat.png deleted file mode 100755 index d5c8997..0000000 Binary files a/mobile/src/main/res/drawable-xxhdpi/ic_chat.png and /dev/null differ diff --git a/mobile/src/main/res/drawable-xxhdpi/ic_connect.png b/mobile/src/main/res/drawable-xxhdpi/ic_connect.png deleted file mode 100755 index 59db6b2..0000000 Binary files a/mobile/src/main/res/drawable-xxhdpi/ic_connect.png and /dev/null differ diff --git a/mobile/src/main/res/drawable-xxhdpi/ic_list.png b/mobile/src/main/res/drawable-xxhdpi/ic_list.png deleted file mode 100755 index abb4a06..0000000 Binary files a/mobile/src/main/res/drawable-xxhdpi/ic_list.png and /dev/null differ diff --git a/mobile/src/main/res/drawable-xxxhdpi/ic_chat.png b/mobile/src/main/res/drawable-xxxhdpi/ic_chat.png deleted file mode 100755 index 502a86d..0000000 Binary files a/mobile/src/main/res/drawable-xxxhdpi/ic_chat.png and /dev/null differ diff --git a/mobile/src/main/res/drawable-xxxhdpi/ic_connect.png b/mobile/src/main/res/drawable-xxxhdpi/ic_connect.png deleted file mode 100755 index 17d8cbf..0000000 Binary files a/mobile/src/main/res/drawable-xxxhdpi/ic_connect.png and /dev/null differ diff --git a/mobile/src/main/res/drawable-xxxhdpi/ic_list.png b/mobile/src/main/res/drawable-xxxhdpi/ic_list.png deleted file mode 100755 index 0fcccfd..0000000 Binary files a/mobile/src/main/res/drawable-xxxhdpi/ic_list.png and /dev/null differ diff --git a/mobile/src/main/res/drawable-xxxhdpi/ripple_selector_white.xml b/mobile/src/main/res/drawable-xxxhdpi/ripple_selector_white.xml deleted file mode 100755 index 167d9b6..0000000 --- a/mobile/src/main/res/drawable-xxxhdpi/ripple_selector_white.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/mobile/src/main/res/drawable/card_stack_bg_shadow.xml b/mobile/src/main/res/drawable/card_stack_bg_shadow.xml deleted file mode 100644 index 1546f9f..0000000 --- a/mobile/src/main/res/drawable/card_stack_bg_shadow.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - diff --git a/mobile/src/main/res/drawable/card_stack_bg_shadow2.xml b/mobile/src/main/res/drawable/card_stack_bg_shadow2.xml deleted file mode 100644 index a95666b..0000000 --- a/mobile/src/main/res/drawable/card_stack_bg_shadow2.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/mobile/src/main/res/drawable/card_stack_bg_shadow3.xml b/mobile/src/main/res/drawable/card_stack_bg_shadow3.xml deleted file mode 100644 index c1aa5cc..0000000 --- a/mobile/src/main/res/drawable/card_stack_bg_shadow3.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/mobile/src/main/res/drawable/list_selector_white.xml b/mobile/src/main/res/drawable/list_selector_white.xml deleted file mode 100755 index ef7c9e5..0000000 --- a/mobile/src/main/res/drawable/list_selector_white.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/mobile/src/main/res/drawable/toolbar_shadow_pre_lolipop.xml b/mobile/src/main/res/drawable/toolbar_shadow_pre_lolipop.xml deleted file mode 100644 index 21eef6c..0000000 --- a/mobile/src/main/res/drawable/toolbar_shadow_pre_lolipop.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - \ No newline at end of file diff --git a/mobile/src/main/res/layout/activity_home.xml b/mobile/src/main/res/layout/activity_home.xml deleted file mode 100755 index a3302a9..0000000 --- a/mobile/src/main/res/layout/activity_home.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/src/main/res/layout/background_ripple_layout.xml b/mobile/src/main/res/layout/background_ripple_layout.xml deleted file mode 100644 index 98d6d2b..0000000 --- a/mobile/src/main/res/layout/background_ripple_layout.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - diff --git a/mobile/src/main/res/layout/card_stack_item.xml b/mobile/src/main/res/layout/card_stack_item.xml deleted file mode 100755 index b231e6d..0000000 --- a/mobile/src/main/res/layout/card_stack_item.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mobile/src/main/res/layout/custom_tab_view.xml b/mobile/src/main/res/layout/custom_tab_view.xml deleted file mode 100755 index 054d069..0000000 --- a/mobile/src/main/res/layout/custom_tab_view.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - diff --git a/mobile/src/main/res/layout/fragment_chat.xml b/mobile/src/main/res/layout/fragment_chat.xml deleted file mode 100755 index 0e47c23..0000000 --- a/mobile/src/main/res/layout/fragment_chat.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - diff --git a/mobile/src/main/res/layout/fragment_connect.xml b/mobile/src/main/res/layout/fragment_connect.xml deleted file mode 100755 index 5d8c1c5..0000000 --- a/mobile/src/main/res/layout/fragment_connect.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - diff --git a/mobile/src/main/res/layout/fragment_discover.xml b/mobile/src/main/res/layout/fragment_discover.xml deleted file mode 100755 index a18fbe6..0000000 --- a/mobile/src/main/res/layout/fragment_discover.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/mobile/src/main/res/menu/menu_main.xml b/mobile/src/main/res/menu/menu_main.xml deleted file mode 100755 index 57c37ed..0000000 --- a/mobile/src/main/res/menu/menu_main.xml +++ /dev/null @@ -1,9 +0,0 @@ -

- - diff --git a/mobile/src/main/res/values-v21/styles.xml b/mobile/src/main/res/values-v21/styles.xml deleted file mode 100644 index b39a4fa..0000000 --- a/mobile/src/main/res/values-v21/styles.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/mobile/src/main/res/values/attrs.xml b/mobile/src/main/res/values/attrs.xml deleted file mode 100755 index 2daedb4..0000000 --- a/mobile/src/main/res/values/attrs.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mobile/src/main/res/values/colors.xml b/mobile/src/main/res/values/colors.xml deleted file mode 100755 index 9d4717d..0000000 --- a/mobile/src/main/res/values/colors.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - //ripple color for Tabs - #77ffffff - - //color for background - #e6e6e6 - - //theme properties mentioned here - #3498db - #2980b9 - #FFFFFF - - #000000 - #ffffff - - #b6b6b6 - - //for ripple background animation - #993498db - - //stroke color for CardStack - #B1B1B1 - - //these three different shadow colors for CardStack - #fbfcfc - #f8f8f8 - #f2f2f2 - - - //for pre-lollipop gradient color - #40000000 - - \ No newline at end of file diff --git a/mobile/src/main/res/values/dimensions.xml b/mobile/src/main/res/values/dimensions.xml deleted file mode 100755 index c6d8397..0000000 --- a/mobile/src/main/res/values/dimensions.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - //sets some default properties for rippleBackgroundAnimation - 2dp - 64dp - \ No newline at end of file diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml deleted file mode 100755 index 0507672..0000000 --- a/mobile/src/main/res/values/strings.xml +++ /dev/null @@ -1,20 +0,0 @@ - - tinderview - - Hello world! - Settings - - Discover - The fastest way to connect with startups in a single tap - - Connect - Simply swipe to connect with a startup. Chat freely & take informed decisions - - Invest - Add friends to your group and invest together, We will help you keep track of everything - - Connect - Discover - Chat - - diff --git a/mobile/src/main/res/values/styles.xml b/mobile/src/main/res/values/styles.xml deleted file mode 100755 index 506ec38..0000000 --- a/mobile/src/main/res/values/styles.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 6a4e79f..0000000 --- a/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -include ':mobile', ':wear' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..a66ea92 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "TinderView" +include(":cardstack") +include(":app") diff --git a/wear/.gitignore b/wear/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/wear/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/wear/build.gradle b/wear/build.gradle deleted file mode 100644 index 6507949..0000000 --- a/wear/build.gradle +++ /dev/null @@ -1,27 +0,0 @@ -apply plugin: 'com.android.application' - - -android { - compileSdkVersion 22 - buildToolsVersion "22.0.1" - - defaultConfig { - applicationId "com.tinderview" - minSdkVersion 20 - targetSdkVersion 22 - versionCode 1 - versionName "1.0" - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } -} - -dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.google.android.support:wearable:1.2.0' - compile 'com.google.android.gms:play-services-wearable:7.8.0' -} diff --git a/wear/proguard-rules.pro b/wear/proguard-rules.pro deleted file mode 100644 index b89bfd7..0000000 --- a/wear/proguard-rules.pro +++ /dev/null @@ -1,17 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in /Users/Aradh/Library/Android/sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} diff --git a/wear/src/main/java/com/letsventure/MainActivity.java b/wear/src/main/java/com/letsventure/MainActivity.java deleted file mode 100644 index 03baabe..0000000 --- a/wear/src/main/java/com/letsventure/MainActivity.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.tinderview; - -import android.app.Activity; -import android.os.Bundle; -import android.support.wearable.view.WatchViewStub; -import android.widget.TextView; - -public class MainActivity extends Activity { - - private TextView mTextView; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub); - stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() { - @Override - public void onLayoutInflated(WatchViewStub stub) { - mTextView = (TextView) stub.findViewById(R.id.text); - } - }); - } -} diff --git a/wear/src/main/res/layout/activity_main.xml b/wear/src/main/res/layout/activity_main.xml deleted file mode 100644 index 7b5851a..0000000 --- a/wear/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,9 +0,0 @@ - - diff --git a/wear/src/main/res/layout/rect_activity_main.xml b/wear/src/main/res/layout/rect_activity_main.xml deleted file mode 100644 index 203071b..0000000 --- a/wear/src/main/res/layout/rect_activity_main.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/wear/src/main/res/layout/round_activity_main.xml b/wear/src/main/res/layout/round_activity_main.xml deleted file mode 100644 index c91ab44..0000000 --- a/wear/src/main/res/layout/round_activity_main.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/wear/src/main/res/mipmap-hdpi/ic_launcher.png b/wear/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index cde69bc..0000000 Binary files a/wear/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/wear/src/main/res/mipmap-mdpi/ic_launcher.png b/wear/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index c133a0c..0000000 Binary files a/wear/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/wear/src/main/res/mipmap-xhdpi/ic_launcher.png b/wear/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index bfa42f0..0000000 Binary files a/wear/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/wear/src/main/res/mipmap-xxhdpi/ic_launcher.png b/wear/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 324e72c..0000000 Binary files a/wear/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/wear/src/main/res/values/strings.xml b/wear/src/main/res/values/strings.xml deleted file mode 100644 index cf92265..0000000 --- a/wear/src/main/res/values/strings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - tinderview - Hello Round World! - Hello Square World! - diff --git a/wear/wear.iml b/wear/wear.iml deleted file mode 100644 index c0ac484..0000000 --- a/wear/wear.iml +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file