├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── kotlinScripting.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── play │ │ └── HomeScreenTest.kt │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── play │ │ ├── anim │ │ ├── BottomNavAnim.kt │ │ ├── FilterChipAnim.kt │ │ ├── HeaderAnim.kt │ │ ├── InstallButtonAnim.kt │ │ └── RatingBarAnim.kt │ │ ├── data │ │ ├── AppRepo.kt │ │ └── Models.kt │ │ ├── theme │ │ ├── Color.kt │ │ ├── Shape.kt │ │ ├── Theme.kt │ │ └── Type.kt │ │ ├── ui │ │ ├── BottomNav.kt │ │ ├── MainActivity.kt │ │ ├── NavGraph.kt │ │ ├── PlayApp.kt │ │ ├── Tabs.kt │ │ ├── apps │ │ │ ├── Apps.kt │ │ │ └── applist │ │ │ │ ├── AppItem.kt │ │ │ │ ├── ForYou.kt │ │ │ │ └── TopCharts.kt │ │ ├── books │ │ │ └── Books.kt │ │ ├── components │ │ │ ├── AppBarLayout.kt │ │ │ ├── Card.kt │ │ │ ├── Divider.kt │ │ │ ├── Filters.kt │ │ │ ├── Gradient.kt │ │ │ ├── Image.kt │ │ │ ├── PlayToolBar.kt │ │ │ ├── Scaffold.kt │ │ │ ├── Surface.kt │ │ │ ├── SwitchComponent.kt │ │ │ └── progressindicator │ │ │ │ └── ProgressIndicator.kt │ │ ├── details │ │ │ ├── AppDetails.kt │ │ │ ├── ScreenshotItem.kt │ │ │ ├── about │ │ │ │ └── About.kt │ │ │ ├── header │ │ │ │ └── Header.kt │ │ │ ├── installbutton │ │ │ │ ├── animated │ │ │ │ │ ├── AnimatedInstallButton.kt │ │ │ │ │ ├── ButtonContent.kt │ │ │ │ │ └── InstallButtonLayout.kt │ │ │ │ └── plain │ │ │ │ │ └── PlainInstallButton.kt │ │ │ ├── reviews │ │ │ │ ├── RatingsAndReviews.kt │ │ │ │ └── ReviewItem.kt │ │ │ ├── screenshots │ │ │ │ └── Screenshots.kt │ │ │ └── stats │ │ │ │ └── Stats.kt │ │ ├── games │ │ │ └── Games.kt │ │ └── movies │ │ │ ├── Movies.kt │ │ │ └── movielist │ │ │ ├── MovieItem.kt │ │ │ ├── MoviesForYou.kt │ │ │ └── TopSelling.kt │ │ └── utils │ │ ├── Navigation.kt │ │ └── SystemUi.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── bg_black_rect.xml │ ├── ic_apps.xml │ ├── ic_books.xml │ ├── ic_books_otlined.xml │ ├── ic_box.xml │ ├── ic_download.xml │ ├── ic_games.xml │ ├── ic_launcher_background.xml │ ├── ic_movies.xml │ ├── ic_movies_outlined.xml │ ├── ic_star_solid.xml │ └── user_profile_pic.jpg │ ├── font │ ├── product_sans_bold.ttf │ └── product_sans_regular.ttf │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── raw │ └── working.json │ ├── values-night │ └── themes.xml │ └── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml ├── build.gradle ├── buildSrc ├── build.gradle.kts ├── build │ ├── classes │ │ └── kotlin │ │ │ └── main │ │ │ ├── META-INF │ │ │ └── buildSrc.kotlin_module │ │ │ └── com │ │ │ └── example │ │ │ └── play │ │ │ └── buildsrc │ │ │ ├── Libs$Accompanist.class │ │ │ ├── Libs$AndroidX$Compose.class │ │ │ ├── Libs$AndroidX$Test$Ext.class │ │ │ ├── Libs$AndroidX$Test.class │ │ │ ├── Libs$AndroidX.class │ │ │ ├── Libs$Coroutines.class │ │ │ ├── Libs$Kotlin.class │ │ │ ├── Libs.class │ │ │ └── Versions.class │ ├── kotlin │ │ └── buildSrcjar-classes.txt │ ├── libs │ │ └── buildSrc.jar │ ├── pluginUnderTestMetadata │ │ └── plugin-under-test-metadata.properties │ ├── reports │ │ └── plugin-development │ │ │ └── validation-report.txt │ └── tmp │ │ └── jar │ │ └── MANIFEST.MF └── src │ └── main │ └── java │ └── com │ └── example │ └── play │ └── buildsrc │ └── Dependencies.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshots ├── demo.gif ├── screenshot_1.png ├── screenshot_2.png ├── screenshot_3.png ├── screenshot_4.png ├── screenshot_5.png └── screenshot_6.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | -------------------------------------------------------------------------------- /.idea/kotlinScripting.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2147483647 6 | true 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JetStore 2 | 3 | A clone of Google PlayStore app with animations in Android built using Jetpack compose. 4 | 5 |
6 | 7 | 8 | 9 |
10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | ### This sample app showcases the following: 33 | 34 | * Working with bottom nav bar and tabs 35 | * Implementation of Chips to filter 36 | * Light and dark theme using custom color palette 37 | * Horizontal and vertical scrollable list 38 | * Using Animation APIs 39 | * Test compose layouts 40 | 41 | ### Screens 42 | Demo UI | Games | App Details 43 | :-------------------------:|:-------------------------: | :-------------------------: 44 | | | 45 | 46 | App Reviews | Apps (Dark) | Movies (Dark) 47 | :-------------------------:|:-------------------------: | :-------------------------: 48 | | | 49 | 50 | ### Status: 🚧 In progress 51 |

JetStore is still under development and some screens are yet to be designed.

52 | 53 | ### Libraries used 54 | 55 | * [Jetpack Compose] 56 | * [Accompanist] 57 | * [Coroutines] 58 | * [Lottie Compose] 59 | * [Compose Navigation] 60 | * [Compose Test] 61 | 62 | [Jetpack Compose]: https://developer.android.com/jetpack/compose 63 | [Accompanist]: https://github.com/chrisbanes/accompanist 64 | [Coroutines]: https://developer.android.com/kotlin/coroutines 65 | [Lottie Compose]: https://github.com/airbnb/lottie 66 | [Compose Navigation]: https://developer.android.com/jetpack/compose/navigation 67 | [Compose Test]: https://developer.android.com/jetpack/compose/testing 68 | 69 | ### How to get started 70 | Please get Android Studio Arctic Fox 2020.3.1 Beta 3 or above to build this project 71 | from [from here](https://developer.android.com/studio/preview/). 72 | 73 | ### License 74 | ``` 75 | Copyright 2020 Pushpal Roy 76 | 77 | Licensed under the Apache License, Version 2.0 (the "License"); 78 | you may not use this file except in compliance with the License. 79 | You may obtain a copy of the License at 80 | 81 | http://www.apache.org/licenses/LICENSE-2.0 82 | 83 | Unless required by applicable law or agreed to in writing, software 84 | distributed under the License is distributed on an "AS IS" BASIS, 85 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 86 | See the License for the specific language governing permissions and 87 | limitations under the License. 88 | ``` 89 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | import com.example.play.buildsrc.Libs 2 | 3 | plugins { 4 | id 'com.android.application' 5 | id 'kotlin-android' 6 | id 'kotlin-android-extensions' 7 | } 8 | 9 | android { 10 | compileSdkVersion 30 11 | buildToolsVersion "30.0.3" 12 | 13 | defaultConfig { 14 | applicationId "com.example.play" 15 | minSdkVersion 21 16 | targetSdkVersion 30 17 | versionCode 1 18 | versionName "1.0" 19 | vectorDrawables.useSupportLibrary true 20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | kotlinOptions { 34 | jvmTarget = '1.8' 35 | useIR = true 36 | } 37 | buildFeatures { 38 | compose true 39 | // Disable unused AGP features 40 | buildConfig false 41 | aidl false 42 | renderScript false 43 | resValues false 44 | shaders false 45 | } 46 | composeOptions { 47 | kotlinCompilerVersion Libs.Kotlin.version 48 | kotlinCompilerExtensionVersion Libs.AndroidX.Compose.composeVer 49 | } 50 | packagingOptions { 51 | // https://github.com/Kotlin/kotlinx.coroutines/issues/2023 52 | exclude 'META-INF/AL2.0' 53 | exclude 'META-INF/LGPL2.1' 54 | } 55 | } 56 | 57 | dependencies { 58 | implementation Libs.Kotlin.stdlib 59 | implementation Libs.AndroidX.coreKtx 60 | implementation Libs.AndroidX.navigation 61 | implementation Libs.AndroidX.Activity.activityCompose 62 | 63 | implementation Libs.AndroidX.Compose.runtime 64 | implementation Libs.AndroidX.Compose.foundation 65 | implementation Libs.AndroidX.Compose.layout 66 | implementation Libs.AndroidX.Compose.ui 67 | implementation Libs.AndroidX.Compose.uiUtil 68 | implementation Libs.AndroidX.Compose.material 69 | implementation Libs.AndroidX.Compose.animation 70 | implementation Libs.AndroidX.Compose.iconsExtended 71 | implementation Libs.AndroidX.Compose.tooling 72 | 73 | implementation Libs.Accompanist.systemuicontroller 74 | implementation Libs.Accompanist.coil 75 | implementation Libs.Accompanist.insets 76 | 77 | //implementation Libs.LottieView.lottie 78 | //implementation Libs.LottieView.lottieCompose 79 | 80 | androidTestImplementation Libs.AndroidX.Compose.uiTestJunit 81 | debugImplementation Libs.AndroidX.Compose.uiTestManifest 82 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/play/HomeScreenTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.play 2 | 3 | import androidx.compose.ui.test.assertIsDisplayed 4 | import androidx.compose.ui.test.junit4.createAndroidComposeRule 5 | import androidx.compose.ui.test.onNodeWithText 6 | import androidx.compose.ui.test.onRoot 7 | import androidx.compose.ui.test.performClick 8 | import androidx.compose.ui.test.printToLog 9 | import com.example.play.theme.PlayTheme 10 | import com.example.play.ui.MainActivity 11 | import com.example.play.ui.PlayApp 12 | import org.junit.Rule 13 | import org.junit.Test 14 | 15 | class HomeScreenTest { 16 | 17 | @get:Rule 18 | val composeTestRule = createAndroidComposeRule() 19 | // createComposeRule() if you don't need access to the activityTestRule 20 | 21 | @Test 22 | fun testTopCharts() { 23 | // Start the app 24 | composeTestRule.setContent { 25 | PlayTheme { 26 | PlayApp {} 27 | } 28 | } 29 | composeTestRule.onNodeWithText("Top Charts").performClick() 30 | composeTestRule.onNodeWithText("Show installed apps").assertIsDisplayed() 31 | composeTestRule.onNodeWithText("Top grossing").performClick() 32 | composeTestRule.onRoot().printToLog("TAG") 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/anim/BottomNavAnim.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.anim 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.animation.core.AnimationSpec 5 | import androidx.compose.animation.core.animateFloatAsState 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.State 8 | import androidx.compose.ui.graphics.Color 9 | import com.example.play.theme.PlayTheme 10 | 11 | @Composable 12 | fun getBottomNavTintState(selected: Boolean): State { 13 | return animateColorAsState( 14 | if (selected) { 15 | PlayTheme.colors.iconInteractive 16 | } else { 17 | PlayTheme.colors.iconInteractiveInactive 18 | } 19 | ) 20 | } 21 | 22 | @Composable 23 | fun getProgressState( 24 | selected: Boolean, 25 | animSpec: AnimationSpec 26 | ): State { 27 | return animateFloatAsState(if (selected) 1f else 0f, animSpec) 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/anim/FilterChipAnim.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.anim 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.State 6 | import androidx.compose.ui.graphics.Color 7 | import com.example.play.theme.PlayTheme 8 | 9 | @Composable 10 | fun getFilterBgColorState(isSelected: Boolean): State { 11 | return animateColorAsState( 12 | if (isSelected) PlayTheme.colors.accent.copy(alpha = 0.1f) 13 | else PlayTheme.colors.uiBackground 14 | ) 15 | } 16 | 17 | @Composable 18 | fun getFilterTextColorState(isSelected: Boolean): State { 19 | return animateColorAsState( 20 | if (isSelected) PlayTheme.colors.accentDark 21 | else PlayTheme.colors.textSecondary 22 | ) 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/anim/HeaderAnim.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.anim 2 | 3 | import androidx.compose.animation.core.animateDpAsState 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.State 7 | import androidx.compose.ui.unit.Dp 8 | import androidx.compose.ui.unit.dp 9 | 10 | @Composable 11 | fun getAppIconSizeAnimState(showProgress: Boolean): State { 12 | return animateDpAsState( 13 | targetValue = if (showProgress) 80.dp else 100.dp, 14 | animationSpec = tween( 15 | durationMillis = 500, 16 | delayMillis = 500 17 | ) 18 | ) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/anim/InstallButtonAnim.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.anim 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.animation.core.FastOutLinearInEasing 5 | import androidx.compose.animation.core.LinearEasing 6 | import androidx.compose.animation.core.RepeatMode.Reverse 7 | import androidx.compose.animation.core.animateDpAsState 8 | import androidx.compose.animation.core.animateFloat 9 | import androidx.compose.animation.core.animateIntAsState 10 | import androidx.compose.animation.core.infiniteRepeatable 11 | import androidx.compose.animation.core.rememberInfiniteTransition 12 | import androidx.compose.animation.core.snap 13 | import androidx.compose.animation.core.tween 14 | import androidx.compose.animation.core.updateTransition 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.State 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.unit.Dp 19 | import androidx.compose.ui.unit.dp 20 | import com.example.play.anim.ButtonState.IDLE 21 | import com.example.play.anim.ButtonState.PRESSED 22 | 23 | enum class ButtonState { 24 | IDLE, 25 | PRESSED 26 | } 27 | 28 | @Composable 29 | fun getInstallButtonOpacityState( 30 | isPressed: Boolean 31 | ): State { 32 | val currentState = if (isPressed) PRESSED else IDLE 33 | val transition = updateTransition(targetState = currentState, label = "installButtonOpacityState") 34 | return transition.animateFloat( 35 | transitionSpec = { 36 | when { 37 | IDLE isTransitioningTo PRESSED -> tween(durationMillis = 1500) 38 | PRESSED isTransitioningTo IDLE -> { 39 | tween(durationMillis = 2000) 40 | } 41 | else -> snap() 42 | } 43 | }, label = "installButtonOpacityState" 44 | ) { installButtonOpacityState -> 45 | when (installButtonOpacityState) { 46 | IDLE -> 0f 47 | PRESSED -> 0.5f 48 | } 49 | } 50 | } 51 | 52 | @Composable 53 | fun getButtonIconSizeState(): State { 54 | val transition = rememberInfiniteTransition() 55 | return transition.animateFloat( 56 | initialValue = 0f, 57 | targetValue = 24f, 58 | animationSpec = infiniteRepeatable( 59 | tween(500), Reverse 60 | ) 61 | ) 62 | } 63 | 64 | @Composable 65 | fun getOpenButtonWidthState(isPressed: Boolean): State { 66 | return animateDpAsState( 67 | targetValue = if (isPressed) 150.dp else 0.dp, 68 | animationSpec = tween( 69 | durationMillis = 800, 70 | delayMillis = if (isPressed) 0 else 1500 71 | ) 72 | ) 73 | } 74 | 75 | @Composable 76 | fun getInstallButtonWidthState(isPressed: Boolean): State { 77 | return animateDpAsState( 78 | targetValue = if (isPressed) 150.dp else 340.dp, 79 | animationSpec = tween( 80 | durationMillis = 1500, 81 | delayMillis = if (isPressed) 800 else 0 82 | ) 83 | ) 84 | } 85 | 86 | @Composable 87 | fun getButtonColorState(isPressed: Boolean): State { 88 | return animateColorAsState( 89 | targetValue = if (isPressed) Color(0xff01875f) else Color.White, 90 | animationSpec = tween( 91 | durationMillis = 500 92 | ) 93 | ) 94 | } 95 | 96 | @Composable 97 | fun getInstallBtnBorderColorState(isPressed: Boolean): State { 98 | return animateColorAsState( 99 | targetValue = if (isPressed) Color.White else Color(0xff01875f) 100 | ) 101 | } 102 | 103 | @Composable 104 | fun getInstallBtnBorderWidthState(isPressed: Boolean): State { 105 | return animateDpAsState( 106 | targetValue = if (isPressed) 0.dp else 1.dp, 107 | ) 108 | } 109 | 110 | @Composable 111 | fun getInstallBtnBgColorState(isPressed: Boolean): State { 112 | return animateColorAsState( 113 | targetValue = if (isPressed) Color.White else Color(0xff01875f), 114 | animationSpec = tween( 115 | durationMillis = 3000 116 | ) 117 | ) 118 | } 119 | 120 | @Composable 121 | fun getInstallBtnCornerState(isPressed: Boolean): State { 122 | return animateIntAsState( 123 | targetValue = if (isPressed) 50 else 10, 124 | animationSpec = tween( 125 | durationMillis = 3000, 126 | easing = FastOutLinearInEasing, 127 | ) 128 | ) 129 | } 130 | 131 | @Composable 132 | fun getOpenButtonGapWidthState(isPressed: Boolean): State { 133 | return animateDpAsState( 134 | targetValue = if (isPressed) 8.dp else 0.dp, 135 | animationSpec = tween( 136 | durationMillis = 800, 137 | delayMillis = if (isPressed) 800 else 1500, 138 | easing = LinearEasing 139 | ) 140 | ) 141 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/anim/RatingBarAnim.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.anim 2 | 3 | import androidx.compose.animation.core.FastOutSlowInEasing 4 | import androidx.compose.animation.core.animateFloat 5 | import androidx.compose.animation.core.snap 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.animation.core.updateTransition 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.State 10 | 11 | enum class AppRatingBarState { 12 | START, 13 | END 14 | } 15 | 16 | @Composable 17 | fun getAppRatingBarState( 18 | progress: Float, 19 | durationMillis: Int = 3000, 20 | showProgress: Boolean 21 | ): State { 22 | val currentState = if (showProgress) AppRatingBarState.END else AppRatingBarState.START 23 | val transition = updateTransition(targetState = currentState, label = "appRatingBarState") 24 | return transition.animateFloat( 25 | transitionSpec = { 26 | when { 27 | AppRatingBarState.START isTransitioningTo AppRatingBarState.END -> 28 | tween(easing = FastOutSlowInEasing, durationMillis = durationMillis) 29 | AppRatingBarState.END isTransitioningTo AppRatingBarState.START -> { 30 | tween(easing = FastOutSlowInEasing, durationMillis = durationMillis) 31 | } 32 | else -> snap() 33 | } 34 | }, label = "appRatingBarState" 35 | ) { appRatingBarState -> 36 | when (appRatingBarState) { 37 | AppRatingBarState.START -> 0f 38 | AppRatingBarState.END -> progress 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/data/Models.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.data 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.runtime.Stable 5 | import androidx.compose.runtime.mutableStateOf 6 | 7 | @Immutable 8 | data class App( 9 | val id: Long, 10 | val name: String, 11 | val imageUrl: String, 12 | val featureImageUrl: String = "", 13 | val size: String = "7 MB", 14 | val category: String = "Utility", 15 | val filterCategory: String = "Top free", 16 | val type: String = "Action", 17 | val ratings: String = "4.1", 18 | val org: String = "XCompany", 19 | val info: String = "Contains Ads", 20 | val reviews: List = listOf(), 21 | val tags: Set = emptySet() 22 | ) 23 | 24 | @Immutable 25 | data class Movie( 26 | val id: Long, 27 | val name: String, 28 | val imageUrl: String = "", 29 | val category: String = "Drama", 30 | val filterCategory: String = "For You", 31 | val ratings: String = "3.5", 32 | val price: String = "₹50.00", 33 | ) 34 | 35 | @Immutable 36 | data class Review( 37 | val id: Long = 1, 38 | val userName: String = "John Barretto", 39 | val userAvatarUrl: String = "", 40 | val reviewDesc: String = "Amazing App!", 41 | val ratings: Double = 4.5, 42 | val date: String = "9/25/20", 43 | val appId: Long = 1 44 | ) 45 | 46 | @Stable 47 | class Filter( 48 | id: Int, 49 | val name: String, 50 | enabled: Boolean = false 51 | ) { 52 | val enabled = mutableStateOf(enabled) 53 | val id = mutableStateOf(id) 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val White = Color(0xffffffff) 6 | val Green = Color(0xff00a173) 7 | 8 | val VeryLightGrey = Color(0xffdadce0) 9 | val LightGrey = Color(0xffe8eaed) 10 | val Grey = Color(0xff5f6368) 11 | val TextPrimary = Color(0xff202124) 12 | val TextSecondaryDark = Color(0xff212121) 13 | val TextSecondary = Color(0xff5f6368) 14 | val DeepGreen = Color(0xff01875f) 15 | val DarkGreen = Color(0xff056449) 16 | val GreyBg = Color(0xff202125) 17 | 18 | val Shadow11 = Color(0xff001787) 19 | val Shadow10 = Color(0xff00119e) 20 | val Shadow9 = Color(0xff0009b3) 21 | val Shadow8 = Color(0xff0200c7) 22 | val Shadow7 = Color(0xff0e00d7) 23 | val Shadow6 = Color(0xff2a13e4) 24 | val Shadow5 = Color(0xff4b30ed) 25 | val Shadow4 = Color(0xff7057f5) 26 | val Shadow3 = Color(0xff9b86fa) 27 | val Shadow2 = Color(0xffc8bbfd) 28 | val Shadow1 = Color(0xffded6fe) 29 | val Shadow0 = Color(0xfff4f2ff) 30 | 31 | val Ocean11 = Color(0xff005687) 32 | val Ocean10 = Color(0xff006d9e) 33 | val Ocean9 = Color(0xff0087b3) 34 | val Ocean8 = Color(0xff00a1c7) 35 | val Ocean7 = Color(0xff00b9d7) 36 | val Ocean6 = Color(0xff13d0e4) 37 | val Ocean5 = Color(0xff30e2ed) 38 | val Ocean4 = Color(0xff57eff5) 39 | val Ocean3 = Color(0xff86f7fa) 40 | val Ocean2 = Color(0xffbbfdfd) 41 | val Ocean1 = Color(0xffd6fefe) 42 | val Ocean0 = Color(0xfff2ffff) 43 | 44 | val Lavender11 = Color(0xff170085) 45 | val Lavender10 = Color(0xff23009e) 46 | val Lavender9 = Color(0xff3300b3) 47 | val Lavender8 = Color(0xff4400c7) 48 | val Lavender7 = Color(0xff5500d7) 49 | val Lavender6 = Color(0xff6f13e4) 50 | val Lavender5 = Color(0xff8a30ed) 51 | val Lavender4 = Color(0xffa557f5) 52 | val Lavender3 = Color(0xffc186fa) 53 | val Lavender2 = Color(0xffdebbfd) 54 | val Lavender1 = Color(0xffebd6fe) 55 | val Lavender0 = Color(0xfff9f2ff) 56 | 57 | val Rose11 = Color(0xff7f0054) 58 | val Rose10 = Color(0xff97005c) 59 | val Rose9 = Color(0xffaf0060) 60 | val Rose8 = Color(0xffc30060) 61 | val Rose7 = Color(0xffd4005d) 62 | val Rose6 = Color(0xffe21365) 63 | val Rose5 = Color(0xffec3074) 64 | val Rose4 = Color(0xfff4568b) 65 | val Rose3 = Color(0xfff985aa) 66 | val Rose2 = Color(0xfffdbbcf) 67 | val Rose1 = Color(0xfffed6e2) 68 | val Rose0 = Color(0xfffff2f6) 69 | 70 | val Neutral8 = Color(0xff121212) 71 | val Neutral7 = Color(0xdef000000) 72 | val Neutral6 = Color(0x99000000) 73 | val Neutral5 = Color(0x61000000) 74 | val Neutral4 = Color(0x1f000000) 75 | val Neutral3 = Color(0x1fffffff) 76 | val Neutral2 = Color(0x61ffffff) 77 | val Neutral1 = Color(0xbdffffff) 78 | val Neutral0 = Color(0xffffffff) 79 | 80 | val FunctionalRed = Color(0xffd00036) 81 | val FunctionalRedDark = Color(0xffea6d7e) 82 | val FunctionalGreen = Color(0xff52c41a) 83 | val FunctionalGrey = Color(0xfff6f6f6) 84 | val FunctionalDarkGrey = Color(0xff2e2e2e) 85 | 86 | const val AlphaNearOpaque = 0.95f 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape(percent = 50), 9 | medium = RoundedCornerShape(20.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.Colors 5 | import androidx.compose.material.MaterialTheme 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.CompositionLocalProvider 8 | import androidx.compose.runtime.SideEffect 9 | import androidx.compose.runtime.Stable 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.runtime.setValue 14 | import androidx.compose.runtime.staticCompositionLocalOf 15 | import androidx.compose.ui.graphics.Color 16 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 17 | 18 | private val LightColorPalette = PlayColorPalette( 19 | brand = White, 20 | accent = DeepGreen, 21 | accentDark = DarkGreen, 22 | iconTint = Grey, 23 | uiBackground = Neutral0, 24 | uiBorder = VeryLightGrey, 25 | uiFloated = FunctionalGrey, 26 | textPrimary = TextPrimary, 27 | textSecondary = TextSecondary, 28 | textSecondaryDark = TextSecondaryDark, 29 | textHelp = Neutral6, 30 | textInteractive = Neutral0, 31 | textLink = Ocean11, 32 | iconSecondary = Neutral7, 33 | iconInteractive = Green, 34 | iconInteractiveInactive = Grey, 35 | error = FunctionalRed, 36 | gradient6_1 = listOf(Shadow4, Ocean3, Shadow2, Ocean3, Shadow4), 37 | gradient6_2 = listOf(Rose4, Lavender3, Rose2, Lavender3, Rose4), 38 | gradient3_1 = listOf(Shadow2, Ocean3, Shadow4), 39 | gradient3_2 = listOf(Rose2, Lavender3, Rose4), 40 | gradient2_1 = listOf(Shadow4, Shadow11), 41 | gradient2_2 = listOf(Ocean3, Shadow3), 42 | progressIndicatorBg = LightGrey, 43 | switchColor = Green, 44 | isDark = false 45 | ) 46 | 47 | private val DarkColorPalette = PlayColorPalette( 48 | brand = Shadow1, 49 | accent = DeepGreen, 50 | accentDark = DarkGreen, 51 | iconTint = Shadow1, 52 | uiBackground = GreyBg, 53 | uiBorder = Neutral3, 54 | uiFloated = FunctionalDarkGrey, 55 | textPrimary = Shadow1, 56 | textSecondary = Neutral0, 57 | textHelp = Neutral1, 58 | textInteractive = Neutral7, 59 | textLink = Ocean2, 60 | iconPrimary = Neutral3, 61 | iconSecondary = Neutral0, 62 | textSecondaryDark = Neutral0, 63 | iconInteractive = White, 64 | iconInteractiveInactive = Neutral2, 65 | error = FunctionalRedDark, 66 | gradient6_1 = listOf(Shadow5, Ocean7, Shadow9, Ocean7, Shadow5), 67 | gradient6_2 = listOf(Rose11, Lavender7, Rose8, Lavender7, Rose11), 68 | gradient3_1 = listOf(Shadow9, Ocean7, Shadow5), 69 | gradient3_2 = listOf(Rose8, Lavender7, Rose11), 70 | gradient2_1 = listOf(Ocean3, Shadow3), 71 | gradient2_2 = listOf(Ocean7, Shadow7), 72 | progressIndicatorBg = LightGrey, 73 | switchColor = Green, 74 | isDark = true 75 | ) 76 | 77 | @Composable 78 | fun PlayTheme( 79 | darkTheme: Boolean = isSystemInDarkTheme(), 80 | content: @Composable () -> Unit 81 | ) { 82 | val colors = if (darkTheme) DarkColorPalette else LightColorPalette 83 | 84 | val sysUiController = rememberSystemUiController() 85 | SideEffect { 86 | sysUiController.setSystemBarsColor( 87 | color = colors.uiBackground.copy(alpha = AlphaNearOpaque) 88 | ) 89 | } 90 | 91 | ProvidePlayColors(colors) { 92 | MaterialTheme( 93 | colors = debugColors(darkTheme), 94 | typography = Typography, 95 | shapes = Shapes, 96 | content = content 97 | ) 98 | } 99 | } 100 | 101 | object PlayTheme { 102 | val colors: PlayColorPalette 103 | @Composable 104 | get() = LocalPlayColor.current 105 | } 106 | 107 | /** 108 | * Play custom Color Palette 109 | */ 110 | @Stable 111 | class PlayColorPalette( 112 | gradient6_1: List, 113 | gradient6_2: List, 114 | gradient3_1: List, 115 | gradient3_2: List, 116 | gradient2_1: List, 117 | gradient2_2: List, 118 | brand: Color, 119 | accent: Color, 120 | accentDark: Color, 121 | iconTint: Color, 122 | uiBackground: Color, 123 | uiBorder: Color, 124 | uiFloated: Color, 125 | interactivePrimary: List = gradient2_1, 126 | interactiveSecondary: List = gradient2_2, 127 | interactiveMask: List = gradient6_1, 128 | textPrimary: Color = brand, 129 | textSecondaryDark: Color, 130 | textSecondary: Color, 131 | textHelp: Color, 132 | textInteractive: Color, 133 | textLink: Color, 134 | iconPrimary: Color = brand, 135 | iconSecondary: Color, 136 | iconInteractive: Color, 137 | iconInteractiveInactive: Color, 138 | error: Color, 139 | notificationBadge: Color = error, 140 | progressIndicatorBg: Color, 141 | switchColor: Color, 142 | isDark: Boolean 143 | ) { 144 | var gradient6_1 by mutableStateOf(gradient6_1) 145 | private set 146 | var gradient6_2 by mutableStateOf(gradient6_2) 147 | private set 148 | var gradient3_1 by mutableStateOf(gradient3_1) 149 | private set 150 | var gradient3_2 by mutableStateOf(gradient3_2) 151 | private set 152 | var gradient2_1 by mutableStateOf(gradient2_1) 153 | private set 154 | var gradient2_2 by mutableStateOf(gradient2_2) 155 | private set 156 | var brand by mutableStateOf(brand) 157 | private set 158 | var accent by mutableStateOf(accent) 159 | private set 160 | var accentDark by mutableStateOf(accentDark) 161 | private set 162 | var iconTint by mutableStateOf(iconTint) 163 | private set 164 | var uiBackground by mutableStateOf(uiBackground) 165 | private set 166 | var uiBorder by mutableStateOf(uiBorder) 167 | private set 168 | var uiFloated by mutableStateOf(uiFloated) 169 | private set 170 | var interactivePrimary by mutableStateOf(interactivePrimary) 171 | private set 172 | var interactiveSecondary by mutableStateOf(interactiveSecondary) 173 | private set 174 | var interactiveMask by mutableStateOf(interactiveMask) 175 | private set 176 | var textPrimary by mutableStateOf(textPrimary) 177 | private set 178 | var textSecondary by mutableStateOf(textSecondary) 179 | private set 180 | var textSecondaryDark by mutableStateOf(textSecondaryDark) 181 | private set 182 | var textHelp by mutableStateOf(textHelp) 183 | private set 184 | var textInteractive by mutableStateOf(textInteractive) 185 | private set 186 | var textLink by mutableStateOf(textLink) 187 | private set 188 | var iconPrimary by mutableStateOf(iconPrimary) 189 | private set 190 | var iconSecondary by mutableStateOf(iconSecondary) 191 | private set 192 | var iconInteractive by mutableStateOf(iconInteractive) 193 | private set 194 | var iconInteractiveInactive by mutableStateOf(iconInteractiveInactive) 195 | private set 196 | var error by mutableStateOf(error) 197 | private set 198 | var notificationBadge by mutableStateOf(notificationBadge) 199 | private set 200 | var progressIndicatorBg by mutableStateOf(progressIndicatorBg) 201 | private set 202 | var switchColor by mutableStateOf(switchColor) 203 | private set 204 | var isDark by mutableStateOf(isDark) 205 | private set 206 | 207 | fun update(other: PlayColorPalette) { 208 | gradient6_1 = other.gradient6_1 209 | gradient6_2 = other.gradient6_2 210 | gradient3_1 = other.gradient3_1 211 | gradient3_2 = other.gradient3_2 212 | gradient2_1 = other.gradient2_1 213 | gradient2_2 = other.gradient2_2 214 | brand = other.brand 215 | uiBackground = other.uiBackground 216 | uiBorder = other.uiBorder 217 | uiFloated = other.uiFloated 218 | interactivePrimary = other.interactivePrimary 219 | interactiveSecondary = other.interactiveSecondary 220 | interactiveMask = other.interactiveMask 221 | textPrimary = other.textPrimary 222 | textSecondary = other.textSecondary 223 | textHelp = other.textHelp 224 | textInteractive = other.textInteractive 225 | textLink = other.textLink 226 | iconPrimary = other.iconPrimary 227 | iconSecondary = other.iconSecondary 228 | iconInteractive = other.iconInteractive 229 | iconInteractiveInactive = other.iconInteractiveInactive 230 | error = other.error 231 | notificationBadge = other.notificationBadge 232 | switchColor = other.switchColor 233 | isDark = other.isDark 234 | } 235 | } 236 | 237 | @Composable 238 | fun ProvidePlayColors( 239 | colors: PlayColorPalette, 240 | content: @Composable () -> Unit 241 | ) { 242 | val colorPalette = remember { colors } 243 | colorPalette.update(colors) 244 | CompositionLocalProvider(LocalPlayColor provides colorPalette, content = content) 245 | } 246 | 247 | private val LocalPlayColor = staticCompositionLocalOf { 248 | error("No PlayColorPalette provided") 249 | } 250 | 251 | /** 252 | * A Material [Colors] implementation which sets all colors to [debugColor] to discourage usage of 253 | * [MaterialTheme.colors] in preference to [PlayTheme.colors]. 254 | */ 255 | fun debugColors( 256 | darkTheme: Boolean, 257 | debugColor: Color = Color.Red 258 | ) = Colors( 259 | primary = debugColor, 260 | primaryVariant = debugColor, 261 | secondary = debugColor, 262 | secondaryVariant = debugColor, 263 | background = debugColor, 264 | surface = debugColor, 265 | error = debugColor, 266 | onPrimary = debugColor, 267 | onSecondary = debugColor, 268 | onBackground = debugColor, 269 | onSurface = debugColor, 270 | onError = debugColor, 271 | isLight = !darkTheme 272 | ) 273 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.Font 6 | import androidx.compose.ui.text.font.FontFamily 7 | import androidx.compose.ui.text.font.FontWeight 8 | import androidx.compose.ui.unit.sp 9 | import com.example.play.R.font 10 | 11 | private val ProductSans = FontFamily( 12 | Font(font.product_sans_regular, FontWeight.Normal), 13 | Font(font.product_sans_bold, FontWeight.Bold) 14 | ) 15 | 16 | val Typography = Typography( 17 | h1 = TextStyle( 18 | fontFamily = ProductSans, 19 | fontSize = 96.sp, 20 | fontWeight = FontWeight.Normal, 21 | lineHeight = 117.sp, 22 | letterSpacing = (-1.5).sp 23 | ), 24 | h2 = TextStyle( 25 | fontFamily = ProductSans, 26 | fontSize = 60.sp, 27 | fontWeight = FontWeight.Normal, 28 | lineHeight = 73.sp, 29 | letterSpacing = (-0.5).sp 30 | ), 31 | h3 = TextStyle( 32 | fontFamily = ProductSans, 33 | fontSize = 48.sp, 34 | fontWeight = FontWeight.Normal, 35 | lineHeight = 59.sp 36 | ), 37 | h4 = TextStyle( 38 | fontFamily = ProductSans, 39 | fontSize = 30.sp, 40 | fontWeight = FontWeight.Normal, 41 | lineHeight = 37.sp 42 | ), 43 | h5 = TextStyle( 44 | fontFamily = ProductSans, 45 | fontSize = 24.sp, 46 | fontWeight = FontWeight.Normal, 47 | lineHeight = 29.sp 48 | ), 49 | h6 = TextStyle( 50 | fontFamily = ProductSans, 51 | fontSize = 20.sp, 52 | fontWeight = FontWeight.Normal, 53 | lineHeight = 24.sp 54 | ), 55 | subtitle1 = TextStyle( 56 | fontFamily = ProductSans, 57 | fontSize = 16.sp, 58 | fontWeight = FontWeight.Normal, 59 | lineHeight = 24.sp, 60 | letterSpacing = 0.15.sp 61 | ), 62 | subtitle2 = TextStyle( 63 | fontFamily = ProductSans, 64 | fontSize = 14.sp, 65 | fontWeight = FontWeight.Normal, 66 | lineHeight = 24.sp, 67 | letterSpacing = 0.1.sp 68 | ), 69 | body1 = TextStyle( 70 | fontFamily = ProductSans, 71 | fontSize = 16.sp, 72 | fontWeight = FontWeight.Normal, 73 | lineHeight = 28.sp, 74 | letterSpacing = 0.15.sp 75 | ), 76 | body2 = TextStyle( 77 | fontFamily = ProductSans, 78 | fontSize = 14.sp, 79 | fontWeight = FontWeight.Normal, 80 | lineHeight = 20.sp, 81 | letterSpacing = 0.25.sp 82 | ), 83 | button = TextStyle( 84 | fontFamily = ProductSans, 85 | fontSize = 14.sp, 86 | fontWeight = FontWeight.Normal, 87 | lineHeight = 16.sp, 88 | letterSpacing = 1.25.sp 89 | ), 90 | caption = TextStyle( 91 | fontFamily = ProductSans, 92 | fontSize = 12.sp, 93 | fontWeight = FontWeight.Normal, 94 | lineHeight = 16.sp, 95 | letterSpacing = 0.4.sp 96 | ), 97 | overline = TextStyle( 98 | fontFamily = ProductSans, 99 | fontSize = 12.sp, 100 | fontWeight = FontWeight.Normal, 101 | lineHeight = 16.sp, 102 | letterSpacing = 1.sp 103 | ) 104 | ) 105 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/BottomNav.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui 2 | 3 | import androidx.annotation.FloatRange 4 | import androidx.compose.animation.core.Animatable 5 | import androidx.compose.animation.core.AnimationSpec 6 | import androidx.compose.animation.core.SpringSpec 7 | import androidx.compose.foundation.border 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.BoxScope 10 | import androidx.compose.foundation.layout.Column 11 | import androidx.compose.foundation.layout.Spacer 12 | import androidx.compose.foundation.layout.fillMaxSize 13 | import androidx.compose.foundation.layout.height 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.selection.selectable 16 | import androidx.compose.foundation.shape.RoundedCornerShape 17 | import androidx.compose.material.Icon 18 | import androidx.compose.material.MaterialTheme 19 | import androidx.compose.material.Text 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.LaunchedEffect 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.draw.clip 27 | import androidx.compose.ui.graphics.Color 28 | import androidx.compose.ui.graphics.Shape 29 | import androidx.compose.ui.graphics.TransformOrigin 30 | import androidx.compose.ui.graphics.graphicsLayer 31 | import androidx.compose.ui.layout.Layout 32 | import androidx.compose.ui.layout.MeasureResult 33 | import androidx.compose.ui.layout.MeasureScope 34 | import androidx.compose.ui.layout.Placeable 35 | import androidx.compose.ui.layout.layoutId 36 | import androidx.compose.ui.res.stringResource 37 | import androidx.compose.ui.unit.Dp 38 | import androidx.compose.ui.unit.dp 39 | import androidx.compose.ui.util.lerp 40 | import androidx.navigation.NavHostController 41 | import androidx.navigation.compose.currentBackStackEntryAsState 42 | import com.example.play.anim.getBottomNavTintState 43 | import com.example.play.anim.getProgressState 44 | import com.example.play.theme.PlayTheme 45 | import com.example.play.ui.components.PlaySurface 46 | import com.google.accompanist.insets.navigationBarsPadding 47 | import java.util.Locale 48 | 49 | private val TextIconSpacing = 5.dp 50 | private val BottomNavHeight = 56.dp 51 | private val BottomNavLabelTransformOrigin = TransformOrigin(0f, 0.5f) 52 | private val BottomNavIndicatorShape = RoundedCornerShape(percent = 50) 53 | private val BottomNavigationItemPadding = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) 54 | 55 | // Bottom Navigation 56 | @Composable 57 | fun PlayBottomNav( 58 | navController: NavHostController, 59 | tabs: Array, 60 | onTabSelected: (String, String) -> Unit, 61 | color: Color = PlayTheme.colors.iconPrimary, 62 | contentColor: Color = PlayTheme.colors.iconInteractive 63 | ) { 64 | val navBackStackEntry by navController.currentBackStackEntryAsState() 65 | val currentRoute = navBackStackEntry?.destination?.route ?: BottomNavTabs.Games.route 66 | val routes = remember { BottomNavTabs.values().map { it.route } } 67 | 68 | PlaySurface( 69 | color = color, 70 | contentColor = contentColor 71 | ) { 72 | val springSpec = remember { getAnimSpec() } 73 | if (currentRoute in routes) { 74 | Column { 75 | PlayBottomNavLayout( 76 | selectedIndex = routes.indexOf(currentRoute), 77 | itemCount = tabs.size, 78 | indicator = { PLayBottomNavIndicator() }, 79 | animSpec = springSpec, 80 | modifier = Modifier.navigationBarsPadding(left = false, right = false) 81 | ) { 82 | tabs.forEach { tab -> 83 | val selected = currentRoute == tab.route 84 | val tint by getBottomNavTintState(selected = selected) 85 | 86 | PlayBottomNavigationItem( 87 | icon = { 88 | Icon( 89 | imageVector = tab.icon, 90 | tint = tint, 91 | contentDescription = null 92 | ) 93 | }, 94 | text = { 95 | Text( 96 | text = stringResource(tab.title).toUpperCase(Locale.ROOT), 97 | color = tint, 98 | style = MaterialTheme.typography.button, 99 | maxLines = 1 100 | ) 101 | }, 102 | selected = selected, 103 | onSelected = { onTabSelected(tab.route, currentRoute) }, 104 | animSpec = springSpec, 105 | modifier = BottomNavigationItemPadding 106 | .clip(BottomNavIndicatorShape) 107 | ) 108 | } 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | // Bottom Navigation Item 116 | @Composable 117 | fun PlayBottomNavigationItem( 118 | icon: @Composable BoxScope.() -> Unit, 119 | text: @Composable BoxScope.() -> Unit, 120 | selected: Boolean, 121 | onSelected: () -> Unit, 122 | animSpec: AnimationSpec, 123 | modifier: Modifier = Modifier 124 | ) { 125 | Box( 126 | modifier = modifier.selectable(selected = selected, onClick = onSelected), 127 | contentAlignment = Alignment.Center 128 | ) { 129 | // Animate the icon/text positions within the item based on selection 130 | val animationProgress by getProgressState(selected = selected, animSpec = animSpec) 131 | PlayBottomNavItemLayout( 132 | icon = icon, 133 | text = text, 134 | animationProgress = animationProgress 135 | ) 136 | } 137 | } 138 | 139 | // Bottom Navigation Item Layout 140 | @Composable 141 | private fun PlayBottomNavItemLayout( 142 | icon: @Composable BoxScope.() -> Unit, 143 | text: @Composable BoxScope.() -> Unit, 144 | @FloatRange(from = 0.0, to = 1.0) animationProgress: Float 145 | ) { 146 | Layout( 147 | content = { 148 | Box(Modifier.layoutId("icon"), content = icon) 149 | val scale = lerp(start = 0.2f, stop = 1f, fraction = animationProgress) 150 | Box( 151 | modifier = Modifier 152 | .padding(start = TextIconSpacing) 153 | .layoutId("text") 154 | .graphicsLayer { 155 | alpha = animationProgress 156 | scaleX = scale 157 | scaleY = scale 158 | transformOrigin = BottomNavLabelTransformOrigin 159 | }, 160 | content = text 161 | ) 162 | } 163 | ) { measurable, constraints -> 164 | val iconPlaceable = measurable.first { it.layoutId == "icon" } 165 | .measure(constraints) 166 | val textPlaceable = measurable.first { it.layoutId == "text" } 167 | .measure(constraints) 168 | 169 | placeTextAndIcon( 170 | textPlaceable, 171 | iconPlaceable, 172 | constraints.maxWidth, 173 | constraints.maxHeight, 174 | animationProgress 175 | ) 176 | } 177 | } 178 | 179 | // Place text and icon using Placeable 180 | private fun MeasureScope.placeTextAndIcon( 181 | textPlaceable: Placeable, 182 | iconPlaceable: Placeable, 183 | width: Int, 184 | height: Int, 185 | @FloatRange(from = 0.0, to = 1.0) animationProgress: Float 186 | ): MeasureResult { 187 | val iconY = (height - iconPlaceable.height) / 2 188 | val textY = (height - textPlaceable.height) / 2 189 | 190 | val textWidth = textPlaceable.width * animationProgress 191 | val iconX = (width - textWidth - iconPlaceable.width) / 2 192 | val textX = iconX + iconPlaceable.width 193 | 194 | return layout(width, height) { 195 | iconPlaceable.place(iconX.toInt(), iconY) 196 | if (animationProgress != 0f) { 197 | textPlaceable.place(textX.toInt(), textY) 198 | } 199 | } 200 | } 201 | 202 | // Bottom Navigation Layout 203 | @Composable 204 | private fun PlayBottomNavLayout( 205 | selectedIndex: Int, 206 | itemCount: Int, 207 | animSpec: AnimationSpec, 208 | indicator: @Composable BoxScope.() -> Unit, 209 | modifier: Modifier = Modifier, 210 | content: @Composable () -> Unit 211 | ) { 212 | // Track how "selected" each item is [0, 1] 213 | val selectionFractions = remember(itemCount) { 214 | List(itemCount) { i -> 215 | Animatable(if (i == selectedIndex) 1f else 0f) 216 | } 217 | } 218 | 219 | selectionFractions.forEachIndexed { index, selectionFraction -> 220 | val target = if (index == selectedIndex) 1f else 0f 221 | LaunchedEffect(target, animSpec) { 222 | selectionFraction.animateTo(target, animSpec) 223 | } 224 | } 225 | 226 | // Animate the position of the indicator 227 | val indicatorIndex = remember { Animatable(0f) } 228 | val targetIndicatorIndex = selectedIndex.toFloat() 229 | LaunchedEffect(targetIndicatorIndex) { 230 | indicatorIndex.animateTo(targetIndicatorIndex, animSpec) 231 | } 232 | 233 | Layout( 234 | modifier = modifier.height(BottomNavHeight), 235 | content = { 236 | content() 237 | Box(Modifier.layoutId("indicator"), content = indicator) 238 | } 239 | ) { measurable, constraints -> 240 | check(itemCount == (measurable.size - 1)) // account for indicator 241 | 242 | // Divide the width into n + 1 slots and give the selected item 2 slots 243 | val unselectedWidth = constraints.maxWidth / (itemCount + 1) 244 | val selectedWidth = constraints.maxWidth - (itemCount - 1) * unselectedWidth 245 | val indicatorMeasurable = measurable.first { it.layoutId == "indicator" } 246 | 247 | val itemPlaceable = measurable 248 | .filterNot { it == indicatorMeasurable } 249 | .mapIndexed { index, measurables -> 250 | // Animate item's width based upon the selection amount 251 | val width = lerp(unselectedWidth, selectedWidth, selectionFractions[index].value) 252 | measurables.measure( 253 | constraints.copy( 254 | minWidth = width, 255 | maxWidth = width 256 | ) 257 | ) 258 | } 259 | 260 | val indicatorPlaceable = indicatorMeasurable.measure( 261 | constraints.copy( 262 | minWidth = selectedWidth, 263 | maxWidth = selectedWidth 264 | ) 265 | ) 266 | 267 | layout( 268 | width = constraints.maxWidth, 269 | height = itemPlaceable.maxByOrNull { it.height }?.height ?: 0 270 | ) { 271 | val indicatorLeft = indicatorIndex.value * unselectedWidth 272 | indicatorPlaceable.place(x = indicatorLeft.toInt(), y = 0) 273 | var x = 0 274 | itemPlaceable.forEach { placeable -> 275 | placeable.place(x = x, y = 0) 276 | x += placeable.width 277 | } 278 | } 279 | } 280 | } 281 | 282 | // Bottom Navigation Indicator 283 | @Composable 284 | private fun PLayBottomNavIndicator( 285 | strokeWidth: Dp = 1.dp, 286 | color: Color = PlayTheme.colors.iconInteractive, 287 | shape: Shape = BottomNavIndicatorShape 288 | ) { 289 | Spacer( 290 | modifier = Modifier 291 | .fillMaxSize() 292 | .then(BottomNavigationItemPadding) 293 | .border(strokeWidth, color, shape) 294 | ) 295 | } 296 | 297 | private fun getAnimSpec(): SpringSpec { 298 | return SpringSpec( 299 | // Determined experimentally 300 | stiffness = 200f, 301 | dampingRatio = 0.9f 302 | ) 303 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.core.view.WindowCompat 7 | 8 | class MainActivity : ComponentActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | 12 | // This app draws behind the system bars, so we want to handle fitting system windows 13 | WindowCompat.setDecorFitsSystemWindows(window, true) 14 | 15 | setContent { 16 | PlayApp { finish() } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/NavGraph.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.play.ui 18 | 19 | import androidx.activity.compose.BackHandler 20 | import androidx.annotation.StringRes 21 | import androidx.compose.material.icons.Icons 22 | import androidx.compose.material.icons.outlined.Apps 23 | import androidx.compose.material.icons.outlined.Book 24 | import androidx.compose.material.icons.outlined.Games 25 | import androidx.compose.material.icons.outlined.Movie 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.graphics.vector.ImageVector 29 | import androidx.lifecycle.Lifecycle 30 | import androidx.navigation.NavBackStackEntry 31 | import androidx.navigation.NavGraphBuilder 32 | import androidx.navigation.NavHostController 33 | import androidx.navigation.NavType 34 | import androidx.navigation.compose.NavHost 35 | import androidx.navigation.compose.composable 36 | import androidx.navigation.compose.navArgument 37 | import androidx.navigation.compose.rememberNavController 38 | import androidx.navigation.navigation 39 | import com.example.play.R 40 | import com.example.play.ui.apps.Apps 41 | import com.example.play.ui.books.Books 42 | import com.example.play.ui.details.AppDetails 43 | import com.example.play.ui.games.Games 44 | import com.example.play.ui.movies.Movies 45 | 46 | /** 47 | * Main destinations used in the ([PlayApp]). 48 | */ 49 | object MainDestinations { 50 | const val MAIN_ROUTE = "main" 51 | const val APP_DETAILS_ROUTE = "details" 52 | const val APP_ID_KEY = "appId" 53 | } 54 | 55 | /** 56 | * Bottom nav destinations used in the ([PlayApp]). 57 | */ 58 | object BottomNavDestinations { 59 | const val GAMES_ROUTE = "bottom/nav/games" 60 | const val APPS_ROUTE = "bottom/nav/apps" 61 | const val MOVIES_ROUTE = "bottom/nav/movies" 62 | const val BOOKS_ROUTE = "bottom/nav/books" 63 | } 64 | 65 | @Composable 66 | fun NavGraph( 67 | modifier: Modifier = Modifier, 68 | finishActivity: () -> Unit = {}, 69 | navController: NavHostController = rememberNavController(), 70 | actions: MainActions, 71 | startDestination: String = MainDestinations.MAIN_ROUTE, 72 | shouldShowAppBar: (Boolean) -> Unit 73 | ) { 74 | NavHost( 75 | navController = navController, 76 | startDestination = startDestination 77 | ) { 78 | 79 | /* 80 | * Add the PlayApp composable to the NavGraphBuilder 81 | */ 82 | composable( 83 | route = MainDestinations.MAIN_ROUTE 84 | ) { 85 | PlayApp { finishActivity() } 86 | } 87 | 88 | /* 89 | * Add the AppDetails composable to the NavGraphBuilder 90 | */ 91 | composable( 92 | route = "${MainDestinations.APP_DETAILS_ROUTE}/{${MainDestinations.APP_ID_KEY}}", 93 | arguments = listOf(navArgument(MainDestinations.APP_ID_KEY) { type = NavType.LongType }) 94 | ) { backStackEntry: NavBackStackEntry -> 95 | // Intercept back in App Details 96 | BackHandler { 97 | shouldShowAppBar(true) 98 | navController.navigateUp() 99 | } 100 | AppDetails( 101 | backStackEntry.arguments?.getLong(MainDestinations.APP_ID_KEY), 102 | upPress = { actions.upPress(backStackEntry) } 103 | ) 104 | } 105 | 106 | /* 107 | * Construct a nested NavGraph with app categories 108 | */ 109 | navigation( 110 | route = MainDestinations.MAIN_ROUTE, 111 | startDestination = BottomNavTabs.Games.route 112 | ) { 113 | categories( 114 | onAppSelected = actions.openAppDetails, 115 | modifier = modifier 116 | ) 117 | } 118 | } 119 | } 120 | 121 | /** 122 | * Models the navigation actions in the app. 123 | */ 124 | class MainActions( 125 | navController: NavHostController, 126 | shouldShowAppBar: (Boolean) -> Unit 127 | ) { 128 | val openAppDetails = { appId: Long, from: NavBackStackEntry -> 129 | // In order to discard duplicated navigation events, we check the Lifecycle 130 | if (from.lifecycleIsResumed()) { 131 | shouldShowAppBar(false) 132 | navController.navigate(route = "${MainDestinations.APP_DETAILS_ROUTE}/$appId") { 133 | //popUpTo = navController.graph.startDestination 134 | launchSingleTop = true 135 | } 136 | } 137 | } 138 | val switchAppCategory = { tabRoute: String, currentRoute: String -> 139 | //shouldShowAppBar(true) 140 | if (tabRoute != currentRoute) { 141 | navController.navigate(route = tabRoute) { 142 | // Pop up to the start destination of the graph to avoid building up a large 143 | // stack of destinations on the back stack as users select items 144 | popUpTo(navController.graph.startDestinationId) 145 | // Avoid multiple copies of the same destination when re-selecting the same item 146 | launchSingleTop = true 147 | } 148 | } 149 | } 150 | val upPress: (rom: NavBackStackEntry) -> Unit = { from -> 151 | // In order to discard duplicated navigation events, we check the Lifecycle 152 | if (from.lifecycleIsResumed()) { 153 | shouldShowAppBar(true) 154 | navController.navigateUp() 155 | } 156 | } 157 | } 158 | 159 | /** 160 | * If the lifecycle is not resumed it means this NavBackStackEntry already processed a nav event. 161 | * 162 | * This is used to de-duplicate navigation events. 163 | */ 164 | private fun NavBackStackEntry.lifecycleIsResumed() = 165 | this.lifecycle.currentState == Lifecycle.State.RESUMED 166 | 167 | fun NavGraphBuilder.categories( 168 | onAppSelected: (Long, NavBackStackEntry) -> Unit, 169 | modifier: Modifier = Modifier 170 | ) { 171 | composable(BottomNavTabs.Games.route) { from -> 172 | Games( 173 | onAppSelected = { id -> onAppSelected(id, from) }, 174 | modifier = modifier 175 | ) 176 | } 177 | composable(BottomNavTabs.Apps.route) { from -> 178 | Apps( 179 | onAppSelected = { id -> onAppSelected(id, from) }, 180 | modifier = modifier 181 | ) 182 | } 183 | composable(BottomNavTabs.Movies.route) { 184 | Movies(modifier = modifier) 185 | } 186 | composable(BottomNavTabs.Books.route) { 187 | Books() 188 | } 189 | } 190 | 191 | enum class AppsCategory { 192 | ForYou, 193 | TopCharts, 194 | Categories, 195 | EditorsChoice, 196 | EarlyAccess 197 | } 198 | 199 | enum class MoviesCategory { 200 | ForYou, 201 | TopSelling, 202 | NewReleases 203 | } 204 | 205 | enum class BottomNavTabs( 206 | @StringRes val title: Int, 207 | val icon: ImageVector, 208 | val route: String 209 | ) { 210 | Games(R.string.home_games, Icons.Outlined.Games, BottomNavDestinations.GAMES_ROUTE), 211 | Apps(R.string.home_apps, Icons.Outlined.Apps, BottomNavDestinations.APPS_ROUTE), 212 | Movies(R.string.home_movies, Icons.Outlined.Movie, BottomNavDestinations.MOVIES_ROUTE), 213 | Books(R.string.home_books, Icons.Outlined.Book, BottomNavDestinations.BOOKS_ROUTE) 214 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/PlayApp.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.tooling.preview.Preview 9 | import androidx.navigation.compose.rememberNavController 10 | import com.example.play.theme.PlayTheme 11 | import com.example.play.ui.components.PlayScaffold 12 | import com.example.play.ui.components.PlayToolBar 13 | import com.google.accompanist.insets.ProvideWindowInsets 14 | 15 | @Composable 16 | fun PlayApp(finishActivity: () -> Unit) { 17 | 18 | /* 19 | * NavController is stateful and keeps track of the back stack of composables 20 | * that make up the screens in your app and the state of each screen. 21 | */ 22 | val navController = rememberNavController() 23 | val (shouldShowAppBar, updateAppBarVisibility) = remember { mutableStateOf(true) } 24 | val navActions = remember(navController) { MainActions(navController, updateAppBarVisibility) } 25 | 26 | ProvideWindowInsets { 27 | PlayTheme { 28 | val tabItems = remember { BottomNavTabs.values() } 29 | PlayScaffold( 30 | topBar = { PlayToolBar(shouldShow = shouldShowAppBar) }, 31 | bottomBar = { 32 | PlayBottomNav( 33 | navController = navController, 34 | tabs = tabItems, 35 | onTabSelected = navActions.switchAppCategory 36 | ) 37 | } 38 | ) { innerPaddingModifier -> 39 | NavGraph( 40 | finishActivity = finishActivity, 41 | navController = navController, 42 | modifier = Modifier.padding(innerPaddingModifier), 43 | actions = navActions, 44 | shouldShowAppBar = updateAppBarVisibility 45 | ) 46 | } 47 | } 48 | } 49 | } 50 | 51 | @Preview 52 | @Composable 53 | private fun PlayAppPreview() { 54 | PlayTheme { 55 | PlayApp {} 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/Tabs.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.foundation.layout.width 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material.ScrollableTabRow 9 | import androidx.compose.material.Tab 10 | import androidx.compose.material.TabPosition 11 | import androidx.compose.material.TabRowDefaults.tabIndicatorOffset 12 | import androidx.compose.material.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.text.TextStyle 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.tooling.preview.Preview 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.unit.sp 22 | import com.example.play.R 23 | import com.example.play.theme.PlayTheme 24 | import com.example.play.ui.AppsCategory.Categories 25 | import com.example.play.ui.AppsCategory.EarlyAccess 26 | import com.example.play.ui.AppsCategory.EditorsChoice 27 | import com.example.play.ui.AppsCategory.ForYou 28 | import com.example.play.ui.AppsCategory.TopCharts 29 | import com.example.play.ui.MoviesCategory.NewReleases 30 | import com.example.play.ui.MoviesCategory.TopSelling 31 | 32 | @Composable 33 | fun AppsCategoryTabs( 34 | categories: List, 35 | selectedCategory: AppsCategory, 36 | onCategorySelected: (AppsCategory) -> Unit 37 | ) { 38 | val selectedIndex = categories.indexOfFirst { it == selectedCategory } 39 | val indicator = @Composable { tabPositions: List -> 40 | HomeCategoryTabIndicator( 41 | Modifier.tabIndicatorOffset(tabPositions[selectedIndex]) 42 | ) 43 | } 44 | 45 | ScrollableTabRow( 46 | selectedTabIndex = selectedIndex, 47 | indicator = indicator, 48 | backgroundColor = PlayTheme.colors.uiBackground, 49 | edgePadding = 32.dp 50 | ) { 51 | categories.forEachIndexed { index, category -> 52 | Tab( 53 | selected = index == selectedIndex, 54 | onClick = { onCategorySelected(category) }, 55 | modifier = Modifier.background(color = PlayTheme.colors.uiBackground), 56 | text = { 57 | Text( 58 | text = when (category) { 59 | ForYou -> stringResource(R.string.for_you) 60 | TopCharts -> stringResource(R.string.top_charts) 61 | Categories -> stringResource(R.string.categories) 62 | EditorsChoice -> stringResource(R.string.editors_choice) 63 | EarlyAccess -> stringResource(R.string.early_access) 64 | }, 65 | color = if (index == selectedIndex) { 66 | PlayTheme.colors.accent 67 | } else { 68 | PlayTheme.colors.textPrimary 69 | }, 70 | style = TextStyle( 71 | fontWeight = FontWeight.Normal, 72 | fontSize = 12.sp 73 | ) 74 | ) 75 | } 76 | ) 77 | } 78 | } 79 | } 80 | 81 | @Composable 82 | fun MoviesCategoryTabs( 83 | categories: List, 84 | selectedCategory: MoviesCategory, 85 | onCategorySelected: (MoviesCategory) -> Unit 86 | ) { 87 | val selectedIndex = categories.indexOfFirst { it == selectedCategory } 88 | val indicator = @Composable { tabPositions: List -> 89 | HomeCategoryTabIndicator( 90 | Modifier.tabIndicatorOffset(tabPositions[selectedIndex]) 91 | ) 92 | } 93 | 94 | ScrollableTabRow( 95 | selectedTabIndex = selectedIndex, 96 | indicator = indicator, 97 | backgroundColor = PlayTheme.colors.uiBackground, 98 | edgePadding = 32.dp 99 | ) { 100 | categories.forEachIndexed { index, category -> 101 | Tab( 102 | selected = index == selectedIndex, 103 | onClick = { onCategorySelected(category) }, 104 | modifier = Modifier.background(color = PlayTheme.colors.uiBackground), 105 | text = { 106 | Text( 107 | text = when (category) { 108 | MoviesCategory.ForYou -> stringResource(R.string.for_you) 109 | TopSelling -> stringResource(R.string.top_selling) 110 | NewReleases -> stringResource(R.string.new_releases) 111 | }, 112 | color = if (index == selectedIndex) { 113 | PlayTheme.colors.accent 114 | } else { 115 | PlayTheme.colors.textPrimary 116 | }, 117 | style = TextStyle( 118 | fontWeight = FontWeight.Normal, 119 | fontSize = 12.sp 120 | ) 121 | ) 122 | } 123 | ) 124 | } 125 | } 126 | } 127 | 128 | @Composable 129 | fun HomeCategoryTabIndicator( 130 | modifier: Modifier = Modifier, 131 | color: Color = PlayTheme.colors.accent 132 | ) { 133 | Spacer( 134 | modifier 135 | .width(5.dp) 136 | .height(3.dp) 137 | .background(color, RoundedCornerShape(percent = 50)) 138 | ) 139 | } 140 | 141 | @Preview 142 | @Composable 143 | fun AppCategoryTabsPreview() { 144 | PlayTheme { 145 | AppsCategoryTabs( 146 | categories = listOf( 147 | ForYou, TopCharts, Categories, EditorsChoice, EarlyAccess 148 | ), 149 | selectedCategory = ForYou, 150 | onCategorySelected = {} 151 | ) 152 | } 153 | } 154 | 155 | @Preview 156 | @Composable 157 | fun AppCategoryTabsDarkPreview() { 158 | PlayTheme(darkTheme = true) { 159 | AppsCategoryTabs( 160 | categories = listOf( 161 | ForYou, TopCharts, Categories, EditorsChoice, EarlyAccess 162 | ), 163 | selectedCategory = ForYou, 164 | onCategorySelected = {} 165 | ) 166 | } 167 | } 168 | 169 | @Preview 170 | @Composable 171 | fun MovieCategoryTabsPreview() { 172 | PlayTheme { 173 | MoviesCategoryTabs( 174 | categories = listOf( 175 | MoviesCategory.ForYou, TopSelling, NewReleases 176 | ), 177 | selectedCategory = MoviesCategory.ForYou, 178 | onCategorySelected = {} 179 | ) 180 | } 181 | } 182 | 183 | @Preview 184 | @Composable 185 | fun MovieCategoryTabsDarkPreview() { 186 | PlayTheme(darkTheme = true) { 187 | MoviesCategoryTabs( 188 | categories = listOf( 189 | MoviesCategory.ForYou, TopSelling, NewReleases 190 | ), 191 | selectedCategory = MoviesCategory.ForYou, 192 | onCategorySelected = {} 193 | ) 194 | } 195 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/apps/Apps.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.apps 2 | 3 | import androidx.compose.animation.Crossfade 4 | import androidx.compose.animation.core.LinearOutSlowInEasing 5 | import androidx.compose.animation.core.TweenSpec 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.saveable.rememberSaveable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.tooling.preview.Preview 14 | import com.example.play.data.AppRepo 15 | import com.example.play.theme.PlayTheme 16 | import com.example.play.ui.AppsCategory 17 | import com.example.play.ui.AppsCategory.Categories 18 | import com.example.play.ui.AppsCategory.EarlyAccess 19 | import com.example.play.ui.AppsCategory.EditorsChoice 20 | import com.example.play.ui.AppsCategory.ForYou 21 | import com.example.play.ui.AppsCategory.TopCharts 22 | import com.example.play.ui.apps.applist.ForYouLayout 23 | import com.example.play.ui.apps.applist.TopChartsLayout 24 | import com.example.play.ui.components.PlaySurface 25 | import com.example.play.ui.AppsCategoryTabs 26 | import com.google.accompanist.insets.navigationBarsPadding 27 | 28 | @Composable 29 | fun Apps( 30 | onAppSelected: (Long) -> Unit, 31 | modifier: Modifier = Modifier, 32 | appsCategories: List = getAppCategoriesList() 33 | ) { 34 | val forYouData = remember { AppRepo.getForYouApps() } 35 | val topChartsData = remember { AppRepo.getTopChartsApps() } 36 | val (currentCategory, setCurrentCategory) = rememberSaveable { mutableStateOf(ForYou) } 37 | 38 | PlaySurface(modifier = modifier.fillMaxSize()) { 39 | Column(modifier = Modifier.navigationBarsPadding(left = true, right = true)) { 40 | AppsCategoryTabs( 41 | categories = appsCategories, 42 | selectedCategory = currentCategory, 43 | onCategorySelected = setCurrentCategory 44 | ) 45 | val tweenSpec = remember { getAnimSpec() } 46 | Crossfade(currentCategory, animationSpec = tweenSpec) { category -> 47 | when (category) { 48 | ForYou -> ForYouLayout(forYouData, onAppSelected = onAppSelected) 49 | TopCharts -> TopChartsLayout(topChartsData, onAppSelected = onAppSelected) 50 | else -> ForYouLayout(forYouData, onAppSelected = onAppSelected) 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | private fun getAnimSpec(): TweenSpec { 58 | return TweenSpec( 59 | durationMillis = 600, 60 | easing = LinearOutSlowInEasing 61 | ) 62 | } 63 | 64 | private fun getAppCategoriesList() = listOf( 65 | ForYou, TopCharts, Categories, EditorsChoice, EarlyAccess 66 | ) 67 | 68 | @Preview("Apps") 69 | @Composable 70 | fun AppsPreview() { 71 | PlayTheme { 72 | Apps( 73 | onAppSelected = {} 74 | ) 75 | } 76 | } 77 | 78 | @Preview("Apps • Dark Theme") 79 | @Composable 80 | fun AppsDarkPreview() { 81 | PlayTheme(darkTheme = true) { 82 | Apps( 83 | onAppSelected = {} 84 | ) 85 | } 86 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/apps/applist/ForYou.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.apps.applist 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.lazy.LazyColumn 10 | import androidx.compose.foundation.lazy.LazyRow 11 | import androidx.compose.foundation.lazy.items 12 | import androidx.compose.material.Icon 13 | import androidx.compose.material.IconButton 14 | import androidx.compose.material.Text 15 | import androidx.compose.material.icons.Icons 16 | import androidx.compose.material.icons.outlined.ArrowForward 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.key 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.text.TextStyle 22 | import androidx.compose.ui.text.font.FontWeight 23 | import androidx.compose.ui.text.style.TextOverflow 24 | import androidx.compose.ui.tooling.preview.Preview 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.sp 27 | import com.example.play.data.App 28 | import com.example.play.data.AppCollection 29 | import com.example.play.data.AppRepo 30 | import com.example.play.data.CollectionType 31 | import com.example.play.theme.PlayTheme 32 | import com.google.accompanist.insets.navigationBarsPadding 33 | 34 | @Composable 35 | fun ForYouLayout( 36 | data: List, 37 | onAppSelected: (Long) -> Unit, 38 | modifier: Modifier = Modifier 39 | ) { 40 | Spacer( 41 | modifier = Modifier 42 | .height(4.dp) 43 | ) 44 | LazyColumn(modifier = modifier) { 45 | items(data) { appCollection -> 46 | key(appCollection.id) { 47 | ForYou( 48 | appCollection = appCollection, 49 | onAppSelected = onAppSelected 50 | ) 51 | } 52 | } 53 | } 54 | Spacer( 55 | modifier = Modifier 56 | .navigationBarsPadding(left = false, right = false) 57 | .height(8.dp) 58 | ) 59 | } 60 | 61 | @Composable 62 | fun ForYou( 63 | appCollection: AppCollection, 64 | onAppSelected: (Long) -> Unit, 65 | modifier: Modifier = Modifier, 66 | featured: Boolean = true 67 | ) { 68 | Column(modifier = modifier) { 69 | Row( 70 | verticalAlignment = Alignment.CenterVertically, 71 | modifier = Modifier 72 | .padding(start = 24.dp) 73 | ) { 74 | Text( 75 | text = appCollection.name, 76 | style = TextStyle( 77 | fontWeight = FontWeight.Medium, 78 | fontSize = 16.sp, 79 | letterSpacing = 0.15.sp 80 | ), 81 | color = PlayTheme.colors.textPrimary, 82 | maxLines = 1, 83 | overflow = TextOverflow.Ellipsis, 84 | modifier = Modifier.weight(1f) 85 | ) 86 | IconButton( 87 | onClick = {}, 88 | modifier = Modifier.align(Alignment.CenterVertically) 89 | ) { 90 | Icon( 91 | imageVector = Icons.Outlined.ArrowForward, 92 | tint = PlayTheme.colors.iconTint, 93 | contentDescription = null 94 | ) 95 | } 96 | } 97 | if (featured && appCollection.type == CollectionType.Featured) { 98 | FeaturedAppsList(appCollection.apps, onAppSelected) 99 | } else { 100 | AppsList(appCollection.apps, onAppSelected) 101 | } 102 | } 103 | } 104 | 105 | @Composable 106 | private fun FeaturedAppsList( 107 | apps: List, 108 | onAppSelected: (Long) -> Unit 109 | ) { 110 | LazyRow(modifier = Modifier.padding(start = 16.dp)) { 111 | items(apps) { app -> 112 | PlayFeaturedAppItem(app, onAppSelected = onAppSelected) 113 | Spacer(modifier = Modifier.width(1.dp)) 114 | } 115 | } 116 | } 117 | 118 | @Composable 119 | private fun AppsList( 120 | apps: List, 121 | onAppSelected: (Long) -> Unit 122 | ) { 123 | LazyRow(modifier = Modifier.padding(start = 16.dp)) { 124 | items(apps) { app -> 125 | AppItem(app = app, onAppSelected = onAppSelected) 126 | Spacer(modifier = Modifier.width(1.dp)) 127 | } 128 | } 129 | } 130 | 131 | @Preview("For You list preview") 132 | @Composable 133 | fun ForYouListPreview() { 134 | PlayTheme { 135 | ForYouLayout(data = AppRepo.getApps(), onAppSelected = {}) 136 | } 137 | } 138 | 139 | @Preview("For You list dark preview") 140 | @Composable 141 | fun ForYouListDarkPreview() { 142 | PlayTheme(darkTheme = true) { 143 | ForYouLayout(data = AppRepo.getApps(), onAppSelected = {}) 144 | } 145 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/apps/applist/TopCharts.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.apps.applist 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.lazy.LazyColumn 7 | import androidx.compose.foundation.lazy.items 8 | import androidx.compose.material.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.text.TextStyle 15 | import androidx.compose.ui.text.font.FontWeight 16 | import androidx.compose.ui.tooling.preview.Preview 17 | import androidx.compose.ui.unit.dp 18 | import androidx.compose.ui.unit.sp 19 | import com.example.play.data.App 20 | import com.example.play.data.AppRepo 21 | import com.example.play.theme.PlayTheme 22 | import com.example.play.ui.components.FilterBar 23 | import com.example.play.ui.components.Switch 24 | 25 | @Composable 26 | fun TopChartsLayout( 27 | appCollection: List, 28 | onAppSelected: (Long) -> Unit, 29 | modifier: Modifier = Modifier 30 | ) { 31 | val (filterSelected, setFilterSelected: (Int) -> Unit) = remember { mutableStateOf(1) } 32 | Column(modifier = modifier) { 33 | TopChartsHeader(filterSelected, setFilterSelected) 34 | TopChartAppsList( 35 | appCollection.filter { app -> 36 | app.filterCategory == AppRepo.getFilter(filterSelected)?.name 37 | }, 38 | onAppSelected = onAppSelected 39 | ) 40 | } 41 | } 42 | 43 | @Composable 44 | private fun TopChartsHeader( 45 | filterSelected: Int, 46 | setFilterSelected: (Int) -> Unit 47 | ) { 48 | val (switchState, updateSwitchState) = remember { mutableStateOf(true) } 49 | Column { 50 | Row( 51 | verticalAlignment = Alignment.CenterVertically, 52 | modifier = Modifier 53 | .padding(start = 24.dp, end = 24.dp, top = 16.dp, bottom = 8.dp) 54 | ) { 55 | Text( 56 | text = "Show installed apps", 57 | style = TextStyle( 58 | fontWeight = FontWeight.Normal, 59 | fontSize = 14.sp 60 | ), 61 | color = PlayTheme.colors.textSecondary, 62 | modifier = Modifier.weight(1f) 63 | ) 64 | Switch( 65 | switchState = switchState, 66 | updateSwitchState = updateSwitchState, 67 | modifier = Modifier.align(Alignment.CenterVertically) 68 | ) 69 | } 70 | FilterBar(filters = AppRepo.getFilters(), filterSelected, setFilterSelected) 71 | } 72 | } 73 | 74 | @Composable 75 | private fun TopChartAppsList( 76 | apps: List, 77 | onAppSelected: (Long) -> Unit, 78 | modifier: Modifier = Modifier 79 | ) { 80 | LazyColumn(modifier = modifier) { 81 | items(apps) { app -> 82 | TopChartAppItem(app, onAppSelected = onAppSelected) 83 | } 84 | } 85 | } 86 | 87 | @Preview("Top Charts preview") 88 | @Composable 89 | fun TopChartsPreview() { 90 | PlayTheme { 91 | TopChartsLayout( 92 | appCollection = AppRepo.getTopChartsApps(), 93 | onAppSelected = {} 94 | ) 95 | } 96 | } 97 | 98 | @Preview("Top Charts dark preview") 99 | @Composable 100 | fun TopChartsDarkPreview() { 101 | PlayTheme(darkTheme = true) { 102 | TopChartsLayout( 103 | appCollection = AppRepo.getTopChartsApps(), 104 | onAppSelected = {} 105 | ) 106 | } 107 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/books/Books.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.books 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.text.TextStyle 11 | import androidx.compose.ui.text.font.FontWeight 12 | import androidx.compose.ui.text.style.TextAlign.Center 13 | import androidx.compose.ui.tooling.preview.Preview 14 | import androidx.compose.ui.unit.dp 15 | import androidx.compose.ui.unit.sp 16 | import com.example.play.theme.PlayTheme 17 | 18 | @Composable 19 | fun Books( 20 | ) { 21 | Column( 22 | modifier = Modifier.padding(horizontal = 16.dp), 23 | horizontalAlignment = Alignment.CenterHorizontally 24 | ) { 25 | //LottieLoadingView() 26 | Text( 27 | text = "Coming Soon", 28 | style = TextStyle( 29 | fontWeight = FontWeight.Medium, 30 | fontSize = 16.sp, 31 | letterSpacing = 0.15.sp 32 | ), 33 | color = PlayTheme.colors.textSecondary, 34 | textAlign = Center, 35 | modifier = Modifier.padding(16.dp) 36 | ) 37 | } 38 | } 39 | 40 | /** 41 | * Commented out because of build issue in snapshot version of lottie compose 42 | */ 43 | //@Composable 44 | //fun LottieLoadingView(modifier: Modifier = Modifier) { 45 | // val animationSpec = remember { LottieAnimationSpec.RawRes(R.raw.working) } 46 | // LottieAnimation( 47 | // animationSpec, 48 | // modifier = Modifier 49 | // .size(250.dp) 50 | // .then(modifier) 51 | // ) 52 | //} 53 | 54 | @Preview 55 | @Composable 56 | private fun PlayBooksPreview() { 57 | PlayTheme { 58 | Books() 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/components/AppBarLayout.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.components 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.material.Icon 8 | import androidx.compose.material.IconButton 9 | import androidx.compose.material.icons.Icons.Outlined 10 | import androidx.compose.material.icons.outlined.ArrowBack 11 | import androidx.compose.material.icons.outlined.MoreVert 12 | import androidx.compose.material.icons.outlined.Search 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.tooling.preview.Preview 16 | import androidx.compose.ui.unit.dp 17 | import com.example.play.theme.PlayTheme 18 | import com.google.accompanist.insets.statusBarsPadding 19 | 20 | @Composable 21 | fun AppBarLayout( 22 | upPress: () -> Unit 23 | ) { 24 | Row( 25 | modifier = Modifier 26 | .fillMaxWidth() 27 | .statusBarsPadding() 28 | .padding(horizontal = 16.dp, vertical = 16.dp) 29 | .size(24.dp) 30 | ) { 31 | BackButton( 32 | modifier = Modifier.weight(1f), 33 | upPress = upPress 34 | ) 35 | SearchButton(Modifier) 36 | MoreButton(Modifier) 37 | } 38 | } 39 | 40 | @Composable 41 | fun BackButton( 42 | modifier: Modifier, 43 | upPress: () -> Unit 44 | ) { 45 | PlaySurface(modifier = modifier) { 46 | IconButton( 47 | onClick = { upPress() } 48 | ) { 49 | Icon( 50 | imageVector = Outlined.ArrowBack, 51 | tint = PlayTheme.colors.iconTint, 52 | contentDescription = null 53 | ) 54 | } 55 | } 56 | } 57 | 58 | @Composable 59 | fun MoreButton(modifier: Modifier) { 60 | IconButton( 61 | onClick = {}, 62 | modifier = modifier 63 | ) { 64 | Icon( 65 | imageVector = Outlined.MoreVert, 66 | tint = PlayTheme.colors.iconTint, 67 | contentDescription = null 68 | ) 69 | } 70 | } 71 | 72 | @Composable 73 | fun SearchButton(modifier: Modifier) { 74 | IconButton( 75 | onClick = {}, 76 | modifier = modifier 77 | ) { 78 | Icon( 79 | imageVector = Outlined.Search, 80 | tint = PlayTheme.colors.iconTint, 81 | contentDescription = null 82 | ) 83 | } 84 | } 85 | 86 | @Preview("Back Button") 87 | @Composable 88 | private fun BackButtonPreview() { 89 | PlayTheme { 90 | PlaySurface { 91 | AppBarLayout( 92 | upPress = {} 93 | ) 94 | } 95 | } 96 | } 97 | 98 | @Preview("Back Button Dark") 99 | @Composable 100 | private fun BackButtonDarkPreview() { 101 | PlayTheme(darkTheme = true) { 102 | PlaySurface { 103 | AppBarLayout( 104 | upPress = {} 105 | ) 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/components/Card.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.components 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.Color 8 | import androidx.compose.ui.graphics.Shape 9 | import androidx.compose.ui.unit.Dp 10 | import androidx.compose.ui.unit.dp 11 | import com.example.play.theme.PlayTheme 12 | 13 | @Composable 14 | fun PlayCard( 15 | modifier: Modifier = Modifier, 16 | shape: Shape = MaterialTheme.shapes.medium, 17 | color: Color = PlayTheme.colors.uiBackground, 18 | contentColor: Color = PlayTheme.colors.textPrimary, 19 | border: BorderStroke? = null, 20 | elevation: Dp = 1.dp, 21 | content: @Composable () -> Unit 22 | ) { 23 | PlaySurface( 24 | modifier = modifier, 25 | shape = shape, 26 | color = color, 27 | contentColor = contentColor, 28 | elevation = elevation, 29 | border = border, 30 | content = content 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/components/Divider.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.components 2 | 3 | import androidx.compose.material.Divider 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.graphics.Color 7 | import androidx.compose.ui.unit.Dp 8 | import androidx.compose.ui.unit.dp 9 | import com.example.play.theme.PlayTheme 10 | 11 | @Composable 12 | fun PlayDivider( 13 | modifier: Modifier = Modifier, 14 | color: Color = PlayTheme.colors.uiBorder.copy(alpha = DividerAlpha), 15 | thickness: Dp = 1.dp, 16 | startIndent: Dp = 0.dp 17 | ) { 18 | Divider( 19 | modifier = modifier, 20 | color = color, 21 | thickness = thickness, 22 | startIndent = startIndent 23 | ) 24 | } 25 | 26 | private const val DividerAlpha = 0.12f 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/components/Filters.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.components 2 | 3 | import androidx.compose.foundation.border 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.heightIn 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.foundation.lazy.LazyRow 11 | import androidx.compose.foundation.selection.toggleable 12 | import androidx.compose.foundation.shape.RoundedCornerShape 13 | import androidx.compose.material.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.getValue 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.graphics.Shape 19 | import androidx.compose.ui.text.TextStyle 20 | import androidx.compose.ui.text.font.FontWeight 21 | import androidx.compose.ui.tooling.preview.Preview 22 | import androidx.compose.ui.unit.dp 23 | import androidx.compose.ui.unit.sp 24 | import com.example.play.anim.getFilterBgColorState 25 | import com.example.play.anim.getFilterTextColorState 26 | import com.example.play.data.AppRepo 27 | import com.example.play.data.Filter 28 | import com.example.play.theme.PlayTheme 29 | 30 | @Composable 31 | fun FilterBar( 32 | filters: List, 33 | filterSelected: Int = AppRepo.getFilters().first().id.value, 34 | setFilterSelected: (Int) -> Unit = {} 35 | ) { 36 | LazyRow(modifier = Modifier.heightIn(min = 56.dp)) { 37 | item { 38 | Spacer(modifier = Modifier.width(24.dp)) 39 | filters.forEach { filter -> 40 | filter.enabled.value = filterSelected == filter.id.value 41 | val (selected, setSelected) = remember { filter.enabled } 42 | FilterChip( 43 | filter = filter, 44 | selected = selected, 45 | setSelected = setSelected, 46 | setFilterSelected = setFilterSelected 47 | ) 48 | Spacer(modifier = Modifier.width(8.dp)) 49 | } 50 | } 51 | } 52 | } 53 | 54 | @Composable 55 | fun FilterChip( 56 | filter: Filter, 57 | modifier: Modifier = Modifier, 58 | shape: Shape = RoundedCornerShape(50), 59 | selected: Boolean = false, 60 | setSelected: (Boolean) -> Unit, 61 | setFilterSelected: (Int) -> Unit 62 | ) { 63 | 64 | val backgroundColor by getFilterBgColorState(isSelected = selected) 65 | val textColor by getFilterTextColorState(isSelected = selected) 66 | 67 | PlaySurface( 68 | modifier = modifier 69 | .height(30.dp) 70 | .border( 71 | 1.dp, if (selected) PlayTheme.colors.accent.copy( 72 | alpha = 0.1f 73 | ) else PlayTheme.colors.uiBorder, shape 74 | ), 75 | color = backgroundColor, 76 | contentColor = textColor, 77 | shape = shape 78 | ) { 79 | Box( 80 | modifier = Modifier.toggleable( 81 | value = selected, 82 | onValueChange = { 83 | setSelected(true) 84 | setFilterSelected(filter.id.value) 85 | } 86 | ) 87 | ) { 88 | Text( 89 | text = filter.name, 90 | style = TextStyle( 91 | fontWeight = FontWeight.Normal, 92 | fontSize = 13.sp, 93 | letterSpacing = 0.15.sp 94 | ), 95 | maxLines = 1, 96 | modifier = Modifier.padding( 97 | horizontal = 20.dp, 98 | vertical = 6.dp 99 | ) 100 | ) 101 | } 102 | } 103 | } 104 | 105 | @Preview 106 | @Composable 107 | fun FilterBarPreview() { 108 | PlayTheme { 109 | FilterBar(filters = AppRepo.getFilters()) 110 | } 111 | } 112 | 113 | @Preview 114 | @Composable 115 | fun FilterBarDarkPreview() { 116 | PlayTheme(darkTheme = true) { 117 | FilterBar(filters = AppRepo.getFilters()) 118 | } 119 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/components/Gradient.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.play.ui.components 18 | 19 | import androidx.compose.animation.animateColorAsState 20 | import androidx.compose.foundation.background 21 | import androidx.compose.foundation.border 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.composed 24 | import androidx.compose.ui.draw.drawWithContent 25 | import androidx.compose.ui.graphics.BlendMode 26 | import androidx.compose.ui.graphics.Brush 27 | import androidx.compose.ui.graphics.Color 28 | import androidx.compose.ui.graphics.Shape 29 | import androidx.compose.ui.graphics.TileMode 30 | import androidx.compose.ui.unit.Dp 31 | import androidx.compose.ui.unit.dp 32 | 33 | fun Modifier.diagonalGradientTint( 34 | colors: List, 35 | blendMode: BlendMode 36 | ) = drawWithContent { 37 | drawContent() 38 | drawRect( 39 | brush = Brush.linearGradient(colors), 40 | blendMode = blendMode 41 | ) 42 | } 43 | 44 | fun Modifier.offsetGradientBackground( 45 | colors: List, 46 | width: Float, 47 | offset: Float = 0f 48 | ) = background( 49 | Brush.horizontalGradient( 50 | colors, 51 | startX = -offset, 52 | endX = width - offset, 53 | tileMode = TileMode.Mirror 54 | ) 55 | ) 56 | 57 | fun Modifier.diagonalGradientBorder( 58 | colors: List, 59 | borderSize: Dp = 2.dp, 60 | shape: Shape 61 | ) = border( 62 | width = borderSize, 63 | brush = Brush.linearGradient(colors), 64 | shape = shape 65 | ) 66 | 67 | fun Modifier.fadeInDiagonalGradientBorder( 68 | showBorder: Boolean, 69 | colors: List, 70 | borderSize: Dp = 2.dp, 71 | shape: Shape 72 | ) = composed { 73 | val animatedColors = List(colors.size) { i -> 74 | animateColorAsState(if (showBorder) colors[i] else colors[i].copy(alpha = 0f)).value 75 | } 76 | diagonalGradientBorder( 77 | colors = animatedColors, 78 | borderSize = borderSize, 79 | shape = shape 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/components/Image.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.components 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.foundation.shape.CircleShape 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.layout.ContentScale 12 | import androidx.compose.ui.res.painterResource 13 | import androidx.compose.ui.tooling.preview.Preview 14 | import androidx.compose.ui.unit.Dp 15 | import androidx.compose.ui.unit.dp 16 | import com.example.play.theme.PlayTheme 17 | import com.google.accompanist.coil.rememberCoilPainter 18 | 19 | @Composable 20 | fun CircularAppImage( 21 | imageUrl: String, 22 | modifier: Modifier = Modifier, 23 | elevation: Dp = 0.dp 24 | ) { 25 | PlaySurface( 26 | color = Color.LightGray, 27 | elevation = elevation, 28 | shape = CircleShape, 29 | modifier = modifier 30 | ) { 31 | Image( 32 | painter = rememberCoilPainter( 33 | request = imageUrl 34 | ), 35 | contentScale = ContentScale.Crop, 36 | modifier = Modifier.fillMaxSize(), 37 | contentDescription = null 38 | ) 39 | } 40 | } 41 | 42 | @Composable 43 | fun CircularLocalImage( 44 | resId: Int, 45 | modifier: Modifier = Modifier, 46 | elevation: Dp = 0.dp 47 | ) { 48 | PlaySurface( 49 | color = Color.LightGray, 50 | elevation = elevation, 51 | shape = CircleShape, 52 | modifier = modifier 53 | ) { 54 | Image( 55 | painterResource(resId), 56 | contentDescription = null, 57 | contentScale = ContentScale.Crop, 58 | modifier = Modifier.fillMaxSize() 59 | ) 60 | } 61 | } 62 | 63 | @Composable 64 | fun RoundedCornerAppImage( 65 | imageUrl: String, 66 | modifier: Modifier = Modifier, 67 | cornerPercent: Int, 68 | elevation: Dp = 0.dp 69 | ) { 70 | PlaySurface( 71 | color = Color.LightGray, 72 | elevation = elevation, 73 | shape = RoundedCornerShape(cornerPercent), 74 | modifier = modifier 75 | ) { 76 | Image( 77 | painter = rememberCoilPainter( 78 | request = imageUrl 79 | ), 80 | contentScale = ContentScale.Crop, 81 | modifier = Modifier.fillMaxSize(), 82 | contentDescription = null 83 | ) 84 | } 85 | } 86 | 87 | @Preview("Circular App Image") 88 | @Composable 89 | fun CircularAppImagePreview() { 90 | PlayTheme(darkTheme = true) { 91 | CircularAppImage( 92 | imageUrl = "", 93 | modifier = Modifier 94 | .size(120.dp), 95 | elevation = 10.dp 96 | ) 97 | } 98 | } 99 | 100 | @Preview("Rounded Corner App Image") 101 | @Composable 102 | fun RoundedCornerAppImagePreview() { 103 | PlayTheme(darkTheme = true) { 104 | RoundedCornerAppImage( 105 | imageUrl = "", 106 | modifier = Modifier 107 | .size(120.dp), 108 | cornerPercent = 5, 109 | elevation = 10.dp 110 | ) 111 | } 112 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/components/PlayToolBar.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.layout.sizeIn 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material.Icon 8 | import androidx.compose.material.IconButton 9 | import androidx.compose.material.Text 10 | import androidx.compose.material.TopAppBar 11 | import androidx.compose.material.icons.Icons 12 | import androidx.compose.material.icons.outlined.Menu 13 | import androidx.compose.material.icons.outlined.Mic 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.text.TextStyle 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.text.style.TextAlign 20 | import androidx.compose.ui.text.style.TextOverflow.Ellipsis 21 | import androidx.compose.ui.tooling.preview.Preview 22 | import androidx.compose.ui.unit.dp 23 | import androidx.compose.ui.unit.sp 24 | import com.example.play.R 25 | import com.example.play.theme.AlphaNearOpaque 26 | import com.example.play.theme.PlayTheme 27 | import com.google.accompanist.insets.statusBarsPadding 28 | 29 | @Composable 30 | fun PlayToolBar( 31 | modifier: Modifier = Modifier, 32 | shouldShow: Boolean 33 | ) { 34 | if (shouldShow) { 35 | PlaySurface(modifier = modifier.statusBarsPadding()) { 36 | PlaySurface( 37 | shape = RoundedCornerShape(5.dp), 38 | modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 8.dp), 39 | elevation = 4.dp, 40 | color = PlayTheme.colors.uiBackground 41 | ) { 42 | TopAppBar( 43 | backgroundColor = PlayTheme.colors.uiBackground.copy(alpha = AlphaNearOpaque), 44 | contentColor = PlayTheme.colors.textSecondary 45 | ) { 46 | IconButton( 47 | onClick = {}, 48 | modifier = Modifier 49 | .align(Alignment.CenterVertically) 50 | .weight(1f, false) 51 | ) { 52 | Icon( 53 | imageVector = Icons.Outlined.Menu, 54 | tint = PlayTheme.colors.iconTint, 55 | contentDescription = null 56 | ) 57 | } 58 | Text( 59 | text = "Search for apps & games", 60 | color = PlayTheme.colors.textSecondary, 61 | textAlign = TextAlign.Start, 62 | maxLines = 1, 63 | style = TextStyle( 64 | fontWeight = FontWeight.Light, 65 | fontSize = 16.sp 66 | ), 67 | overflow = Ellipsis, 68 | modifier = Modifier 69 | .align(Alignment.CenterVertically) 70 | .weight(4f, true) 71 | ) 72 | IconButton( 73 | onClick = {}, 74 | modifier = Modifier 75 | .align(Alignment.CenterVertically) 76 | .weight(1f, false) 77 | ) { 78 | Icon( 79 | imageVector = Icons.Outlined.Mic, 80 | tint = PlayTheme.colors.iconTint, 81 | contentDescription = null 82 | ) 83 | } 84 | CircularLocalImage( 85 | resId = R.drawable.user_profile_pic, 86 | modifier = Modifier 87 | .sizeIn(maxHeight = 40.dp, maxWidth = 40.dp) 88 | .weight(1f, false) 89 | .clickable(onClick = {}) 90 | ) 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | @Preview 98 | @Composable 99 | fun ToolBarPreview() { 100 | PlayTheme { 101 | PlayToolBar(shouldShow = true) 102 | } 103 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/components/Scaffold.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.components 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.material.DrawerDefaults 6 | import androidx.compose.material.ExperimentalMaterialApi 7 | import androidx.compose.material.FabPosition 8 | import androidx.compose.material.MaterialTheme 9 | import androidx.compose.material.Scaffold 10 | import androidx.compose.material.ScaffoldState 11 | import androidx.compose.material.SnackbarHost 12 | import androidx.compose.material.SnackbarHostState 13 | import androidx.compose.material.rememberScaffoldState 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.graphics.Shape 18 | import androidx.compose.ui.unit.Dp 19 | import com.example.play.theme.PlayTheme 20 | 21 | /** 22 | * Wrap Material [androidx.compose.material.Scaffold] and set [PlayTheme] colors. 23 | */ 24 | @OptIn(ExperimentalMaterialApi::class) 25 | @Composable 26 | fun PlayScaffold( 27 | modifier: Modifier = Modifier, 28 | scaffoldState: ScaffoldState = rememberScaffoldState(), 29 | topBar: @Composable (() -> Unit) = {}, 30 | bottomBar: @Composable (() -> Unit) = {}, 31 | snackBarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, 32 | floatingActionButton: @Composable (() -> Unit) = {}, 33 | floatingActionButtonPosition: FabPosition = FabPosition.End, 34 | isFloatingActionButtonDocked: Boolean = false, 35 | drawerContent: @Composable (ColumnScope.() -> Unit)? = null, 36 | drawerShape: Shape = MaterialTheme.shapes.large, 37 | drawerElevation: Dp = DrawerDefaults.Elevation, 38 | drawerBackgroundColor: Color = PlayTheme.colors.uiBackground, 39 | drawerContentColor: Color = PlayTheme.colors.textSecondary, 40 | drawerScrimColor: Color = PlayTheme.colors.uiBorder, 41 | backgroundColor: Color = PlayTheme.colors.uiBackground, 42 | contentColor: Color = PlayTheme.colors.textSecondary, 43 | bodyContent: @Composable (PaddingValues) -> Unit 44 | ) { 45 | Scaffold( 46 | modifier = modifier, 47 | scaffoldState = scaffoldState, 48 | topBar = topBar, 49 | bottomBar = bottomBar, 50 | snackbarHost = snackBarHost, 51 | floatingActionButton = floatingActionButton, 52 | floatingActionButtonPosition = floatingActionButtonPosition, 53 | isFloatingActionButtonDocked = isFloatingActionButtonDocked, 54 | drawerContent = drawerContent, 55 | drawerShape = drawerShape, 56 | drawerElevation = drawerElevation, 57 | drawerBackgroundColor = drawerBackgroundColor, 58 | drawerContentColor = drawerContentColor, 59 | drawerScrimColor = drawerScrimColor, 60 | backgroundColor = backgroundColor, 61 | contentColor = contentColor, 62 | content = bodyContent 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/components/Surface.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.components 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.border 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.material.LocalContentColor 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.CompositionLocalProvider 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.clip 12 | import androidx.compose.ui.draw.shadow 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.graphics.RectangleShape 15 | import androidx.compose.ui.graphics.Shape 16 | import androidx.compose.ui.graphics.compositeOver 17 | import androidx.compose.ui.unit.Dp 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.zIndex 20 | import com.example.play.theme.PlayTheme 21 | import kotlin.math.ln 22 | 23 | /** 24 | * An alternative to [androidx.compose.material.Surface] 25 | */ 26 | @Composable 27 | fun PlaySurface( 28 | modifier: Modifier = Modifier, 29 | shape: Shape = RectangleShape, 30 | color: Color = PlayTheme.colors.uiBackground, 31 | contentColor: Color = PlayTheme.colors.textSecondary, 32 | border: BorderStroke? = null, 33 | elevation: Dp = 0.dp, 34 | content: @Composable () -> Unit 35 | ) { 36 | Box( 37 | modifier = modifier 38 | .shadow(elevation = elevation, shape = shape, clip = false) 39 | .zIndex(elevation.value) 40 | .then(if (border != null) Modifier.border(border, shape) else Modifier) 41 | .background( 42 | color = getBackgroundColorForElevation(color, elevation), 43 | shape = shape 44 | ) 45 | .clip(shape) 46 | ) { 47 | CompositionLocalProvider(LocalContentColor provides contentColor, content = content) 48 | } 49 | } 50 | 51 | @Composable 52 | private fun getBackgroundColorForElevation( 53 | color: Color, 54 | elevation: Dp 55 | ): Color { 56 | return if (elevation > 0.dp 57 | ) { 58 | color.withElevation(elevation) 59 | } else { 60 | color 61 | } 62 | } 63 | 64 | /** 65 | * Applies a [Color.White] overlay to this color based on the [elevation]. This increases visibility 66 | * of elevation for surfaces in a dark theme. 67 | */ 68 | private fun Color.withElevation(elevation: Dp): Color { 69 | val foreground = calculateForeground(elevation) 70 | return foreground.compositeOver(this) 71 | } 72 | 73 | /** 74 | * @return the alpha-modified [Color.White] to overlay on top of the surface color to produce 75 | * the resultant color. 76 | */ 77 | private fun calculateForeground(elevation: Dp): Color { 78 | val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 20f 79 | return Color.White.copy(alpha = alpha) 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/components/SwitchComponent.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.components 2 | 3 | import androidx.compose.material.Switch 4 | import androidx.compose.material.SwitchDefaults 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import com.example.play.theme.PlayTheme 8 | 9 | @Composable 10 | fun Switch( 11 | switchState: Boolean = false, 12 | updateSwitchState: (Boolean) -> Unit, 13 | modifier: Modifier 14 | ) { 15 | Switch( 16 | checked = switchState, 17 | onCheckedChange = { checked -> 18 | updateSwitchState(checked) 19 | }, 20 | modifier = modifier, 21 | colors = SwitchDefaults.colors( 22 | PlayTheme.colors.switchColor 23 | ) 24 | ) 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/components/progressindicator/ProgressIndicator.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.components.progressindicator 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.foundation.layout.width 11 | import androidx.compose.foundation.progressSemantics 12 | import androidx.compose.material.Icon 13 | import androidx.compose.material.MaterialTheme 14 | import androidx.compose.material.icons.Icons 15 | import androidx.compose.material.icons.filled.Star 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.MutableState 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.geometry.CornerRadius 20 | import androidx.compose.ui.geometry.Size 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.tooling.preview.Preview 23 | import androidx.compose.ui.unit.Dp 24 | import androidx.compose.ui.unit.dp 25 | import com.example.play.anim.getAppRatingBarState 26 | import com.example.play.theme.PlayTheme 27 | import com.example.play.ui.components.PlaySurface 28 | 29 | /** 30 | * A custom animated determinate linear progress indicator that represents progress by drawing a rounded rectangle. 31 | * 32 | * @param color The color of the progress indicator. 33 | * @param backgroundColor The color of the background behind the indicator, visible when the 34 | * progress has not reached that area of the overall indicator yet. 35 | */ 36 | @Composable 37 | fun AnimatedProgressIndicator( 38 | modifier: Modifier = Modifier.padding(start = 8.dp, end = 8.dp), 39 | progress: Float, 40 | durationMillis: Int = 3000, 41 | color: Color = MaterialTheme.colors.primary, 42 | strokeWidth: Dp = 9.dp, 43 | backgroundColor: Color = PlayTheme.colors.progressIndicatorBg, 44 | showProgress: MutableState 45 | ) { 46 | val state = getAppRatingBarState( 47 | progress = progress, durationMillis = durationMillis, showProgress = showProgress.value 48 | ) 49 | Canvas( 50 | modifier 51 | .progressSemantics(state.value) 52 | .size(280.dp, strokeWidth) 53 | ) { 54 | drawRoundRect( 55 | color = backgroundColor, 56 | cornerRadius = CornerRadius(15f, 15f), 57 | size = Size(size.width, size.height) 58 | ) 59 | drawRoundRect( 60 | color = color, cornerRadius = CornerRadius(15f, 15f), 61 | size = Size(state.value * size.width, size.height) 62 | ) 63 | } 64 | } 65 | 66 | @Composable 67 | fun StarRatings( 68 | modifier: Modifier = Modifier, 69 | ratings: Double = 3.5, 70 | sizeInDp: Dp = 15.dp 71 | ) { 72 | Box(modifier = modifier) { 73 | Row { 74 | for (i in 1..5) { 75 | Star(sizeInDp) 76 | } 77 | } 78 | Row { 79 | for (i in 1..ratings.toInt()) { 80 | StarFilled(sizeInDp) 81 | } 82 | } 83 | } 84 | } 85 | 86 | @Composable 87 | fun Star(sizeInDp: Dp) { 88 | Icon( 89 | imageVector = Icons.Filled.Star, tint = PlayTheme.colors.progressIndicatorBg, 90 | modifier = Modifier 91 | .height(sizeInDp) 92 | .width(sizeInDp), 93 | contentDescription = null 94 | ) 95 | } 96 | 97 | @Composable 98 | fun StarFilled(sizeInDp: Dp) { 99 | Icon( 100 | imageVector = Icons.Filled.Star, tint = PlayTheme.colors.accent, 101 | modifier = Modifier 102 | .height(sizeInDp) 103 | .width(sizeInDp), 104 | contentDescription = null 105 | ) 106 | } 107 | 108 | @Preview 109 | @Composable 110 | fun PreviewStarRatings() { 111 | PlayTheme { 112 | StarRatings() 113 | } 114 | } 115 | 116 | @Preview 117 | @Composable 118 | fun PreviewStar() { 119 | PlayTheme { 120 | Star(sizeInDp = 20.dp) 121 | } 122 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/details/AppDetails.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.details 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.lazy.LazyColumn 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import androidx.compose.ui.unit.dp 13 | import com.example.play.data.AppRepo 14 | import com.example.play.theme.PlayTheme 15 | import com.example.play.ui.components.AppBarLayout 16 | import com.example.play.ui.components.PlaySurface 17 | import com.example.play.ui.details.about.About 18 | import com.example.play.ui.details.header.Header 19 | import com.example.play.ui.details.installbutton.animated.InstallButtonLayout 20 | import com.example.play.ui.details.reviews.RatingsAndReviews 21 | import com.example.play.ui.details.screenshots.Screenshots 22 | import com.example.play.ui.details.stats.Stats 23 | 24 | @Composable 25 | fun AppDetails( 26 | appId: Long?, 27 | upPress: () -> Unit 28 | ) { 29 | val app = remember(appId) { AppRepo.getApp(appId) } 30 | val isInstalling = remember { mutableStateOf(false) } 31 | 32 | PlaySurface( 33 | modifier = Modifier.fillMaxSize() 34 | ) { 35 | AppBarLayout(upPress = upPress) 36 | LazyColumn( 37 | horizontalAlignment = Alignment.CenterHorizontally, 38 | modifier = Modifier 39 | .fillMaxSize() 40 | .padding(top = 56.dp) 41 | ) { 42 | item { 43 | app?.let { safeApp -> 44 | Header(app = safeApp, showProgress = isInstalling) 45 | Stats(app = safeApp) 46 | } 47 | InstallButtonLayout(isInstalling = isInstalling) 48 | Screenshots() 49 | About() 50 | RatingsAndReviews() 51 | } 52 | } 53 | } 54 | } 55 | 56 | @Preview("App Detail") 57 | @Composable 58 | private fun AppDetailPreview() { 59 | PlayTheme { 60 | AppDetails( 61 | appId = 1L, 62 | upPress = {} 63 | ) 64 | } 65 | } 66 | 67 | //@Preview("App Detail Dark") 68 | //@Composable 69 | //private fun AppDetailDarkPreview() { 70 | // PlayTheme(darkTheme = true) { 71 | // AppDetails( 72 | // appId = 1L, 73 | // upPress = {} 74 | // ) 75 | // } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/details/ScreenshotItem.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.details 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.foundation.layout.width 12 | import androidx.compose.material.MaterialTheme 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.tooling.preview.Preview 17 | import androidx.compose.ui.unit.dp 18 | import com.example.play.theme.PlayTheme 19 | import com.example.play.ui.components.PlayCard 20 | import com.example.play.ui.components.RoundedCornerAppImage 21 | 22 | @Composable 23 | fun AppImageItem( 24 | imageUrl: String, 25 | onImageClick: () -> Unit, 26 | modifier: Modifier = Modifier 27 | ) { 28 | PlayCard( 29 | elevation = 0.dp, 30 | shape = MaterialTheme.shapes.large, 31 | modifier = modifier 32 | .size( 33 | width = 300.dp, 34 | height = 180.dp 35 | ) 36 | .padding(bottom = 8.dp) 37 | ) { 38 | Column( 39 | modifier = Modifier 40 | .clickable(onClick = onImageClick) 41 | .fillMaxSize() 42 | ) { 43 | Box( 44 | modifier = Modifier 45 | .height(180.dp) 46 | .fillMaxWidth() 47 | ) { 48 | RoundedCornerAppImage( 49 | imageUrl = imageUrl, 50 | modifier = Modifier 51 | .width(300.dp) 52 | .height(180.dp) 53 | .align(Alignment.TopStart) 54 | .padding(8.dp), 55 | cornerPercent = 5 56 | ) 57 | } 58 | } 59 | } 60 | } 61 | 62 | @Preview("App Image Item") 63 | @Composable 64 | fun PlayFeaturedAppItemPreview() { 65 | PlayTheme { 66 | AppImageItem( 67 | imageUrl = "", 68 | onImageClick = { } 69 | ) 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/details/about/About.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.details.about 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material.Icon 7 | import androidx.compose.material.IconButton 8 | import androidx.compose.material.MaterialTheme 9 | import androidx.compose.material.Text 10 | import androidx.compose.material.icons.Icons.Outlined 11 | import androidx.compose.material.icons.outlined.ArrowForward 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.text.TextStyle 16 | import androidx.compose.ui.text.font.FontWeight 17 | import androidx.compose.ui.unit.dp 18 | import androidx.compose.ui.unit.sp 19 | import androidx.compose.ui.tooling.preview.Preview 20 | import com.example.play.theme.PlayTheme 21 | import com.example.play.ui.components.PlaySurface 22 | 23 | @Composable 24 | fun About() { 25 | Column( 26 | modifier = Modifier.padding(top = 8.dp, start = 24.dp, end = 16.dp) 27 | ) { 28 | Row( 29 | verticalAlignment = Alignment.CenterVertically, 30 | ) { 31 | Text( 32 | text = "About this app", 33 | style = TextStyle( 34 | fontWeight = FontWeight.Medium, 35 | fontSize = 16.sp, 36 | letterSpacing = 0.15.sp 37 | ), 38 | color = PlayTheme.colors.textSecondaryDark, 39 | modifier = Modifier.weight(1f) 40 | ) 41 | IconButton( 42 | onClick = {}, 43 | modifier = Modifier.align(Alignment.CenterVertically) 44 | ) { 45 | Icon( 46 | imageVector = Outlined.ArrowForward, 47 | tint = PlayTheme.colors.iconTint, 48 | contentDescription = null 49 | ) 50 | } 51 | } 52 | Text( 53 | text = "Fun and addictive game. Start downloading and explore the new world!", 54 | style = MaterialTheme.typography.subtitle2, 55 | color = PlayTheme.colors.textSecondary 56 | ) 57 | } 58 | } 59 | 60 | @Preview("About") 61 | @Composable 62 | private fun AboutPreview() { 63 | PlayTheme { 64 | PlaySurface { 65 | About() 66 | } 67 | } 68 | } 69 | 70 | @Preview("About Dark") 71 | @Composable 72 | private fun AboutDarkPreview() { 73 | PlayTheme(darkTheme = true) { 74 | PlaySurface { 75 | About() 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/details/header/Header.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.details.header 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.foundation.layout.width 11 | import androidx.compose.material.CircularProgressIndicator 12 | import androidx.compose.material.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.MutableState 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.text.TextStyle 20 | import androidx.compose.ui.text.font.FontWeight 21 | import androidx.compose.ui.tooling.preview.Preview 22 | import androidx.compose.ui.unit.dp 23 | import androidx.compose.ui.unit.sp 24 | import com.example.play.anim.getAppIconSizeAnimState 25 | import com.example.play.data.App 26 | import com.example.play.data.AppRepo 27 | import com.example.play.theme.PlayTheme 28 | import com.example.play.theme.Typography 29 | import com.example.play.ui.components.PlaySurface 30 | import com.example.play.ui.components.RoundedCornerAppImage 31 | 32 | @Composable 33 | fun Header( 34 | app: App, 35 | showProgress: MutableState 36 | ) { 37 | val appIconSizeState = getAppIconSizeAnimState(showProgress = showProgress.value) 38 | 39 | Row(modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)) { 40 | Box( 41 | modifier = Modifier 42 | .height(100.dp) 43 | .width(100.dp) 44 | ) { 45 | if (showProgress.value) { 46 | CircularProgressIndicator( 47 | color = PlayTheme.colors.accent, 48 | strokeWidth = 2.dp, 49 | modifier = Modifier.size(100.dp) 50 | ) 51 | } 52 | RoundedCornerAppImage( 53 | imageUrl = app.imageUrl, 54 | modifier = Modifier 55 | .width(appIconSizeState.value) 56 | .height(appIconSizeState.value) 57 | .align(Alignment.Center) 58 | .padding(8.dp), 59 | cornerPercent = 20 60 | ) 61 | } 62 | Column( 63 | modifier = Modifier.fillMaxWidth() 64 | ) { 65 | Text( 66 | text = app.name, 67 | style = TextStyle( 68 | fontWeight = FontWeight.SemiBold, 69 | fontSize = 24.sp, 70 | letterSpacing = 0.15.sp 71 | ), 72 | color = PlayTheme.colors.textPrimary, 73 | modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp) 74 | ) 75 | Text( 76 | text = app.org, 77 | maxLines = 1, 78 | style = Typography.subtitle2, 79 | fontWeight = FontWeight.W600, 80 | color = PlayTheme.colors.accent, 81 | modifier = Modifier.padding(start = 16.dp, top = 4.dp) 82 | ) 83 | Text( 84 | text = app.info, 85 | maxLines = 1, 86 | style = Typography.caption, 87 | color = PlayTheme.colors.iconTint, 88 | modifier = Modifier.padding(start = 16.dp) 89 | ) 90 | } 91 | } 92 | } 93 | 94 | @Preview("Header") 95 | @Composable 96 | private fun HeaderPreview() { 97 | PlayTheme { 98 | PlaySurface { 99 | AppRepo.getApp(1L)?.let { safeApp -> 100 | Header( 101 | app = safeApp, 102 | showProgress = remember { 103 | mutableStateOf(true) 104 | } 105 | ) 106 | } 107 | } 108 | } 109 | } 110 | 111 | @Preview("Header Dark") 112 | @Composable 113 | private fun HeaderDarkPreview() { 114 | PlayTheme(darkTheme = true) { 115 | PlaySurface { 116 | AppRepo.getApp(1L)?.let { safeApp -> 117 | Header( 118 | app = safeApp, 119 | showProgress = remember { 120 | mutableStateOf(false) 121 | } 122 | ) 123 | } 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/details/installbutton/animated/AnimatedInstallButton.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.details.installbutton.animated 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material.Button 11 | import androidx.compose.material.ButtonDefaults 12 | import androidx.compose.material.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.MutableState 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.unit.dp 17 | import com.example.play.anim.getInstallBtnBgColorState 18 | import com.example.play.anim.getInstallBtnBorderColorState 19 | import com.example.play.anim.getInstallBtnBorderWidthState 20 | import com.example.play.anim.getInstallBtnCornerState 21 | import com.example.play.anim.getInstallButtonWidthState 22 | import com.example.play.anim.getOpenButtonGapWidthState 23 | import com.example.play.anim.getOpenButtonWidthState 24 | import com.example.play.theme.PlayTheme 25 | 26 | @Composable 27 | fun InstallButtonPanel( 28 | isPressed: MutableState 29 | ) { 30 | Row( 31 | modifier = Modifier.padding(8.dp) 32 | ) { 33 | val openButtonWidthState = getOpenButtonWidthState(isPressed = isPressed.value) 34 | val installButtonWidthState = getInstallButtonWidthState(isPressed = isPressed.value) 35 | 36 | OpenButton( 37 | isPressed = isPressed, 38 | modifier = Modifier.size(openButtonWidthState.value, 38.dp) 39 | ) 40 | 41 | InstallButton( 42 | isPressed = isPressed, 43 | modifier = Modifier 44 | .size(installButtonWidthState.value, 38.dp) 45 | .weight(1f, true) 46 | ) 47 | } 48 | } 49 | 50 | @Composable 51 | fun InstallButton( 52 | isPressed: MutableState, 53 | modifier: Modifier 54 | ) { 55 | val buttonBorderColorState = getInstallBtnBorderColorState(isPressed = isPressed.value) 56 | val buttonBorderWidthState = getInstallBtnBorderWidthState(isPressed = isPressed.value) 57 | val buttonBgColorState = getInstallBtnBgColorState(isPressed = isPressed.value) 58 | val buttonCornersState = getInstallBtnCornerState(isPressed = isPressed.value) 59 | 60 | Button( 61 | border = BorderStroke( 62 | buttonBorderWidthState.value, 63 | buttonBorderColorState.value 64 | ), 65 | colors = ButtonDefaults.buttonColors( 66 | backgroundColor = buttonBgColorState.value 67 | ), 68 | shape = RoundedCornerShape(buttonCornersState.value), 69 | modifier = modifier, 70 | onClick = { 71 | isPressed.value = isPressed.value.not() 72 | } 73 | ) { 74 | ButtonContent(isPressed = isPressed.value) 75 | } 76 | } 77 | 78 | @Composable 79 | fun OpenButton( 80 | isPressed: MutableState, 81 | modifier: Modifier 82 | ) { 83 | val buttonGapWidthState = getOpenButtonGapWidthState(isPressed = isPressed.value) 84 | 85 | Button( 86 | border = BorderStroke(1.dp, PlayTheme.colors.accent), 87 | colors = ButtonDefaults.buttonColors( 88 | backgroundColor = PlayTheme.colors.uiBackground 89 | ), 90 | shape = RoundedCornerShape(50), 91 | modifier = modifier, 92 | onClick = {} 93 | ) { 94 | Text( 95 | "Open", 96 | softWrap = false, 97 | color = PlayTheme.colors.accent 98 | ) 99 | } 100 | Spacer(modifier = Modifier.width(buttonGapWidthState.value)) 101 | } 102 | 103 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/details/installbutton/animated/ButtonContent.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.details.installbutton.animated 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.foundation.layout.width 8 | import androidx.compose.material.Icon 9 | import androidx.compose.material.Text 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.filled.CloudDownload 12 | import androidx.compose.material.icons.outlined.CloudDownload 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.alpha 18 | import androidx.compose.ui.unit.dp 19 | import com.example.play.anim.getButtonColorState 20 | import com.example.play.anim.getButtonIconSizeState 21 | import com.example.play.anim.getInstallButtonOpacityState 22 | 23 | @Composable 24 | fun ButtonContent( 25 | isPressed: Boolean 26 | ) { 27 | val buttonTextColorState = getButtonColorState(isPressed = isPressed) 28 | val installButtonOpacityState = getInstallButtonOpacityState(isPressed = isPressed) 29 | val idleIconSizeState by getButtonIconSizeState() 30 | 31 | if (isPressed.not()) { 32 | Row(verticalAlignment = Alignment.CenterVertically) { 33 | Column( 34 | Modifier.width(24.dp), 35 | horizontalAlignment = Alignment.CenterHorizontally 36 | ) { 37 | Icon( 38 | imageVector = Icons.Outlined.CloudDownload, 39 | tint = buttonTextColorState.value, 40 | modifier = Modifier.size(idleIconSizeState.dp), 41 | contentDescription = null 42 | ) 43 | } 44 | Spacer(modifier = Modifier.width(16.dp)) 45 | Text( 46 | "Install", 47 | softWrap = false, 48 | color = buttonTextColorState.value 49 | ) 50 | } 51 | } else { 52 | Row(verticalAlignment = Alignment.CenterVertically) { 53 | Column( 54 | Modifier.width(24.dp), 55 | horizontalAlignment = Alignment.CenterHorizontally 56 | ) { 57 | Icon( 58 | imageVector = Icons.Default.CloudDownload, 59 | tint = buttonTextColorState.value, 60 | modifier = Modifier 61 | .size(idleIconSizeState.dp) 62 | .alpha(installButtonOpacityState.value), 63 | contentDescription = null 64 | ) 65 | } 66 | Spacer(modifier = Modifier.width(16.dp)) 67 | Text( 68 | "Cancel", 69 | softWrap = false, 70 | color = buttonTextColorState.value 71 | ) 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/details/installbutton/animated/InstallButtonLayout.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.details.installbutton.animated 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.MutableState 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.unit.dp 10 | import androidx.compose.ui.tooling.preview.Preview 11 | import com.example.play.theme.PlayTheme 12 | import com.example.play.ui.components.PlaySurface 13 | 14 | @Composable 15 | fun InstallButtonLayout( 16 | isInstalling: MutableState 17 | ) { 18 | PlaySurface(modifier = Modifier.padding(16.dp)) { 19 | InstallButtonPanel(isPressed = isInstalling) 20 | } 21 | } 22 | 23 | @Preview("Install Button Layout") 24 | @Composable 25 | private fun AboutPreview() { 26 | PlayTheme { 27 | PlaySurface { 28 | InstallButtonLayout(isInstalling = remember { 29 | mutableStateOf(true) 30 | }) 31 | } 32 | } 33 | } 34 | 35 | @Preview("Install Button Layout Dark") 36 | @Composable 37 | private fun AboutDarkPreview() { 38 | PlayTheme(darkTheme = true) { 39 | PlaySurface { 40 | InstallButtonLayout(isInstalling = remember { 41 | mutableStateOf(false) 42 | }) 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/details/installbutton/plain/PlainInstallButton.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.details.installbutton.plain 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.Button 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Composable 13 | fun PlainInstallButton() { 14 | Button( 15 | onClick = {}, 16 | shape = RoundedCornerShape(10), 17 | modifier = Modifier 18 | .fillMaxWidth() 19 | .padding(start = 32.dp, end = 32.dp, top = 32.dp, bottom = 16.dp) 20 | ) { 21 | Text("Install") 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/details/reviews/RatingsAndReviews.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.details.reviews 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material.Icon 9 | import androidx.compose.material.IconButton 10 | import androidx.compose.material.Text 11 | import androidx.compose.material.icons.Icons.Outlined 12 | import androidx.compose.material.icons.outlined.ArrowForward 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.text.TextStyle 19 | import androidx.compose.ui.text.font.FontWeight 20 | import androidx.compose.ui.tooling.preview.Preview 21 | import androidx.compose.ui.unit.dp 22 | import androidx.compose.ui.unit.sp 23 | import com.example.play.data.AppRepo 24 | import com.example.play.theme.PlayTheme 25 | import com.example.play.theme.Typography 26 | import com.example.play.ui.components.PlaySurface 27 | import com.example.play.ui.components.progressindicator.AnimatedProgressIndicator 28 | import com.example.play.ui.components.progressindicator.StarRatings 29 | 30 | @Composable 31 | fun RatingsAndReviews() { 32 | Column( 33 | modifier = Modifier.padding(top = 4.dp, start = 24.dp, end = 16.dp, bottom = 8.dp) 34 | ) { 35 | RatingsAndReviewsHeader() 36 | Row(modifier = Modifier.padding(top = 16.dp)) { 37 | Column(modifier = Modifier.align(Alignment.CenterVertically)) { 38 | Text( 39 | text = "4.7", 40 | style = TextStyle( 41 | fontWeight = FontWeight.SemiBold, 42 | fontSize = 45.sp, 43 | letterSpacing = 1.sp 44 | ), 45 | color = PlayTheme.colors.textPrimary, 46 | modifier = Modifier.align(Alignment.Start) 47 | ) 48 | StarRatings() 49 | Text( 50 | text = "2,907,517", 51 | style = TextStyle( 52 | fontWeight = FontWeight.Normal, 53 | fontSize = 11.sp, 54 | letterSpacing = 0.70.sp 55 | ), 56 | color = PlayTheme.colors.textSecondary 57 | ) 58 | } 59 | AppRatingBars( 60 | modifier = Modifier 61 | .padding(start = 24.dp) 62 | .align(Alignment.CenterVertically) 63 | ) 64 | } 65 | Spacer(modifier = Modifier.height(16.dp)) 66 | val reviews = AppRepo.getReviews() 67 | Column { 68 | reviews.forEach { 69 | Spacer(modifier = Modifier.height(16.dp)) 70 | ReviewItem(review = it) 71 | } 72 | } 73 | } 74 | } 75 | 76 | @Composable 77 | private fun RatingsAndReviewsHeader() { 78 | Row( 79 | verticalAlignment = Alignment.CenterVertically, 80 | ) { 81 | Text( 82 | text = "Ratings and reviews", 83 | style = TextStyle( 84 | fontWeight = FontWeight.Medium, 85 | fontSize = 16.sp, 86 | letterSpacing = 0.15.sp 87 | ), 88 | color = PlayTheme.colors.textSecondaryDark, 89 | modifier = Modifier.weight(1f) 90 | ) 91 | IconButton( 92 | onClick = {}, 93 | modifier = Modifier.align(Alignment.Top) 94 | ) { 95 | Icon( 96 | imageVector = Outlined.ArrowForward, 97 | tint = PlayTheme.colors.iconTint, 98 | contentDescription = null 99 | ) 100 | } 101 | } 102 | } 103 | 104 | @Composable 105 | private fun AppRatingBars(modifier: Modifier) { 106 | val showProgress = remember { mutableStateOf(false) } 107 | Column(modifier = modifier) { 108 | Row { 109 | Text( 110 | text = "5", 111 | style = Typography.caption, 112 | color = PlayTheme.colors.textSecondaryDark, 113 | modifier = Modifier.align(Alignment.CenterVertically) 114 | ) 115 | AnimatedProgressIndicator( 116 | progress = 0.8f, 117 | durationMillis = 4000, 118 | color = PlayTheme.colors.accent, 119 | showProgress = showProgress 120 | ) 121 | } 122 | Spacer(modifier = Modifier.height(3.dp)) 123 | Row { 124 | Text( 125 | text = "4", 126 | style = Typography.caption, 127 | color = PlayTheme.colors.textSecondaryDark, 128 | modifier = Modifier.align(Alignment.CenterVertically) 129 | ) 130 | AnimatedProgressIndicator( 131 | progress = 0.5f, 132 | color = PlayTheme.colors.accent, 133 | showProgress = showProgress 134 | ) 135 | } 136 | Spacer(modifier = Modifier.height(3.dp)) 137 | Row { 138 | Text( 139 | text = "3", 140 | style = Typography.caption, 141 | color = PlayTheme.colors.textSecondaryDark, 142 | modifier = Modifier.align(Alignment.CenterVertically) 143 | ) 144 | AnimatedProgressIndicator( 145 | progress = 0.3f, 146 | durationMillis = 4000, 147 | color = PlayTheme.colors.accent, 148 | showProgress = showProgress 149 | ) 150 | } 151 | Spacer(modifier = Modifier.height(3.dp)) 152 | Row { 153 | Text( 154 | text = "2", 155 | style = Typography.caption, 156 | color = PlayTheme.colors.textSecondaryDark, 157 | modifier = Modifier.align(Alignment.CenterVertically) 158 | ) 159 | AnimatedProgressIndicator( 160 | progress = 0.1f, 161 | color = PlayTheme.colors.accent, 162 | showProgress = showProgress 163 | ) 164 | } 165 | Spacer(modifier = Modifier.height(3.dp)) 166 | Row { 167 | Text( 168 | text = "1", 169 | style = Typography.caption, 170 | color = PlayTheme.colors.textSecondaryDark, 171 | modifier = Modifier.align(Alignment.CenterVertically) 172 | ) 173 | AnimatedProgressIndicator( 174 | progress = 0.2f, 175 | durationMillis = 4000, 176 | color = PlayTheme.colors.accent, 177 | showProgress = showProgress 178 | ) 179 | } 180 | } 181 | showProgress.value = true 182 | } 183 | 184 | @Preview("Ratings and Reviews") 185 | @Composable 186 | private fun ReviewsPreview() { 187 | PlayTheme { 188 | PlaySurface { 189 | RatingsAndReviews() 190 | } 191 | } 192 | } 193 | 194 | @Preview("Ratings and Reviews Dark") 195 | @Composable 196 | private fun ReviewsDarkPreview() { 197 | PlayTheme(darkTheme = true) { 198 | PlaySurface { 199 | RatingsAndReviews() 200 | } 201 | } 202 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/details/reviews/ReviewItem.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.details.reviews 2 | 3 | import androidx.compose.foundation.border 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.width 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material.Icon 13 | import androidx.compose.material.Text 14 | import androidx.compose.material.icons.Icons 15 | import androidx.compose.material.icons.filled.MoreVert 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.graphics.Shape 21 | import androidx.compose.ui.text.TextStyle 22 | import androidx.compose.ui.text.font.FontWeight 23 | import androidx.compose.ui.text.style.TextAlign.Center 24 | import androidx.compose.ui.text.style.TextOverflow.Ellipsis 25 | import androidx.compose.ui.unit.Dp 26 | import androidx.compose.ui.unit.dp 27 | import androidx.compose.ui.unit.sp 28 | import androidx.compose.ui.tooling.preview.Preview 29 | import com.example.play.data.AppRepo 30 | import com.example.play.data.Review 31 | import com.example.play.theme.PlayTheme 32 | import com.example.play.ui.components.CircularAppImage 33 | import com.example.play.ui.components.PlaySurface 34 | import com.example.play.ui.components.progressindicator.StarRatings 35 | 36 | @Composable 37 | fun ReviewItem(review: Review) { 38 | PlaySurface( 39 | modifier = Modifier.padding(bottom = 16.dp) 40 | ) { 41 | Column { 42 | Row( 43 | modifier = Modifier.padding(bottom = 8.dp) 44 | ) { 45 | CircularAppImage( 46 | imageUrl = review.userAvatarUrl, 47 | modifier = Modifier 48 | .height(35.dp) 49 | .width(35.dp) 50 | .align(Alignment.CenterVertically) 51 | .weight(2f, false) 52 | ) 53 | Text( 54 | text = review.userName, 55 | style = TextStyle( 56 | fontWeight = FontWeight.Normal, 57 | fontSize = 12.sp, 58 | letterSpacing = 0.15.sp 59 | ), 60 | color = PlayTheme.colors.textPrimary, 61 | maxLines = 1, 62 | overflow = Ellipsis, 63 | modifier = Modifier 64 | .align(Alignment.CenterVertically) 65 | .weight(12f, true) 66 | .padding(start = 8.dp) 67 | ) 68 | Icon( 69 | imageVector = Icons.Filled.MoreVert, 70 | contentDescription = null, 71 | modifier = Modifier 72 | .align(Alignment.CenterVertically) 73 | .weight(1f, false) 74 | .clickable(onClick = {}) 75 | ) 76 | } 77 | Row( 78 | modifier = Modifier.padding(bottom = 8.dp) 79 | ) { 80 | StarRatings( 81 | ratings = review.ratings, modifier = Modifier 82 | .align(Alignment.CenterVertically), 83 | sizeInDp = 10.dp 84 | ) 85 | Text( 86 | text = review.date, 87 | style = TextStyle( 88 | fontWeight = FontWeight.Normal, 89 | fontSize = 12.sp, 90 | letterSpacing = 0.15.sp 91 | ), 92 | color = PlayTheme.colors.textSecondary, 93 | modifier = Modifier.padding(start = 8.dp) 94 | ) 95 | } 96 | Text( 97 | text = review.reviewDesc, 98 | style = TextStyle( 99 | fontWeight = FontWeight.Normal, 100 | fontSize = 12.sp, 101 | letterSpacing = 0.15.sp 102 | ), 103 | color = PlayTheme.colors.textSecondaryDark 104 | ) 105 | Row( 106 | modifier = Modifier.padding(top = 24.dp) 107 | ) { 108 | Text( 109 | text = "Was this review helpful?", 110 | style = TextStyle( 111 | fontWeight = FontWeight.Normal, 112 | fontSize = 10.sp, 113 | letterSpacing = 0.15.sp 114 | ), 115 | color = PlayTheme.colors.textSecondary, 116 | modifier = Modifier 117 | .align(Alignment.CenterVertically) 118 | .weight(3f, true) 119 | ) 120 | Row( 121 | modifier = Modifier 122 | .align(Alignment.CenterVertically) 123 | .weight(1f, false) 124 | ) { 125 | Chip(text = "Yes") 126 | Spacer(modifier = Modifier.width(8.dp)) 127 | Chip(text = "No") 128 | } 129 | } 130 | } 131 | } 132 | } 133 | 134 | // Chip 135 | @Composable 136 | private fun Chip( 137 | strokeWidth: Dp = 1.dp, 138 | color: Color = PlayTheme.colors.uiBorder, 139 | shape: Shape = RoundedCornerShape(50), 140 | text: String = "Yes" 141 | ) { 142 | Text( 143 | text = text, 144 | style = TextStyle( 145 | fontWeight = FontWeight.Normal, 146 | fontSize = 10.sp, 147 | textAlign = Center, 148 | letterSpacing = 0.15.sp 149 | ), 150 | color = PlayTheme.colors.textSecondary, 151 | modifier = Modifier 152 | .border(strokeWidth, color, shape) 153 | .padding(top = 2.dp, bottom = 2.dp) 154 | .width(40.dp) 155 | .height(15.dp) 156 | .clickable(onClick = {}) 157 | ) 158 | } 159 | 160 | @Preview("Review Item") 161 | @Composable 162 | private fun ReviewItemPreview() { 163 | PlayTheme { 164 | PlaySurface { 165 | ReviewItem( 166 | AppRepo.getReview(1L) 167 | ) 168 | } 169 | } 170 | } 171 | 172 | @Preview("Review Item Dark") 173 | @Composable 174 | private fun ReviewItemDarkPreview() { 175 | PlayTheme(darkTheme = true) { 176 | PlaySurface { 177 | ReviewItem( 178 | AppRepo.getReview(1L) 179 | ) 180 | } 181 | } 182 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/details/screenshots/Screenshots.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.details.screenshots 2 | 3 | import androidx.compose.foundation.layout.Spacer 4 | import androidx.compose.foundation.layout.width 5 | import androidx.compose.foundation.lazy.LazyRow 6 | import androidx.compose.foundation.lazy.items 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.tooling.preview.Preview 10 | import androidx.compose.ui.unit.dp 11 | import com.example.play.data.AppRepo 12 | import com.example.play.theme.PlayTheme 13 | import com.example.play.ui.details.AppImageItem 14 | 15 | @Composable 16 | fun Screenshots( 17 | imageUrlList: List = AppRepo.getScreenshots(), 18 | onImageClick: () -> Unit = {}, 19 | modifier: Modifier = Modifier 20 | ) { 21 | //val scrollState = rememberScrollState(0) 22 | Spacer(modifier = Modifier.width(16.dp)) 23 | LazyRow( 24 | modifier = modifier 25 | ) { 26 | items(imageUrlList) { imageUrlItem -> 27 | ScreenshotListItem(imageUrlItem, onImageClick) 28 | } 29 | } 30 | } 31 | 32 | @Composable 33 | fun ScreenshotListItem( 34 | imageUrl: String, 35 | onImageClick: () -> Unit 36 | ) { 37 | AppImageItem(imageUrl, onImageClick) 38 | Spacer(modifier = Modifier.width(8.dp)) 39 | } 40 | 41 | @Preview("App Images") 42 | @Composable 43 | private fun AppImagesPreview() { 44 | PlayTheme { 45 | Screenshots( 46 | imageUrlList = listOf("", "", "", "") 47 | ) 48 | } 49 | } 50 | 51 | @Preview("App Images Dark") 52 | @Composable 53 | private fun AppImagesDarkPreview() { 54 | PlayTheme(darkTheme = true) { 55 | Screenshots( 56 | imageUrlList = listOf("", "", "", "") 57 | ) 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/details/stats/Stats.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.details.stats 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxHeight 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.width 11 | import androidx.compose.material.Icon 12 | import androidx.compose.material.MaterialTheme 13 | import androidx.compose.material.Text 14 | import androidx.compose.material.icons.Icons 15 | import androidx.compose.material.icons.outlined.Download 16 | import androidx.compose.material.icons.outlined.Star 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.text.font.FontWeight 21 | import androidx.compose.ui.text.style.TextOverflow.Ellipsis 22 | import androidx.compose.ui.tooling.preview.Preview 23 | import androidx.compose.ui.unit.dp 24 | import com.example.play.data.App 25 | import com.example.play.data.AppRepo 26 | import com.example.play.theme.PlayTheme 27 | import com.example.play.ui.components.PlaySurface 28 | 29 | @Composable 30 | fun Stats(app: App) { 31 | Row( 32 | modifier = Modifier 33 | .padding(top = 8.dp, start = 16.dp, end = 16.dp) 34 | ) { 35 | Column(modifier = Modifier.padding(end = 12.dp)) { 36 | Row(modifier = Modifier.align(Alignment.CenterHorizontally)) { 37 | Text( 38 | text = app.ratings, 39 | maxLines = 1, 40 | overflow = Ellipsis, 41 | style = MaterialTheme.typography.subtitle1, 42 | fontWeight = FontWeight.Bold, 43 | color = PlayTheme.colors.textSecondaryDark 44 | ) 45 | Icon( 46 | imageVector = Icons.Outlined.Star, 47 | tint = PlayTheme.colors.textSecondaryDark, 48 | modifier = Modifier 49 | .padding(end = 8.dp) 50 | .width(14.dp) 51 | .height(14.dp) 52 | .align(Alignment.CenterVertically), 53 | contentDescription = null 54 | ) 55 | } 56 | Text( 57 | text = "636K Reviews", 58 | maxLines = 1, 59 | style = MaterialTheme.typography.caption, 60 | color = PlayTheme.colors.textSecondary 61 | ) 62 | } 63 | Spacer( 64 | modifier = Modifier 65 | .width(1.dp) 66 | .height(24.dp) 67 | .fillMaxHeight() 68 | .align(Alignment.CenterVertically) 69 | .background(color = PlayTheme.colors.uiBorder) 70 | ) 71 | Column(modifier = Modifier.padding(start = 12.dp, end = 12.dp)) { 72 | Icon( 73 | imageVector = Icons.Outlined.Download, 74 | tint = PlayTheme.colors.textSecondaryDark, 75 | modifier = Modifier 76 | .padding(end = 8.dp) 77 | .width(21.dp) 78 | .height(21.dp) 79 | .align(Alignment.CenterHorizontally), 80 | contentDescription = null 81 | ) 82 | Text( 83 | text = "9.9 MB", 84 | maxLines = 1, 85 | style = MaterialTheme.typography.caption, 86 | color = PlayTheme.colors.textSecondary, 87 | modifier = Modifier.align(Alignment.CenterHorizontally) 88 | ) 89 | } 90 | Spacer( 91 | modifier = Modifier 92 | .width(1.dp) 93 | .height(24.dp) 94 | .fillMaxHeight() 95 | .align(Alignment.CenterVertically) 96 | .background(color = PlayTheme.colors.uiBorder) 97 | ) 98 | Column(modifier = Modifier.padding(start = 12.dp, end = 12.dp)) { 99 | Text( 100 | text = "3+", 101 | maxLines = 1, 102 | style = MaterialTheme.typography.subtitle1, 103 | fontWeight = FontWeight.Bold, 104 | color = PlayTheme.colors.textSecondaryDark, 105 | modifier = Modifier.align(Alignment.CenterHorizontally) 106 | ) 107 | Text( 108 | text = "Rated For 3+", 109 | maxLines = 1, 110 | style = MaterialTheme.typography.caption, 111 | color = PlayTheme.colors.textSecondary, 112 | modifier = Modifier.align(Alignment.CenterHorizontally) 113 | ) 114 | } 115 | Spacer( 116 | modifier = Modifier 117 | .width(1.dp) 118 | .height(24.dp) 119 | .fillMaxHeight() 120 | .align(Alignment.CenterVertically) 121 | .background(color = PlayTheme.colors.uiBorder) 122 | ) 123 | Column(modifier = Modifier.padding(start = 12.dp, end = 12.dp)) { 124 | Text( 125 | text = "10M+", 126 | maxLines = 1, 127 | style = MaterialTheme.typography.subtitle1, 128 | fontWeight = FontWeight.Bold, 129 | color = PlayTheme.colors.textSecondaryDark, 130 | modifier = Modifier.align(Alignment.CenterHorizontally) 131 | ) 132 | Text( 133 | text = "Downloads", 134 | maxLines = 1, 135 | style = MaterialTheme.typography.caption, 136 | color = PlayTheme.colors.textSecondary, 137 | modifier = Modifier.align(Alignment.CenterHorizontally) 138 | ) 139 | } 140 | } 141 | } 142 | 143 | @Preview("Stats") 144 | @Composable 145 | private fun StatsPreview() { 146 | PlayTheme { 147 | PlaySurface { 148 | AppRepo.getApp(1L)?.let { safeApp -> 149 | Stats(safeApp) 150 | } 151 | } 152 | } 153 | } 154 | 155 | @Preview("Stats Dark") 156 | @Composable 157 | private fun StatsDarkPreview() { 158 | PlayTheme(darkTheme = true) { 159 | PlaySurface { 160 | AppRepo.getApp(1L)?.let { safeApp -> 161 | Stats(safeApp) 162 | } 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/games/Games.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.games 2 | 3 | import androidx.compose.animation.Crossfade 4 | import androidx.compose.animation.core.LinearOutSlowInEasing 5 | import androidx.compose.animation.core.TweenSpec 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.saveable.rememberSaveable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.tooling.preview.Preview 14 | import com.example.play.data.AppRepo 15 | import com.example.play.theme.PlayTheme 16 | import com.example.play.ui.AppsCategory 17 | import com.example.play.ui.AppsCategory.Categories 18 | import com.example.play.ui.AppsCategory.EarlyAccess 19 | import com.example.play.ui.AppsCategory.EditorsChoice 20 | import com.example.play.ui.AppsCategory.ForYou 21 | import com.example.play.ui.AppsCategory.TopCharts 22 | import com.example.play.ui.apps.applist.ForYouLayout 23 | import com.example.play.ui.apps.applist.TopChartsLayout 24 | import com.example.play.ui.components.PlaySurface 25 | import com.example.play.ui.AppsCategoryTabs 26 | import com.google.accompanist.insets.navigationBarsPadding 27 | 28 | @Composable 29 | fun Games( 30 | onAppSelected: (Long) -> Unit, 31 | modifier: Modifier = Modifier, 32 | appsCategories: List = getGamesCategoriesList() 33 | ) { 34 | val forYouData = remember { AppRepo.getForYouGames() } 35 | val topChartsData = remember { AppRepo.getTopChartsGames() } 36 | val (currentCategory, setCurrentCategory) = rememberSaveable { mutableStateOf(ForYou) } 37 | 38 | PlaySurface(modifier = modifier.fillMaxSize()) { 39 | Column(modifier = Modifier.navigationBarsPadding(left = true, right = true)) { 40 | AppsCategoryTabs( 41 | categories = appsCategories, 42 | selectedCategory = currentCategory, 43 | onCategorySelected = setCurrentCategory 44 | ) 45 | val tweenSpec = remember { getAnimSpec() } 46 | 47 | Crossfade(currentCategory, animationSpec = tweenSpec) { category -> 48 | when (category) { 49 | ForYou -> ForYouLayout(forYouData, onAppSelected = onAppSelected) 50 | TopCharts -> TopChartsLayout(topChartsData, onAppSelected = onAppSelected) 51 | else -> ForYouLayout(forYouData, onAppSelected = onAppSelected) 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | private fun getAnimSpec(): TweenSpec { 59 | return TweenSpec( 60 | durationMillis = 600, 61 | easing = LinearOutSlowInEasing 62 | ) 63 | } 64 | 65 | private fun getGamesCategoriesList() = listOf( 66 | ForYou, TopCharts, Categories, EditorsChoice, EarlyAccess 67 | ) 68 | 69 | @Preview("Games") 70 | @Composable 71 | fun GamesPreview() { 72 | PlayTheme { 73 | Games(onAppSelected = {}) 74 | } 75 | } 76 | 77 | @Preview("Games • Dark Theme") 78 | @Composable 79 | fun GamesDarkPreview() { 80 | PlayTheme(darkTheme = true) { 81 | Games(onAppSelected = {}) 82 | } 83 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/movies/Movies.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.movies 2 | 3 | import androidx.compose.animation.Crossfade 4 | import androidx.compose.animation.core.LinearOutSlowInEasing 5 | import androidx.compose.animation.core.TweenSpec 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.saveable.rememberSaveable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.tooling.preview.Preview 14 | import com.example.play.data.AppRepo 15 | import com.example.play.theme.PlayTheme 16 | import com.example.play.ui.MoviesCategory 17 | import com.example.play.ui.MoviesCategory.ForYou 18 | import com.example.play.ui.MoviesCategory.NewReleases 19 | import com.example.play.ui.MoviesCategory.TopSelling 20 | import com.example.play.ui.components.PlaySurface 21 | import com.example.play.ui.MoviesCategoryTabs 22 | import com.example.play.ui.movies.movielist.MoviesForYouLayout 23 | import com.example.play.ui.movies.movielist.TopSellingLayout 24 | import com.google.accompanist.insets.navigationBarsPadding 25 | 26 | @Composable 27 | fun Movies( 28 | modifier: Modifier = Modifier, 29 | moviesCategories: List = listOf( 30 | ForYou, TopSelling, NewReleases 31 | ) 32 | ) { 33 | val forYouData = remember { AppRepo.getForYouMovies() } 34 | val topSellingData = remember { AppRepo.getTopSellingMovies() } 35 | val newReleasesData = remember { AppRepo.getNewReleasesMovies() } 36 | 37 | val (currentCategory, setCurrentCategory) = rememberSaveable { 38 | mutableStateOf(ForYou) 39 | } 40 | 41 | PlaySurface(modifier = modifier.fillMaxSize()) { 42 | Column(modifier = Modifier.navigationBarsPadding(left = true, right = true)) { 43 | MoviesCategoryTabs( 44 | categories = moviesCategories, 45 | selectedCategory = currentCategory, 46 | onCategorySelected = setCurrentCategory 47 | ) 48 | val tweenSpec = remember { getAnimSpec() } 49 | Crossfade(currentCategory, animationSpec = tweenSpec) { category -> 50 | when (category) { 51 | ForYou -> MoviesForYouLayout(forYouData) 52 | TopSelling -> TopSellingLayout(topSellingData) 53 | NewReleases -> TopSellingLayout(newReleasesData) 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | private fun getAnimSpec(): TweenSpec { 61 | return TweenSpec( 62 | durationMillis = 600, 63 | easing = LinearOutSlowInEasing 64 | ) 65 | } 66 | 67 | @Preview("Movies") 68 | @Composable 69 | fun MoviesPreview() { 70 | PlayTheme { 71 | Movies() 72 | } 73 | } 74 | 75 | @Preview("Movies • Dark Theme") 76 | @Composable 77 | fun MoviesDarkPreview() { 78 | PlayTheme(darkTheme = true) { 79 | Movies() 80 | } 81 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/movies/movielist/MovieItem.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.movies.movielist 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.layout.width 14 | import androidx.compose.material.Icon 15 | import androidx.compose.material.MaterialTheme 16 | import androidx.compose.material.Text 17 | import androidx.compose.material.icons.Icons 18 | import androidx.compose.material.icons.outlined.Star 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.text.TextStyle 23 | import androidx.compose.ui.text.font.FontWeight 24 | import androidx.compose.ui.text.style.TextAlign.Center 25 | import androidx.compose.ui.text.style.TextOverflow.Ellipsis 26 | import androidx.compose.ui.tooling.preview.Preview 27 | import androidx.compose.ui.unit.dp 28 | import androidx.compose.ui.unit.sp 29 | import com.example.play.data.AppRepo 30 | import com.example.play.data.Movie 31 | import com.example.play.theme.PlayTheme 32 | import com.example.play.ui.components.PlayCard 33 | import com.example.play.ui.components.PlaySurface 34 | import com.example.play.ui.components.RoundedCornerAppImage 35 | 36 | @Composable 37 | fun MovieItem( 38 | movie: Movie, 39 | modifier: Modifier = Modifier 40 | ) { 41 | PlayCard( 42 | elevation = 0.dp, 43 | shape = MaterialTheme.shapes.large, 44 | modifier = modifier 45 | .size( 46 | width = 120.dp, 47 | height = 240.dp 48 | ) 49 | .padding(bottom = 8.dp) 50 | ) { 51 | Column( 52 | modifier = Modifier 53 | .clickable(onClick = {}) 54 | .fillMaxSize() 55 | ) { 56 | Box( 57 | modifier = Modifier 58 | .height(180.dp) 59 | .fillMaxWidth() 60 | ) { 61 | RoundedCornerAppImage( 62 | imageUrl = movie.imageUrl, 63 | modifier = Modifier 64 | .width(120.dp) 65 | .height(180.dp) 66 | .align(Alignment.TopStart) 67 | .padding(8.dp), 68 | cornerPercent = 10 69 | ) 70 | } 71 | Spacer(modifier = Modifier.height(3.dp)) 72 | Text( 73 | text = movie.name, 74 | style = TextStyle( 75 | fontWeight = FontWeight.Normal, 76 | fontSize = 12.sp, 77 | letterSpacing = 0.15.sp 78 | ), 79 | color = PlayTheme.colors.textPrimary, 80 | maxLines = 1, 81 | overflow = Ellipsis, 82 | modifier = Modifier.padding(start = 8.dp) 83 | ) 84 | Spacer(modifier = Modifier.height(4.dp)) 85 | Row(modifier = Modifier.padding(start = 8.dp)) { 86 | Text( 87 | text = movie.ratings, 88 | style = TextStyle( 89 | fontWeight = FontWeight.Light, 90 | fontSize = 12.sp, 91 | letterSpacing = 0.15.sp 92 | ), 93 | color = PlayTheme.colors.textSecondaryDark, 94 | maxLines = 1, 95 | overflow = Ellipsis 96 | ) 97 | Icon( 98 | imageVector = Icons.Outlined.Star, 99 | tint = PlayTheme.colors.iconTint, 100 | modifier = Modifier 101 | .padding(start = 2.dp, end = 8.dp) 102 | .width(8.dp) 103 | .height(8.dp) 104 | .align(Alignment.CenterVertically), 105 | contentDescription = null 106 | ) 107 | Text( 108 | text = movie.price, 109 | style = TextStyle( 110 | fontWeight = FontWeight.Light, 111 | fontSize = 12.sp, 112 | letterSpacing = 0.15.sp 113 | ), 114 | color = PlayTheme.colors.textSecondaryDark, 115 | maxLines = 1, 116 | overflow = Ellipsis 117 | ) 118 | } 119 | } 120 | } 121 | } 122 | 123 | @Composable 124 | fun TopSellingMovieItem( 125 | movie: Movie, 126 | modifier: Modifier = Modifier 127 | ) { 128 | PlaySurface( 129 | modifier = modifier 130 | .fillMaxWidth() 131 | .clickable(onClick = { }) 132 | ) { 133 | Row(modifier = Modifier.padding(start = 16.dp, end = 8.dp, top = 4.dp, bottom = 4.dp)) { 134 | Text( 135 | text = movie.id.toString(), 136 | style = TextStyle( 137 | fontWeight = FontWeight.Normal, 138 | fontSize = 14.sp 139 | ), 140 | textAlign = Center, 141 | color = PlayTheme.colors.textSecondary, 142 | modifier = Modifier 143 | .align(Alignment.CenterVertically) 144 | .width(30.dp) 145 | .padding(end = 8.dp) 146 | ) 147 | Box( 148 | modifier = Modifier 149 | .height(90.dp) 150 | .width(65.dp) 151 | ) { 152 | RoundedCornerAppImage( 153 | imageUrl = movie.imageUrl, 154 | modifier = Modifier 155 | .fillMaxWidth() 156 | .align(Alignment.TopStart) 157 | .padding(8.dp), 158 | cornerPercent = 5 159 | ) 160 | } 161 | Column( 162 | modifier = Modifier 163 | .padding(top = 4.dp, start = 8.dp) 164 | .align(Alignment.Top) 165 | ) { 166 | Text( 167 | text = movie.name, 168 | style = TextStyle( 169 | fontWeight = FontWeight.Normal, 170 | fontSize = 14.sp, 171 | letterSpacing = 0.15.sp 172 | ), 173 | color = PlayTheme.colors.textPrimary, 174 | maxLines = 1, 175 | modifier = Modifier.padding(bottom = 3.dp), 176 | overflow = Ellipsis 177 | ) 178 | Text( 179 | text = movie.category, 180 | style = TextStyle( 181 | fontWeight = FontWeight.Normal, 182 | fontSize = 12.sp, 183 | letterSpacing = 0.15.sp 184 | ), 185 | color = PlayTheme.colors.textSecondary, 186 | maxLines = 1, 187 | overflow = Ellipsis, 188 | modifier = Modifier.padding(bottom = 2.dp) 189 | ) 190 | Row(modifier = Modifier.padding(bottom = 2.dp)) { 191 | Text( 192 | text = movie.ratings, 193 | style = TextStyle( 194 | fontWeight = FontWeight.Light, 195 | fontSize = 12.sp, 196 | letterSpacing = 0.15.sp 197 | ), 198 | color = PlayTheme.colors.textSecondaryDark, 199 | maxLines = 1, 200 | overflow = Ellipsis 201 | ) 202 | Icon( 203 | imageVector = Icons.Outlined.Star, 204 | tint = PlayTheme.colors.iconTint, 205 | modifier = Modifier 206 | .padding(start = 2.dp, end = 8.dp) 207 | .width(8.dp) 208 | .height(8.dp) 209 | .align(Alignment.CenterVertically), 210 | contentDescription = null 211 | ) 212 | } 213 | Text( 214 | text = movie.price, 215 | style = TextStyle( 216 | fontWeight = FontWeight.Light, 217 | fontSize = 12.sp, 218 | letterSpacing = 0.15.sp 219 | ), 220 | color = PlayTheme.colors.textSecondaryDark, 221 | maxLines = 1, 222 | overflow = Ellipsis, 223 | modifier = Modifier.padding(bottom = 2.dp), 224 | ) 225 | } 226 | } 227 | } 228 | } 229 | 230 | @Preview("Movie Item") 231 | @Composable 232 | fun PlayMovieItemPreview() { 233 | PlayTheme { 234 | val movie = AppRepo.getMovie(2L) 235 | MovieItem( 236 | movie = movie 237 | ) 238 | } 239 | } 240 | 241 | @Preview("Top Selling Movie Item") 242 | @Composable 243 | fun TopSellingMovieItemPreview() { 244 | PlayTheme { 245 | val movie = AppRepo.getMovie(1L) 246 | TopSellingMovieItem( 247 | movie = movie 248 | ) 249 | } 250 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/movies/movielist/MoviesForYou.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.movies.movielist 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.lazy.LazyColumn 10 | import androidx.compose.foundation.lazy.LazyRow 11 | import androidx.compose.foundation.lazy.items 12 | import androidx.compose.material.Icon 13 | import androidx.compose.material.IconButton 14 | import androidx.compose.material.Text 15 | import androidx.compose.material.icons.Icons 16 | import androidx.compose.material.icons.outlined.ArrowForward 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.key 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.text.TextStyle 22 | import androidx.compose.ui.text.font.FontWeight 23 | import androidx.compose.ui.text.style.TextOverflow 24 | import androidx.compose.ui.tooling.preview.Preview 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.sp 27 | import com.example.play.data.AppRepo 28 | import com.example.play.data.Movie 29 | import com.example.play.data.MovieCollection 30 | import com.example.play.theme.PlayTheme 31 | import com.google.accompanist.insets.navigationBarsPadding 32 | 33 | @Composable 34 | fun MoviesForYouLayout( 35 | data: List, 36 | modifier: Modifier = Modifier 37 | ) { 38 | Spacer( 39 | modifier = Modifier 40 | .height(4.dp) 41 | ) 42 | LazyColumn(modifier = modifier) { 43 | items(data) { moviesCollection -> 44 | key(moviesCollection.id) { 45 | MoviesForYou( 46 | moviesCollection = moviesCollection 47 | ) 48 | } 49 | } 50 | } 51 | Spacer( 52 | modifier = Modifier 53 | .navigationBarsPadding(left = false, right = false) 54 | .height(8.dp) 55 | ) 56 | } 57 | 58 | @Composable 59 | fun MoviesForYou( 60 | moviesCollection: MovieCollection, 61 | modifier: Modifier = Modifier 62 | ) { 63 | Column(modifier = modifier) { 64 | Row( 65 | verticalAlignment = Alignment.CenterVertically, 66 | modifier = Modifier 67 | .padding(start = 24.dp) 68 | ) { 69 | Text( 70 | text = moviesCollection.name, 71 | style = TextStyle( 72 | fontWeight = FontWeight.Medium, 73 | fontSize = 16.sp, 74 | letterSpacing = 0.15.sp 75 | ), 76 | color = PlayTheme.colors.textPrimary, 77 | maxLines = 1, 78 | overflow = TextOverflow.Ellipsis, 79 | modifier = Modifier.weight(1f) 80 | ) 81 | IconButton( 82 | onClick = {}, 83 | modifier = Modifier.align(Alignment.CenterVertically) 84 | ) { 85 | Icon( 86 | imageVector = Icons.Outlined.ArrowForward, 87 | tint = PlayTheme.colors.iconTint, 88 | contentDescription = null 89 | ) 90 | } 91 | } 92 | MoviesList(moviesCollection.movies) 93 | } 94 | } 95 | 96 | @Composable 97 | private fun MoviesList( 98 | movies: List 99 | ) { 100 | LazyRow(modifier = Modifier.padding(start = 16.dp)) { 101 | items(movies) { movie -> 102 | MovieItem(movie) 103 | Spacer(modifier = Modifier.width(1.dp)) 104 | } 105 | } 106 | } 107 | 108 | @Preview("Movies For You list preview") 109 | @Composable 110 | fun MoviesForYouListPreview() { 111 | PlayTheme { 112 | MoviesForYouLayout(data = AppRepo.getMovies()) 113 | } 114 | } 115 | 116 | @Preview("Movies For You list dark preview") 117 | @Composable 118 | fun MoviesForYouListDarkPreview() { 119 | PlayTheme(darkTheme = true) { 120 | MoviesForYouLayout(data = AppRepo.getMovies()) 121 | } 122 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/ui/movies/movielist/TopSelling.kt: -------------------------------------------------------------------------------- 1 | package com.example.play.ui.movies.movielist 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.lazy.LazyColumn 6 | import androidx.compose.foundation.lazy.items 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.unit.dp 10 | import androidx.compose.ui.tooling.preview.Preview 11 | import com.example.play.data.AppRepo 12 | import com.example.play.data.Movie 13 | import com.example.play.theme.PlayTheme 14 | 15 | @Composable 16 | fun TopSellingLayout( 17 | moviesCollection: List, 18 | modifier: Modifier = Modifier 19 | ) { 20 | Column(modifier = modifier) { 21 | TopSellingMoviesList( 22 | movies = moviesCollection 23 | ) 24 | } 25 | } 26 | 27 | @Composable 28 | private fun TopSellingMoviesList( 29 | movies: List, 30 | modifier: Modifier = Modifier 31 | ) { 32 | LazyColumn(modifier = modifier.padding(top = 8.dp)) { 33 | items(movies) { movie -> 34 | TopSellingMovieItem(movie) 35 | } 36 | } 37 | } 38 | 39 | @Preview("Top Selling preview") 40 | @Composable 41 | fun TopSellingPreview() { 42 | PlayTheme { 43 | TopSellingLayout(moviesCollection = AppRepo.getTopSellingMovies()) 44 | } 45 | } 46 | 47 | @Preview("Top Selling dark preview") 48 | @Composable 49 | fun TopSellingDarkPreview() { 50 | PlayTheme(darkTheme = true) { 51 | TopSellingLayout(moviesCollection = AppRepo.getTopSellingMovies()) 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/utils/Navigation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.play.utils 18 | 19 | import android.os.Parcelable 20 | import androidx.activity.OnBackPressedCallback 21 | import androidx.activity.OnBackPressedDispatcher 22 | import androidx.compose.runtime.saveable.listSaver 23 | import androidx.compose.runtime.toMutableStateList 24 | 25 | /** 26 | * A simple navigator which maintains a back stack. 27 | */ 28 | class Navigator private constructor( 29 | initialBackStack: List, 30 | backDispatcher: OnBackPressedDispatcher 31 | ) { 32 | constructor( 33 | initial: T, 34 | backDispatcher: OnBackPressedDispatcher 35 | ) : this(listOf(initial), backDispatcher) 36 | 37 | private val backStack = initialBackStack.toMutableStateList() 38 | private val backCallback = object : OnBackPressedCallback(canGoBack()) { 39 | override fun handleOnBackPressed() { 40 | back() 41 | } 42 | }.also { callback -> 43 | backDispatcher.addCallback(callback) 44 | } 45 | val current: T get() = backStack.last() 46 | 47 | fun back() { 48 | backStack.removeAt(backStack.lastIndex) 49 | backCallback.isEnabled = canGoBack() 50 | } 51 | 52 | fun navigate(destination: T) { 53 | backStack += destination 54 | backCallback.isEnabled = canGoBack() 55 | } 56 | 57 | private fun canGoBack(): Boolean = backStack.size > 1 58 | 59 | companion object { 60 | /** 61 | * Serialize the back stack to save to instance state. 62 | */ 63 | fun saver(backDispatcher: OnBackPressedDispatcher) = 64 | listSaver, T>( 65 | save = { navigator -> navigator.backStack.toList() }, 66 | restore = { backstack -> Navigator(backstack, backDispatcher) } 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/play/utils/SystemUi.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.play.utils 18 | 19 | /** 20 | * Moved to https://google.github.io/accompanist/systemuicontroller/ 21 | */ -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_black_rect.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_apps.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_books.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_books_otlined.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_box.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_download.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_games.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_movies.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_movies_outlined.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star_solid.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/user_profile_pic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pushpalroy/jetstore/5d9487920c60a3a582f99bdcfa3a8a71dd174d46/app/src/main/res/drawable/user_profile_pic.jpg -------------------------------------------------------------------------------- /app/src/main/res/font/product_sans_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pushpalroy/jetstore/5d9487920c60a3a582f99bdcfa3a8a71dd174d46/app/src/main/res/font/product_sans_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/product_sans_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pushpalroy/jetstore/5d9487920c60a3a582f99bdcfa3a8a71dd174d46/app/src/main/res/font/product_sans_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pushpalroy/jetstore/5d9487920c60a3a582f99bdcfa3a8a71dd174d46/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pushpalroy/jetstore/5d9487920c60a3a582f99bdcfa3a8a71dd174d46/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pushpalroy/jetstore/5d9487920c60a3a582f99bdcfa3a8a71dd174d46/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pushpalroy/jetstore/5d9487920c60a3a582f99bdcfa3a8a71dd174d46/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pushpalroy/jetstore/5d9487920c60a3a582f99bdcfa3a8a71dd174d46/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pushpalroy/jetstore/5d9487920c60a3a582f99bdcfa3a8a71dd174d46/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pushpalroy/jetstore/5d9487920c60a3a582f99bdcfa3a8a71dd174d46/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pushpalroy/jetstore/5d9487920c60a3a582f99bdcfa3a8a71dd174d46/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pushpalroy/jetstore/5d9487920c60a3a582f99bdcfa3a8a71dd174d46/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pushpalroy/jetstore/5d9487920c60a3a582f99bdcfa3a8a71dd174d46/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 22 | 23 |