From 7ba9c0ddc1c2482bc63b28bc27134d36fe39bf28 Mon Sep 17 00:00:00 2001 From: AbdElraouf Sabri Date: Wed, 3 Mar 2021 06:31:13 +0200 Subject: [PATCH] Finish --- README.md | 43 +- app/build.gradle | 73 ++-- .../ExampleInstrumentedTest.kt | 1 + app/src/main/AndroidManifest.xml | 11 +- .../androiddevchallenge/MainActivity.kt | 61 --- .../example/androiddevchallenge/model/Pet.kt | 413 ++++++++++++++++++ .../androiddevchallenge/ui/MainActivity.kt | 37 ++ .../androiddevchallenge/ui/NavGraph.kt | 79 ++++ .../example/androiddevchallenge/ui/PetsApp.kt | 19 + .../ui/common/LikeButton.kt | 41 ++ .../ui/common/OutlinedAvatar.kt | 78 ++++ .../ui/common/StaggeredVerticalGrid.kt | 62 +++ .../androiddevchallenge/ui/pet/PetDetails.kt | 250 +++++++++++ .../androiddevchallenge/ui/pets/AllPets.kt | 282 ++++++++++++ .../ui/pets/FavoritePets.kt | 17 + .../androiddevchallenge/ui/pets/Pets.kt | 160 +++++++ .../androiddevchallenge/ui/theme/Color.kt | 12 + .../ui/theme/Elevations.kt | 30 ++ .../androiddevchallenge/ui/theme/Images.kt | 31 ++ .../androiddevchallenge/ui/theme/Theme.kt | 48 +- .../ui/utils/Navigation.kt | 63 +++ .../ui/utils/NetworkImage.kt | 116 +++++ .../androiddevchallenge/ui/utils/scrim.kt | 14 + app/src/main/res/drawable/adopt.xml | 266 +++++++++++ app/src/main/res/drawable/ic_cat.xml | 60 +++ app/src/main/res/drawable/ic_dog.xml | 83 ++++ app/src/main/res/drawable/ic_favorite.xml | 9 + app/src/main/res/drawable/ic_home.xml | 10 + app/src/main/res/values-night/themes.xml | 45 +- app/src/main/res/values-v29/color.xml | 19 + app/src/main/res/values/colors.xml | 9 +- app/src/main/res/values/strings.xml | 38 +- app/src/main/res/values/themes.xml | 53 +-- build.gradle | 42 +- buildSrc/build.gradle.kts | 23 + .../buildsrc/Dependencies.kt | 97 ++++ gradle.properties | 39 +- results/screenshot_1.png | Bin 27926 -> 627970 bytes results/screenshot_2.png | Bin 27926 -> 934133 bytes results/video.mp4 | Bin 17857 -> 15593548 bytes 40 files changed, 2519 insertions(+), 215 deletions(-) delete mode 100644 app/src/main/java/com/example/androiddevchallenge/MainActivity.kt create mode 100644 app/src/main/java/com/example/androiddevchallenge/model/Pet.kt create mode 100644 app/src/main/java/com/example/androiddevchallenge/ui/MainActivity.kt create mode 100644 app/src/main/java/com/example/androiddevchallenge/ui/NavGraph.kt create mode 100644 app/src/main/java/com/example/androiddevchallenge/ui/PetsApp.kt create mode 100644 app/src/main/java/com/example/androiddevchallenge/ui/common/LikeButton.kt create mode 100644 app/src/main/java/com/example/androiddevchallenge/ui/common/OutlinedAvatar.kt create mode 100644 app/src/main/java/com/example/androiddevchallenge/ui/common/StaggeredVerticalGrid.kt create mode 100644 app/src/main/java/com/example/androiddevchallenge/ui/pet/PetDetails.kt create mode 100644 app/src/main/java/com/example/androiddevchallenge/ui/pets/AllPets.kt create mode 100644 app/src/main/java/com/example/androiddevchallenge/ui/pets/FavoritePets.kt create mode 100644 app/src/main/java/com/example/androiddevchallenge/ui/pets/Pets.kt create mode 100644 app/src/main/java/com/example/androiddevchallenge/ui/theme/Elevations.kt create mode 100644 app/src/main/java/com/example/androiddevchallenge/ui/theme/Images.kt create mode 100644 app/src/main/java/com/example/androiddevchallenge/ui/utils/Navigation.kt create mode 100644 app/src/main/java/com/example/androiddevchallenge/ui/utils/NetworkImage.kt create mode 100644 app/src/main/java/com/example/androiddevchallenge/ui/utils/scrim.kt create mode 100644 app/src/main/res/drawable/adopt.xml create mode 100644 app/src/main/res/drawable/ic_cat.xml create mode 100644 app/src/main/res/drawable/ic_dog.xml create mode 100644 app/src/main/res/drawable/ic_favorite.xml create mode 100644 app/src/main/res/drawable/ic_home.xml create mode 100644 app/src/main/res/values-v29/color.xml create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/src/main/java/com/example/androiddevchallenge/buildsrc/Dependencies.kt diff --git a/README.md b/README.md index 1c4a2c9..0ddc6af 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,22 @@ -# Template repository - -Template repository for the Jetpack Compose [#AndroidDevChallenge](https://developer.android.com/dev-challenge). - -## Getting started -Copy this repository by pressing the "Use this template" button in Github. -Clone your repository and open it in the latest [Android Studio (Canary build)](https://developer.android.com/studio/preview). - -## Submission requirements -- Follow the challenge description on the project website: [developer.android.com/dev-challenge](https://developer.android.com/dev-challenge) -- All UI should be written using Jetpack Compose -- The Github Actions workflow should complete successfully -- Include two screenshots of your submission in the [results](results) folder. The names should be - screenshot_1.png and screenshot_2.png. -- Include a screen record of your submission in the [results](results) folder. The name should be - video.mp4 -- Replace the contents of [README.md](README.md) with the contents of [README-template.md](README-template.md) and fill out the template. - -## Code formatting -The CI uses [Spotless](https://github.com/diffplug/spotless) to check if your code is formatted correctly and contains the right licenses. -Internally, Spotless uses [ktlint](https://github.com/pinterest/ktlint) to check the formatting of your code. -To set up ktlint correctly with Android Studio, follow one of the [listed setup options](https://github.com/pinterest/ktlint#-with-intellij-idea). - -Before committing your code, run `./gradlew app:spotlessApply` to automatically format your code. +# PEts + + + +![Workflow result](https://github.com/AbdElraoufSabri//workflows/Check/badge.svg) + + +## :scroll: Description + + + +## :bulb: Motivation and Context + + + + +## :camera_flash: Screenshots + + ## License ``` diff --git a/app/build.gradle b/app/build.gradle index 14961cf..0ccb1c6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,3 +1,5 @@ +import com.example.androiddevchallenge.buildsrc.Libs + plugins { id 'com.android.application' id 'kotlin-android' @@ -8,12 +10,13 @@ android { defaultConfig { applicationId "com.example.androiddevchallenge" - minSdkVersion 23 + minSdkVersion 21 targetSdkVersion 30 versionCode 1 versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables.useSupportLibrary true + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } signingConfigs { @@ -31,18 +34,18 @@ android { signingConfig signingConfigs.debug } release { - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } - kotlinOptions { - jvmTarget = "1.8" + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } buildFeatures { compose true - // Disable unused AGP features buildConfig false aidl false @@ -51,31 +54,47 @@ android { shaders false } - composeOptions { - kotlinCompilerExtensionVersion compose_version + packagingOptions { + exclude "META-INF/licenses/**" + exclude "META-INF/AL2.0" + exclude "META-INF/LGPL2.1" } - packagingOptions { - // Multiple dependency bring these files in. Exclude them to enable - // our test APK to build (has no effect on our AARs) - excludes += "/META-INF/AL2.0" - excludes += "/META-INF/LGPL2.1" + composeOptions { + kotlinCompilerExtensionVersion Libs.AndroidX.Compose.version } } dependencies { - implementation 'androidx.core:core-ktx:1.3.2' - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'com.google.android.material:material:1.3.0' - implementation "androidx.activity:activity-compose:1.3.0-alpha03" - implementation "androidx.compose.ui:ui:$compose_version" - implementation "androidx.compose.material:material:$compose_version" - implementation "androidx.compose.material:material-icons-extended:$compose_version" - implementation "androidx.compose.ui:ui-tooling:$compose_version" - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0' - - testImplementation 'junit:junit:4.13.2' - - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" + implementation Libs.Kotlin.stdlib + implementation Libs.Kotlin.reflect + implementation Libs.Coroutines.android + + implementation Libs.AndroidX.coreKtx + implementation Libs.AndroidX.navigation + implementation Libs.AndroidX.Activity.activityCompose + implementation Libs.AndroidX.ConstraintLayout.constraintLayoutCompose + + implementation Libs.AndroidX.Compose.runtime + implementation Libs.AndroidX.Compose.foundation + implementation Libs.AndroidX.Compose.layout + implementation Libs.AndroidX.Compose.ui + implementation Libs.AndroidX.Compose.uiUtil + implementation Libs.AndroidX.Compose.material + implementation Libs.AndroidX.Compose.animation + implementation Libs.AndroidX.Compose.iconsExtended + implementation Libs.AndroidX.Compose.tooling + + implementation Libs.Accompanist.coil + implementation Libs.Accompanist.gif + implementation Libs.Accompanist.insets + + androidTestImplementation Libs.AndroidX.Activity.activityCompose + + androidTestImplementation Libs.JUnit.junit + androidTestImplementation Libs.AndroidX.Test.core + androidTestImplementation Libs.AndroidX.Test.espressoCore + androidTestImplementation Libs.AndroidX.Test.rules + androidTestImplementation Libs.AndroidX.Test.Ext.junit + androidTestImplementation Libs.AndroidX.Compose.uiTest } \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/androiddevchallenge/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/androiddevchallenge/ExampleInstrumentedTest.kt index f036cb4..29d2cfc 100644 --- a/app/src/androidTest/java/com/example/androiddevchallenge/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/example/androiddevchallenge/ExampleInstrumentedTest.kt @@ -17,6 +17,7 @@ package com.example.androiddevchallenge import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.example.androiddevchallenge.ui.MainActivity import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 223581b..f938ed0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,16 +13,17 @@ + + + + android:theme="@style/Theme.Pets"> + android:name="com.example.androiddevchallenge.ui.MainActivity"> diff --git a/app/src/main/java/com/example/androiddevchallenge/MainActivity.kt b/app/src/main/java/com/example/androiddevchallenge/MainActivity.kt deleted file mode 100644 index d024d08..0000000 --- a/app/src/main/java/com/example/androiddevchallenge/MainActivity.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.example.androiddevchallenge - -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview -import com.example.androiddevchallenge.ui.theme.MyTheme - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - MyTheme { - MyApp() - } - } - } -} - -// Start building your app here! -@Composable -fun MyApp() { - Surface(color = MaterialTheme.colors.background) { - Text(text = "Ready... Set... GO!") - } -} - -@Preview("Light Theme", widthDp = 360, heightDp = 640) -@Composable -fun LightPreview() { - MyTheme { - MyApp() - } -} - -@Preview("Dark Theme", widthDp = 360, heightDp = 640) -@Composable -fun DarkPreview() { - MyTheme(darkTheme = true) { - MyApp() - } -} diff --git a/app/src/main/java/com/example/androiddevchallenge/model/Pet.kt b/app/src/main/java/com/example/androiddevchallenge/model/Pet.kt new file mode 100644 index 0000000..8aaee98 --- /dev/null +++ b/app/src/main/java/com/example/androiddevchallenge/model/Pet.kt @@ -0,0 +1,413 @@ +package com.example.androiddevchallenge.model + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.runtime.Immutable +import androidx.compose.ui.tooling.preview.datasource.LoremIpsum +import com.example.androiddevchallenge.R + +@Immutable +data class Pet( + val id: Long, + val name: String, + val category: PetCategory, + val thumbUrl: String, + val thumbContentDesc: String, + val description: String, + val owner: PetOwner, + val likes: Int, + val address: String, + val age: Int, + val weight: Float, + val color: String, +) + +sealed class PetCategory( + @StringRes val name: Int, + @DrawableRes val icon: Int +) { + object Cat : PetCategory(R.string.cats, R.drawable.ic_cat) + object Dog : PetCategory(R.string.dpgs, R.drawable.ic_dog) +} + +@Immutable +data class PetOwner( + val name: String, + val thumbUrl: String, + val thumbContentDesc: String, +) + +object PetRepo { + fun getPet(petId: Long): Pet = allPets.find { it.id == petId }!! + + fun getRelated(petId: Long): List { + val petType = getPet(petId).category + return allPets + .filter { it.category == petType } + .shuffled() + } +} + +fun randomDescriptionCount() = (50 until 70).random() +fun randomLikes() = (0 until 10000).random() +fun randomAmount() = (10 until 70).random() +fun randomAge() = (10 until 35).random() +fun randomWeight() = (15 until 25).random().toFloat() + +val owners = listOf( + PetOwner( + "Daniel Smith", + "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=500&h=500", + "Lisa Smith's image" + ), + PetOwner( + "Heather King", + "https://images.unsplash.com/photo-1554151228-14d9def656e4?w=500&h=500", + "Heather King's image" + ), + PetOwner( + "Donna Martin", + "https://images.unsplash.com/photo-1604426633861-11b2faead63c?w=500&h=500", + "Donna Martin's image" + ), + PetOwner( + "Anderson Cook", + "https://images.unsplash.com/photo-1499996860823-5214fcc65f8f?w=500&h=500", + "Sandra Cook's image" + ), + PetOwner( + "Laura Perez", + "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=500&h=500", + "Laura Perez's image" + ), + PetOwner( + "Carol Roberts", + "https://images.unsplash.com/photo-1557296387-5358ad7997bb?w=500&h=500", + "Carol Roberts's image" + ), + PetOwner( + "Charles Bryant", + "https://images.unsplash.com/photo-1592124549776-a7f0cc973b24?w=500&h=500", + "Charles Bryant's image" + ), + PetOwner( + "Linda Bennett", + "https://images.unsplash.com/photo-1521146764736-56c929d59c83?w=500&h=500", + "Linda Bennett's image" + ), + PetOwner( + "Frances Garcia", + "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=500&h=500", + "Frances Garcia's image" + ), + PetOwner( + "Rose Alexander", + "https://images.unsplash.com/photo-1488426862026-3ee34a7d66df?w=500&h=500", + "Rose Alexander's image" + ), +) + +val addresses = listOf( + "586 Stout Street, Harrisburg, Pennsylvania", + "2606 Williams Avenue, Newhall, California", + "921 Ventura Drive, Santa Clara, California", + "1016 Pride Avenue, Staten Island, New York", + "4957 Lang Avenue, Vernon, Utah", + "1979 Franklee Lane, LAS VEGAS, Nevada", + "2656 Colonial Drive, Houston, Texas", + "1041 Lake Road, Pleasantville, New Jersey", + "2587 Benedum Drive, New York, New York", +) + +val descriptions = listOf( + "Not everything that is faced can be changed, but nothing can be changed until it is faced.", + "It doesn't matter how strong your opinion are. If you don't use your power for positive change, you are indeed part of the problem", + "All great changes are preceded by choas", + "We all get scared and want to turn away, but it isn't always strength that makes you stay. Strength is also making the decision to change your destiny.", + "We must be impatient for change. Let us remember that our voice is a precious gift and we must use it", + "If you do not change direction, you might end up where you are heading", + "The moment of change is the only poem", + "The only way to make sense out of change is to plunge into it, move with it, and join the dance", + "Change will not come if we wait for some other person or some other time. We are the ones we’ve been waiting for. We are the change that we seek", +) + +val colors = listOf( + "White", + "Yellow", + "Blue", + "Red", + "Green", + "Black", + "Brown", + "Azure", +) + +val allPets = listOf( + Pet( + 0, + "Charlie", + PetCategory.Dog, + "https://images.unsplash.com/photo-1601979031925-424e53b6caaa?w=500&h=500", + "Charlie's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + Pet( + 0, + "Max", + PetCategory.Dog, + "https://images.unsplash.com/photo-1530667912788-f976e8ee0bd5?w=500&h=500", + "Max's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + Pet( + 0, + "Buddy", + PetCategory.Dog, + "https://images.unsplash.com/photo-1583511655826-05700d52f4d9?w=500&h=500", + "Buddy's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + Pet( + 0, + "Oscar", + PetCategory.Dog, + "https://images.unsplash.com/photo-1559284957-298b8b225576?w=500&h=500", + "Oscar's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + Pet( + 0, + "Archie", + PetCategory.Dog, + "https://images.unsplash.com/photo-1537151672256-6caf2e9f8c95?w=500&h=500", + "Archie's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + Pet( + 0, + "Ollie", + PetCategory.Dog, + "https://images.unsplash.com/photo-1608787450139-538a6d3bb683?w=500&h=500", + "Ollie's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + Pet( + 0, + "Toby", + PetCategory.Dog, + "https://images.unsplash.com/photo-1548858565-461b87144b6a?w=500&h=500", + "Toby's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + Pet( + 0, + "Jack", + PetCategory.Dog, + "https://images.unsplash.com/photo-1548658146-f142deadf8f7?w=500&h=500", + "Jack's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + Pet( + 0, + "Teddy", + PetCategory.Dog, + "https://images.unsplash.com/photo-1599507303682-4055ce7235ae?w=500&h=500", + "Teddy's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + Pet( + 0, + "Luna", + PetCategory.Cat, + "https://images.unsplash.com/photo-1560114928-40f1f1eb26a0?w=500&h=500", + "Luna's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + Pet( + 0, + "Bella", + PetCategory.Cat, + "https://images.unsplash.com/photo-1591871937573-74dbba515c4c?w=500&h=500", + "Bella's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + Pet( + 0, + "Lucy", + PetCategory.Cat, + "https://images.unsplash.com/photo-1533743983669-94fa5c4338ec?w=500&h=500", + "Lucy's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + Pet( + 0, + "Lily", + PetCategory.Cat, + "https://images.unsplash.com/photo-1603314585442-ee3b3c16fbcf?w=500&h=500", + "Lily's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + Pet( + 0, + "Chloe", + PetCategory.Cat, + "https://images.unsplash.com/photo-1568035105640-89538ccccd24?w=500&h=500", + "Chloe's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + Pet( + 0, + "Daisy", + PetCategory.Cat, + "https://images.unsplash.com/photo-1567270671170-fdc10a5bf831?w=500&h=500", + "Daisy's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + Pet( + 0, + "Mia", + PetCategory.Cat, + "https://images.unsplash.com/photo-1559059699-085698eba48c?w=500&h=500", + "Mia's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + Pet( + 0, + "Kiki", + PetCategory.Cat, + "https://images.unsplash.com/photo-1570419929578-07349d2138fa?w=500&h=500", + "Kiki's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + Pet( + 0, + "Zoe", + PetCategory.Cat, + "https://images.unsplash.com/photo-1589720247367-97b62ed5cfd1?w=500&h=500", + "Zoe's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + Pet( + 0, + "Piper", + PetCategory.Cat, + "https://images.unsplash.com/photo-1605530489666-6162f2cbd3b4?w=500&h=500", + "Piper's photo", + descriptions.random(), + owners.random(), + randomLikes(), + addresses.random(), + randomAge(), + randomWeight(), + colors.random() + ), + + ).shuffled() \ No newline at end of file diff --git a/app/src/main/java/com/example/androiddevchallenge/ui/MainActivity.kt b/app/src/main/java/com/example/androiddevchallenge/ui/MainActivity.kt new file mode 100644 index 0000000..54816a1 --- /dev/null +++ b/app/src/main/java/com/example/androiddevchallenge/ui/MainActivity.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.androiddevchallenge.ui + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.core.view.WindowCompat + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // This app draws behind the system bars, so we want to handle fitting system windows + WindowCompat.setDecorFitsSystemWindows(window, false) + + setContent { + PetsApp(onBackPressedDispatcher) + } + } + +} + diff --git a/app/src/main/java/com/example/androiddevchallenge/ui/NavGraph.kt b/app/src/main/java/com/example/androiddevchallenge/ui/NavGraph.kt new file mode 100644 index 0000000..33db9df --- /dev/null +++ b/app/src/main/java/com/example/androiddevchallenge/ui/NavGraph.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.androiddevchallenge.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.* +import com.example.androiddevchallenge.ui.MainDestinations.PET_DETAIL_ID_KEY +import com.example.androiddevchallenge.ui.pet.PetDetails +import com.example.androiddevchallenge.ui.pets.Pets + +/** + * Destinations used in the ([PetsApp]). + */ +object MainDestinations { + const val PETS_ROUTE = "pets" + const val PET_DETAIL_ROUTE = "pet" + const val PET_DETAIL_ID_KEY = "petId" +} + +@Composable +fun NavGraph(startDestination: String = MainDestinations.PETS_ROUTE) { + val navController = rememberNavController() + + val actions = remember(navController) { MainActions(navController) } + NavHost( + navController = navController, + startDestination = startDestination + ) { + composable(MainDestinations.PETS_ROUTE) { + Pets(lovePet = actions.lovePet, adoptPet = actions.adoptPet) + } + composable( + "${MainDestinations.PET_DETAIL_ROUTE}/{$PET_DETAIL_ID_KEY}", + arguments = listOf(navArgument(PET_DETAIL_ID_KEY) { type = NavType.LongType }) + ) { backStackEntry -> + val arguments = requireNotNull(backStackEntry.arguments) + PetDetails( + petId = arguments.getLong(PET_DETAIL_ID_KEY), + lovePet = actions.lovePet, + adoptPet = actions.lovePet, + upPress = actions.upPress + ) + } + } +} + + +/** + * Models the navigation actions in the app. + */ +class MainActions(navController: NavHostController) { + val lovePet: (Long) -> Unit = { petId: Long -> + navController.navigate("${MainDestinations.PET_DETAIL_ROUTE}/$petId") + } + val adoptPet: (Long) -> Unit = { petId: Long -> + navController.navigate("${MainDestinations.PET_DETAIL_ROUTE}/$petId") + } + + val upPress: () -> Unit = { + navController.navigateUp() + } +} diff --git a/app/src/main/java/com/example/androiddevchallenge/ui/PetsApp.kt b/app/src/main/java/com/example/androiddevchallenge/ui/PetsApp.kt new file mode 100644 index 0000000..faedee5 --- /dev/null +++ b/app/src/main/java/com/example/androiddevchallenge/ui/PetsApp.kt @@ -0,0 +1,19 @@ +package com.example.androiddevchallenge.ui + +import androidx.activity.OnBackPressedDispatcher +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import com.example.androiddevchallenge.ui.utils.LocalBackDispatcher +import com.example.androiddevchallenge.ui.utils.ProvideImageLoader +import dev.chrisbanes.accompanist.insets.ProvideWindowInsets + +@Composable +fun PetsApp(backDispatcher: OnBackPressedDispatcher) { + CompositionLocalProvider(LocalBackDispatcher provides backDispatcher) { + ProvideWindowInsets { + ProvideImageLoader { + NavGraph() + } + } + } +} diff --git a/app/src/main/java/com/example/androiddevchallenge/ui/common/LikeButton.kt b/app/src/main/java/com/example/androiddevchallenge/ui/common/LikeButton.kt new file mode 100644 index 0000000..05c0da8 --- /dev/null +++ b/app/src/main/java/com/example/androiddevchallenge/ui/common/LikeButton.kt @@ -0,0 +1,41 @@ +package com.example.androiddevchallenge.ui.common + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.androiddevchallenge.ui.theme.PetsTheme + + +@Composable +@Preview +fun LikeButton(modifier: Modifier = Modifier) { + val checked = remember { mutableStateOf(false) } + Box( + modifier = modifier + .size(32.dp) + .clip(CircleShape) + .clickable(onClick = { checked.value = !checked.value }) + .background(color = if (checked.value) Color.Red.copy(.5F) else Color.White) + .padding(8.dp) + ) { + Icon( + Icons.Filled.Favorite, + tint = if (checked.value) Color.White else PetsTheme.colors.secondary, + contentDescription = if (checked.value) "Unlike" else "Like" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/androiddevchallenge/ui/common/OutlinedAvatar.kt b/app/src/main/java/com/example/androiddevchallenge/ui/common/OutlinedAvatar.kt new file mode 100644 index 0000000..dba1838 --- /dev/null +++ b/app/src/main/java/com/example/androiddevchallenge/ui/common/OutlinedAvatar.kt @@ -0,0 +1,78 @@ +package com.example.androiddevchallenge.ui.common + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.example.androiddevchallenge.ui.theme.PetsTheme +import com.example.androiddevchallenge.ui.utils.NetworkImage + +@Composable +fun OutlinedAvatar( + url: String, + modifier: Modifier = Modifier, + outlineSize: Dp = 3.dp, + outlineColor: Color = MaterialTheme.colors.surface +) { + Box( + modifier = modifier.background( + color = outlineColor, + shape = CircleShape + ) + ) { + NetworkImage( + url = url, + contentDescription = null, + modifier = Modifier + .padding(outlineSize) + .fillMaxSize() + .clip(CircleShape) + ) + } +} + +@Composable +fun LocalRoundAvatar( + @DrawableRes resource: Int, + backgroundColor:Color, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .clip(CircleShape) + ) { + Image( + painter = painterResource(resource), + contentDescription = null, + modifier = Modifier + .fillMaxSize() + .clip(CircleShape) + .background(backgroundColor) + .padding(4.dp) + ) + } +} + +@Preview( + name = "Outlined Avatar", + widthDp = 40, + heightDp = 40 +) +@Composable +private fun OutlinedAvatarPreview() { + PetsTheme { + OutlinedAvatar(url = "") + } +} diff --git a/app/src/main/java/com/example/androiddevchallenge/ui/common/StaggeredVerticalGrid.kt b/app/src/main/java/com/example/androiddevchallenge/ui/common/StaggeredVerticalGrid.kt new file mode 100644 index 0000000..aa6a311 --- /dev/null +++ b/app/src/main/java/com/example/androiddevchallenge/ui/common/StaggeredVerticalGrid.kt @@ -0,0 +1,62 @@ +package com.example.androiddevchallenge.ui.common + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.unit.Dp +import kotlin.math.ceil + +@Composable +fun StaggeredVerticalGrid( + modifier: Modifier = Modifier, + maxColumnWidth: Dp, + content: @Composable () -> Unit +) { + Layout( + content = content, + modifier = modifier + ) { measurables, constraints -> + check(constraints.hasBoundedWidth) { + "Unbounded width not supported" + } + val columns = ceil(constraints.maxWidth / maxColumnWidth.toPx()).toInt() + val columnWidth = constraints.maxWidth / columns + val itemConstraints = constraints.copy(maxWidth = columnWidth) + val colHeights = IntArray(columns) { 0 } // track each column's height + val placeables = measurables.map { measurable -> + val column = shortestColumn(colHeights) + val placeable = measurable.measure(itemConstraints) + colHeights[column] += placeable.height + placeable + } + + val height = colHeights.maxOrNull()?.coerceIn(constraints.minHeight, constraints.maxHeight) + ?: constraints.minHeight + layout( + width = constraints.maxWidth, + height = height + ) { + val colY = IntArray(columns) { 0 } + placeables.forEach { placeable -> + val column = shortestColumn(colY) + placeable.place( + x = columnWidth * column, + y = colY[column] + ) + colY[column] += placeable.height + } + } + } +} + +private fun shortestColumn(colHeights: IntArray): Int { + var minHeight = Int.MAX_VALUE + var column = 0 + colHeights.forEachIndexed { index, height -> + if (height < minHeight) { + minHeight = height + column = index + } + } + return column +} diff --git a/app/src/main/java/com/example/androiddevchallenge/ui/pet/PetDetails.kt b/app/src/main/java/com/example/androiddevchallenge/ui/pet/PetDetails.kt new file mode 100644 index 0000000..2ee6fd0 --- /dev/null +++ b/app/src/main/java/com/example/androiddevchallenge/ui/pet/PetDetails.kt @@ -0,0 +1,250 @@ +package com.example.androiddevchallenge.ui.pet + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Call +import androidx.compose.material.icons.outlined.LocationOn +import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import com.example.androiddevchallenge.R +import com.example.androiddevchallenge.model.Pet +import com.example.androiddevchallenge.model.PetRepo +import com.example.androiddevchallenge.ui.common.LikeButton +import com.example.androiddevchallenge.ui.common.OutlinedAvatar +import com.example.androiddevchallenge.ui.theme.PetsTheme +import com.example.androiddevchallenge.ui.utils.NetworkImage +import com.example.androiddevchallenge.ui.utils.scrim +import dev.chrisbanes.accompanist.insets.statusBarsPadding + +@Composable +fun PetDetails( + petId: Long, + lovePet: (Long) -> Unit, + adoptPet: (Long) -> Unit, + upPress: () -> Unit +) { + val pet = remember(petId) { PetRepo.getPet(petId) } + PetDetails(pet, lovePet, adoptPet, upPress) +} + +@Composable +fun PetDetails(pet: Pet, lovePet: (Long) -> Unit, adoptPet: (Long) -> Unit, upPress: () -> Unit) { + PetsTheme { + Column(Modifier) { + Box { + NetworkImage( + url = pet.thumbUrl, + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .scrim(colors = listOf(Color(0x80000000), Color(0x33000000))) + .aspectRatio(4f / 3f) + ) + TopAppBar( + backgroundColor = Color.Transparent, + elevation = 0.dp, + contentColor = Color.White, // always white as image has dark scrim + modifier = Modifier.statusBarsPadding() + ) { + IconButton(onClick = upPress) { + Icon( + imageVector = Icons.Rounded.ArrowBack, + contentDescription = stringResource(R.string.label_back) + ) + } + Image( + painter = painterResource(id = R.drawable.adopt), + contentDescription = null, + modifier = Modifier + .padding(bottom = 4.dp) + .size(24.dp) + .align(Alignment.CenterVertically) + ) + } + } + ConstraintLayout { + val (name, addressIcon, address, like, age, color, weight, petStory, petStoryText, ownerAvatar, ownerPostedBy, ownerName, contactMe) = createRefs() + Text( + text = pet.name, + style = MaterialTheme.typography.h4, + modifier = Modifier + .constrainAs(name) { + top.linkTo(parent.top) + start.linkTo(parent.start, margin = 16.dp) + } + + ) + + Icon( + imageVector = Icons.Outlined.LocationOn, + contentDescription = pet.address, + tint = PetsTheme.colors.onSurface.copy(.5F), + modifier = Modifier.constrainAs(addressIcon) { + top.linkTo(name.bottom, margin = 16.dp) + start.linkTo(name.start) + } + ) + + Text( + text = pet.address, + style = MaterialTheme.typography.caption, + modifier = Modifier.constrainAs(address) { + top.linkTo(addressIcon.top) + bottom.linkTo(addressIcon.bottom) + start.linkTo(addressIcon.end, margin = 16.dp) + } + + ) + + LikeButton( + modifier = Modifier + .constrainAs(like) { + top.linkTo(name.top) + bottom.linkTo(addressIcon.bottom) + end.linkTo(parent.end, margin = 16.dp) + + } + ) + + PetDetailsCategoryCard("Age", "${pet.age} months", + modifier = Modifier + .constrainAs(age) { + top.linkTo(addressIcon.bottom, margin = 16.dp) + start.linkTo(addressIcon.start) + } + ) + PetDetailsCategoryCard("Color", pet.color, + modifier = Modifier + .constrainAs(color) { + top.linkTo(addressIcon.bottom, margin = 16.dp) + start.linkTo(age.end, margin = 4.dp) + } + ) + PetDetailsCategoryCard("Weight", "${pet.weight.toInt()} KG", + modifier = Modifier + .constrainAs(weight) { + top.linkTo(addressIcon.bottom, margin = 16.dp) + start.linkTo(color.end, margin = 4.dp) + } + ) + + Text( + text = "Pet Story", + style = MaterialTheme.typography.h6, + modifier = Modifier + .fillMaxWidth() + .constrainAs(petStory) { + top.linkTo(age.bottom, margin = 24.dp) + start.linkTo(age.start) + } + ) + + Text( + text = pet.description, + style = MaterialTheme.typography.body1, + modifier = Modifier + .fillMaxWidth() + .constrainAs(petStoryText) { + top.linkTo(petStory.bottom, margin = 16.dp) + start.linkTo(petStory.start) + } + ) + + OutlinedAvatar( + url = pet.owner.thumbUrl, + modifier = Modifier + .size(40.dp) + .constrainAs(ownerAvatar) { + top.linkTo(petStoryText.bottom, margin = 32.dp) + start.linkTo(petStory.start) + } + ) + + Text( + text = "Posted by", + style = MaterialTheme.typography.subtitle2, + modifier = Modifier + .fillMaxWidth() + .constrainAs(ownerPostedBy) { + top.linkTo(ownerAvatar.top) + start.linkTo(ownerAvatar.end, margin = 16.dp) + } + ) + + Text( + text = pet.owner.name, + style = MaterialTheme.typography.caption, + modifier = Modifier + .fillMaxWidth() + .constrainAs(ownerName) { + top.linkTo(ownerPostedBy.bottom) + bottom.linkTo(ownerAvatar.bottom) + start.linkTo(ownerPostedBy.start) + } + ) + + Button(onClick = { /*TODO*/ }, + shape = RoundedCornerShape(50), + modifier = Modifier + .constrainAs(contactMe) { + top.linkTo(ownerAvatar.top) + bottom.linkTo(ownerAvatar.bottom) + end.linkTo(parent.end, margin = 16.dp) + } + ) { + Icon(Icons.Default.Call, contentDescription = "Contact ${pet.owner.name}") + Text(modifier = Modifier.padding(start = 10.dp),text = "Contact me") + } + + } + } + } +} + +@Composable +fun PetDetailsCategoryCard( + title: String, + content: String, + modifier: Modifier = Modifier +) { + Surface( + modifier = modifier + .padding(4.dp) + .wrapContentSize() + .clip(RoundedCornerShape(8.dp)), + color = MaterialTheme.colors.surface.copy(.15F), + elevation = PetsTheme.elevations.card, + shape = RoundedCornerShape(16.dp), + border = BorderStroke(width = 1.dp, color = Color.Gray.copy(.12F)) + ) { + Column( + modifier = Modifier + .padding(8.dp) + ) { + Text( + text = content, + style = MaterialTheme.typography.subtitle2, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + Text( + text = title, + style = MaterialTheme.typography.caption, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + + } +} diff --git a/app/src/main/java/com/example/androiddevchallenge/ui/pets/AllPets.kt b/app/src/main/java/com/example/androiddevchallenge/ui/pets/AllPets.kt new file mode 100644 index 0000000..6ef6c61 --- /dev/null +++ b/app/src/main/java/com/example/androiddevchallenge/ui/pets/AllPets.kt @@ -0,0 +1,282 @@ +package com.example.androiddevchallenge.ui.pets + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.outlined.LocationOn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import com.example.androiddevchallenge.R +import com.example.androiddevchallenge.model.Pet +import com.example.androiddevchallenge.model.PetCategory +import com.example.androiddevchallenge.model.allPets +import com.example.androiddevchallenge.model.randomAmount +import com.example.androiddevchallenge.ui.common.LikeButton +import com.example.androiddevchallenge.ui.common.LocalRoundAvatar +import com.example.androiddevchallenge.ui.common.OutlinedAvatar +import com.example.androiddevchallenge.ui.common.StaggeredVerticalGrid +import com.example.androiddevchallenge.ui.theme.PetsTheme +import com.example.androiddevchallenge.ui.utils.NetworkImage +import dev.chrisbanes.accompanist.insets.statusBarsPadding + +@Composable +fun AllPets( + modifier: Modifier = Modifier, + pets: List = allPets, + lovePet: (Long) -> Unit = {}, + adoptPet: (Long) -> Unit = {} +) { + Column( + modifier = modifier + .verticalScroll(rememberScrollState()) + .statusBarsPadding() + ) { + PetsAppBar() + Header() + Categories {} + NewestPets(pets,adoptPet) + + Spacer(modifier = Modifier.height(100.dp)) + } + +} + +@Composable +fun NewestPets( + pets: List, + selectPet: (Long) -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text(text = "Newest Pets", style = MaterialTheme.typography.h6) + StaggeredVerticalGrid( + maxColumnWidth = 220.dp, + modifier = Modifier.padding(4.dp) + ) { + pets.forEach { + PetCard(Modifier, it, selectPet) + } + } + } +} + +@Composable +fun PetCard( + modifier: Modifier, + pet: Pet, + selectPet: (Long) -> Unit +) { + Surface( + modifier = modifier.padding(4.dp), + color = MaterialTheme.colors.surface, + elevation = PetsTheme.elevations.card, + shape = MaterialTheme.shapes.medium + ) { + val featuredString = stringResource(id = R.string.favorite) + ConstraintLayout( + modifier = Modifier + .clickable( + onClick = { selectPet(pet.id) } + ) + .semantics { + contentDescription = featuredString + } + ) { + val (image, like, avatar, name, address) = createRefs() + NetworkImage( + url = pet.thumbUrl, + contentDescription = null, + modifier = Modifier + .aspectRatio(4f / 3f) + .constrainAs(image) { + centerHorizontallyTo(parent) + top.linkTo(parent.top) + } + ) + val outlineColor = LocalElevationOverlay.current?.apply( + color = MaterialTheme.colors.surface, + elevation = PetsTheme.elevations.card + ) ?: MaterialTheme.colors.surface + + LikeButton( + modifier = Modifier + .constrainAs(like) { + top.linkTo(parent.top, margin = 16.dp) + end.linkTo(parent.end, margin = 16.dp) + } + ) + + OutlinedAvatar( + url = pet.owner.thumbUrl, + outlineColor = outlineColor, + modifier = Modifier + .size(38.dp) + .constrainAs(avatar) { + centerHorizontallyTo(parent) + centerAround(image.bottom) + } + ) + Text( + text = pet.name, + style = MaterialTheme.typography.subtitle2, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(horizontal = 16.dp) + .constrainAs(name) { + start.linkTo(parent.start) + top.linkTo(avatar.bottom) + } + ) + Row( + modifier = Modifier + .padding(16.dp) + .constrainAs(address) { + start.linkTo(parent.start) + top.linkTo(name.bottom) + end.linkTo(parent.end) + } + ) { + Icon( + imageVector = Icons.Outlined.LocationOn, + contentDescription = pet.address, + tint = PetsTheme.colors.onSurface.copy(.5F) + ) + Text( + text = pet.address, + style = MaterialTheme.typography.caption, + ) + } + } + } + +} + + +@Composable +fun Categories( + selectCategory: (PetCategory) -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text(text = "Pet Category", style = MaterialTheme.typography.h6) + StaggeredVerticalGrid( + maxColumnWidth = 220.dp, + modifier = Modifier.padding(4.dp) + ) { + PetCategory::class.sealedSubclasses.map { it.objectInstance as PetCategory } + .forEach { PetCategoryCard(it, selectCategory) } + } + } +} + +@Composable +fun PetCategoryCard( + petCategory: PetCategory, + selectCategory: (PetCategory) -> Unit, + modifier: Modifier = Modifier +) { + Surface( + modifier = modifier + .fillMaxWidth() + .padding(6.dp) + .clip(RoundedCornerShape(16.dp)) + .clickable(onClick = {}), + color = MaterialTheme.colors.surface.copy(.15F), + elevation = PetsTheme.elevations.card, + shape = RoundedCornerShape(16.dp), + border = BorderStroke(width = 1.dp, color = Color.Gray.copy(.12F)) + ) { + Row(modifier = Modifier.padding(start = 16.dp)) { + LocalRoundAvatar( + petCategory.icon, + PetsTheme.colors.surface.copy(.5F), + modifier = Modifier + .size(40.dp) + .align(Alignment.CenterVertically) + ) + Column( + modifier = Modifier + .padding(16.dp) + .align(Alignment.CenterVertically) + ) { + Text( + text = stringResource(id = petCategory.name), + style = MaterialTheme.typography.subtitle2 + ) + Text(text = "Total of ${randomAmount()}", style = MaterialTheme.typography.caption) + } + } + } +} + +@Composable +fun Header(modifier: Modifier = Modifier) { + var textFieldValue = remember { mutableStateOf("") } + + Column( + modifier = modifier + .padding(16.dp) + .fillMaxWidth() + ) { + + Text( + stringResource(R.string.find_your), + style = MaterialTheme.typography.h6, + ) + Text( + stringResource(R.string.subtitle), + style = MaterialTheme.typography.subtitle1, + ) + Spacer(Modifier.height(16.dp)) + + TextField( + value = textFieldValue.value, + onValueChange = { value -> textFieldValue.value = value }, + Modifier + .fillMaxWidth() + .background(color = Color.Transparent), + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = null, + tint = PetsTheme.colors.onPrimary + ) + }, + placeholder = { Text("Search", color = PetsTheme.colors.onPrimary) }, + shape = RoundedCornerShape(30.dp), + colors = TextFieldDefaults.textFieldColors( + backgroundColor = Color.Black.copy(alpha = 0.05F), + focusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + errorIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + textColor = MaterialTheme.colors.onPrimary, + leadingIconColor = MaterialTheme.colors.primary + ), + textStyle = TextStyle(fontSize = 20.sp) + ) + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/androiddevchallenge/ui/pets/FavoritePets.kt b/app/src/main/java/com/example/androiddevchallenge/ui/pets/FavoritePets.kt new file mode 100644 index 0000000..6d9ad88 --- /dev/null +++ b/app/src/main/java/com/example/androiddevchallenge/ui/pets/FavoritePets.kt @@ -0,0 +1,17 @@ +package com.example.androiddevchallenge.ui.pets + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.example.androiddevchallenge.ui.utils.NetworkImage + +@Composable +fun FavoritePets() { + Box(modifier = Modifier.fillMaxWidth()) { + NetworkImage( + url = "https://partypropz.com/wp-content/uploads/2019/08/CodePen-404-Page.gif", + contentDescription = "404" + ) + } +} diff --git a/app/src/main/java/com/example/androiddevchallenge/ui/pets/Pets.kt b/app/src/main/java/com/example/androiddevchallenge/ui/pets/Pets.kt new file mode 100644 index 0000000..3514a29 --- /dev/null +++ b/app/src/main/java/com/example/androiddevchallenge/ui/pets/Pets.kt @@ -0,0 +1,160 @@ +package com.example.androiddevchallenge.ui.pets + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Notifications +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.androiddevchallenge.R +import com.example.androiddevchallenge.model.allPets +import com.example.androiddevchallenge.ui.common.OutlinedAvatar +import com.example.androiddevchallenge.ui.theme.PetsTheme +import dev.chrisbanes.accompanist.insets.navigationBarsHeight +import dev.chrisbanes.accompanist.insets.navigationBarsPadding +import java.util.* + +@Composable +fun Pets(lovePet: (Long) -> Unit, adoptPet: (Long) -> Unit) { + PetsTheme { + val (selectedTab, setSelectedTab) = remember { mutableStateOf(PetsTab.Home as PetsTab) } + val tabs = arrayOf(PetsTab.Home, PetsTab.Favorite) + Scaffold( + backgroundColor = MaterialTheme.colors.primarySurface, + bottomBar = { + BottomNavigation(Modifier.navigationBarsHeight(additional = 56.dp)) { + tabs.forEach { tab -> + BottomNavigationItem( + icon = { Icon(painterResource(tab.icon), contentDescription = null) }, + label = { Text(stringResource(tab.title).toUpperCase(Locale.ROOT)) }, + selected = tab == selectedTab, + onClick = { setSelectedTab(tab) }, + alwaysShowLabel = false, + selectedContentColor = MaterialTheme.colors.secondary, + modifier = Modifier.navigationBarsPadding() + ) + } + } + } + ) { innerPadding -> + val modifier = Modifier.padding(innerPadding) + when (selectedTab) { + PetsTab.Home -> AllPets(modifier, allPets, lovePet, adoptPet) + PetsTab.Favorite -> FavoritePets() + } + } + } +} + +@Preview +@Composable +fun PetsAppBar(modifier: Modifier = Modifier) { + TopAppBar( + elevation = 0.dp, + modifier = modifier.height(56.dp), + ) { + Image( + modifier = Modifier + .padding(8.dp) + .align(Alignment.CenterVertically), + painter = painterResource(id = R.drawable.adopt), + contentDescription = null + ) + Row( + modifier = Modifier + .align(Alignment.CenterVertically) + ) { + IconButton( + modifier = Modifier.align(Alignment.CenterVertically), + onClick = { /* todo */ } + ) { + NotificationIcon() + } + + OutlinedAvatar( + url = "https://0.gravatar.com/avatar/bd13d45ee871bb17bffab6f1e18f73e9?s=200", + outlineColor = MaterialTheme.colors.surface, + outlineSize = 0.dp, + modifier = Modifier + .size(38.dp) + .align(Alignment.CenterVertically) + .clickable( + onClick = { }, + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false) + + ) + ) + + } + } +} + +sealed class PetsTab( + @StringRes val title: Int, + @DrawableRes val icon: Int +) { + object Home : PetsTab(R.string.home, R.drawable.ic_home) + object Favorite : PetsTab(R.string.favorites, R.drawable.ic_favorite) +} + + +@Composable +@Preview +fun NotificationIcon( + modifier: Modifier = Modifier, + text: String = "1" +) { + Box(modifier = Modifier.size(24.dp)) { + Icon( + imageVector = Icons.Outlined.Notifications, + contentDescription = "Notification", + modifier = Modifier.rotate(315F) + ) + NotificationCount( + modifier = Modifier + .align(Alignment.TopEnd) + ) + } +} + +@Composable +fun NotificationCount( + modifier: Modifier = Modifier, + text: String = "1" +) { + Box( + modifier = modifier + .size(13.dp) + .clip(CircleShape) + .background(color = Color.Red) + ) { + Text( + text, + modifier = Modifier + .align(Alignment.Center), + fontSize = 10.sp + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/androiddevchallenge/ui/theme/Color.kt b/app/src/main/java/com/example/androiddevchallenge/ui/theme/Color.kt index 673f979..6b36fe8 100644 --- a/app/src/main/java/com/example/androiddevchallenge/ui/theme/Color.kt +++ b/app/src/main/java/com/example/androiddevchallenge/ui/theme/Color.kt @@ -15,9 +15,21 @@ */ package com.example.androiddevchallenge.ui.theme +import androidx.compose.material.Colors +import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver val purple200 = Color(0xFFBB86FC) val purple500 = Color(0xFF6200EE) val purple700 = Color(0xFF3700B3) val teal200 = Color(0xFF03DAC5) + +/** + * Return the fully opaque color that results from compositing [onSurface] atop [surface] with the + * given [alpha]. Useful for situations where semi-transparent colors are undesirable. + */ +@Composable +fun Colors.compositedOnSurface(alpha: Float): Color { + return onSurface.copy(alpha = alpha).compositeOver(surface) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/androiddevchallenge/ui/theme/Elevations.kt b/app/src/main/java/com/example/androiddevchallenge/ui/theme/Elevations.kt new file mode 100644 index 0000000..d1dfcaa --- /dev/null +++ b/app/src/main/java/com/example/androiddevchallenge/ui/theme/Elevations.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.androiddevchallenge.ui.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Elevation values that can be themed. + */ +@Immutable +data class Elevations(val card: Dp = 0.dp) + +internal val LocalElevations = staticCompositionLocalOf { Elevations() } diff --git a/app/src/main/java/com/example/androiddevchallenge/ui/theme/Images.kt b/app/src/main/java/com/example/androiddevchallenge/ui/theme/Images.kt new file mode 100644 index 0000000..dab95bd --- /dev/null +++ b/app/src/main/java/com/example/androiddevchallenge/ui/theme/Images.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.androiddevchallenge.ui.theme + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf + +/** + * Images that can vary by theme. + */ +@Immutable +data class Images(@DrawableRes val lockupLogo: Int) + +internal val LocalImages = staticCompositionLocalOf { + error("No LocalImages specified") +} diff --git a/app/src/main/java/com/example/androiddevchallenge/ui/theme/Theme.kt b/app/src/main/java/com/example/androiddevchallenge/ui/theme/Theme.kt index 29166a3..0ea40b1 100644 --- a/app/src/main/java/com/example/androiddevchallenge/ui/theme/Theme.kt +++ b/app/src/main/java/com/example/androiddevchallenge/ui/theme/Theme.kt @@ -16,9 +16,7 @@ package com.example.androiddevchallenge.ui.theme import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material.MaterialTheme -import androidx.compose.material.darkColors -import androidx.compose.material.lightColors +import androidx.compose.material.* import androidx.compose.runtime.Composable private val DarkColorPalette = darkColors( @@ -43,7 +41,7 @@ private val LightColorPalette = lightColors( ) @Composable -fun MyTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) { +fun PetsTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) { val colors = if (darkTheme) { DarkColorPalette } else { @@ -57,3 +55,45 @@ fun MyTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() ( content = content ) } + +/** + * Alternate to [MaterialTheme] allowing us to add our own theme systems (e.g. [Elevations]) or to + * extend [MaterialTheme]'s types e.g. return our own [Colors] extension + */ +object PetsTheme { + + /** + * Proxy to [MaterialTheme] + */ + val colors: Colors + @Composable + get() = MaterialTheme.colors + + /** + * Proxy to [MaterialTheme] + */ + val typography: Typography + @Composable + get() = MaterialTheme.typography + + /** + * Proxy to [MaterialTheme] + */ + val shapes: Shapes + @Composable + get() = MaterialTheme.shapes + + /** + * Retrieves the current [Elevations] at the call site's position in the hierarchy. + */ + val elevations: Elevations + @Composable + get() = LocalElevations.current + + /** + * Retrieves the current [Images] at the call site's position in the hierarchy. + */ + val images: Images + @Composable + get() = LocalImages.current +} diff --git a/app/src/main/java/com/example/androiddevchallenge/ui/utils/Navigation.kt b/app/src/main/java/com/example/androiddevchallenge/ui/utils/Navigation.kt new file mode 100644 index 0000000..f0cc6b9 --- /dev/null +++ b/app/src/main/java/com/example/androiddevchallenge/ui/utils/Navigation.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.androiddevchallenge.ui.utils + +import androidx.activity.OnBackPressedCallback +import androidx.activity.OnBackPressedDispatcher +import androidx.compose.runtime.* + +/** + * An effect for handling presses of the device back button. + */ +@Composable +fun backHandler( + enabled: Boolean = true, + onBack: () -> Unit +) { + // Safely update the current `onBack` lambda when a new one is provided + val currentOnBack by rememberUpdatedState(onBack) + // Remember in Composition a back callback that calls the `onBack` lambda + val backCallback = remember { + object : OnBackPressedCallback(enabled) { + override fun handleOnBackPressed() { + currentOnBack() + } + } + } + // On every successful composition, update the callback with the `enabled` value + SideEffect { + backCallback.isEnabled = enabled + } + val backDispatcher = LocalBackDispatcher.current + // If `backDispatcher` changes, dispose and reset the effect + DisposableEffect(backDispatcher) { + // Add callback to the backDispatcher + backDispatcher.addCallback(backCallback) + // When the effect leaves the Composition, remove the callback + onDispose { + backCallback.remove() + } + } +} + +/** + * An [androidx.compose.runtime.Ambient] providing the current [OnBackPressedDispatcher]. You must + * [provide][androidx.compose.runtime.Providers] a value before use. + */ +internal val LocalBackDispatcher = staticCompositionLocalOf { + error("No Back Dispatcher provided") +} diff --git a/app/src/main/java/com/example/androiddevchallenge/ui/utils/NetworkImage.kt b/app/src/main/java/com/example/androiddevchallenge/ui/utils/NetworkImage.kt new file mode 100644 index 0000000..42c314f --- /dev/null +++ b/app/src/main/java/com/example/androiddevchallenge/ui/utils/NetworkImage.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.androiddevchallenge.ui.utils + +import android.os.Build.VERSION.SDK_INT +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import coil.ImageLoader +import coil.annotation.ExperimentalCoilApi +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder +import coil.intercept.Interceptor +import coil.request.ImageResult +import coil.size.PixelSize +import com.example.androiddevchallenge.ui.theme.compositedOnSurface +import dev.chrisbanes.accompanist.coil.CoilImage +import dev.chrisbanes.accompanist.coil.LocalImageLoader +import okhttp3.HttpUrl + +/** + * A wrapper around [CoilImage] setting a default [contentScale] and loading placeholder. + */ +@Composable +fun NetworkImage( + url: String, + contentDescription: String?, + modifier: Modifier = Modifier, + contentScale: ContentScale = ContentScale.Crop, + placeholderColor: Color? = MaterialTheme.colors.compositedOnSurface(0.2f) +) { + CoilImage( + data = url, + modifier = modifier, + contentDescription = contentDescription, + contentScale = contentScale, + loading = { + if (placeholderColor != null) { + Spacer( + modifier = Modifier + .fillMaxSize() + .background(placeholderColor) + ) + } + } + ) +} + +@Composable +fun ProvideImageLoader(content: @Composable () -> Unit) { + val context = LocalContext.current + val loader = remember(context) { + ImageLoader.Builder(context) + .componentRegistry { + add(UnsplashSizingInterceptor) + } + .componentRegistry { + if (SDK_INT >= 28) { + add(ImageDecoderDecoder()) + } else { + add(GifDecoder()) + } + } + + .build() + } + CompositionLocalProvider(LocalImageLoader provides loader, content = content) +} + +/** + * A Coil [Interceptor] which appends query params to Unsplash urls to request sized images. + */ +@OptIn(ExperimentalCoilApi::class) +object UnsplashSizingInterceptor : Interceptor { + override suspend fun intercept(chain: Interceptor.Chain): ImageResult { + val data = chain.request.data + val size = chain.size + if (data is String && + data.startsWith("https://images.unsplash.com/photo-") && + size is PixelSize && + size.width > 0 && + size.height > 0 + ) { + val url = HttpUrl.parse(data)!! + .newBuilder() + .addQueryParameter("w", size.width.toString()) + .addQueryParameter("h", size.height.toString()) + .build() + val request = chain.request.newBuilder().data(url).build() + return chain.proceed(request) + } + return chain.proceed(chain.request) + } +} diff --git a/app/src/main/java/com/example/androiddevchallenge/ui/utils/scrim.kt b/app/src/main/java/com/example/androiddevchallenge/ui/utils/scrim.kt new file mode 100644 index 0000000..710326f --- /dev/null +++ b/app/src/main/java/com/example/androiddevchallenge/ui/utils/scrim.kt @@ -0,0 +1,14 @@ +package com.example.androiddevchallenge.ui.utils + +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color + +/** + * A [Modifier] which draws a vertical gradient + */ +fun Modifier.scrim(colors: List): Modifier = drawWithContent { + drawContent() + drawRect(Brush.verticalGradient(colors)) +} diff --git a/app/src/main/res/drawable/adopt.xml b/app/src/main/res/drawable/adopt.xml new file mode 100644 index 0000000..0b48a12 --- /dev/null +++ b/app/src/main/res/drawable/adopt.xml @@ -0,0 +1,266 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_cat.xml b/app/src/main/res/drawable/ic_cat.xml new file mode 100644 index 0000000..9b11efd --- /dev/null +++ b/app/src/main/res/drawable/ic_cat.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_dog.xml b/app/src/main/res/drawable/ic_dog.xml new file mode 100644 index 0000000..49c7ca5 --- /dev/null +++ b/app/src/main/res/drawable/ic_dog.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_favorite.xml b/app/src/main/res/drawable/ic_favorite.xml new file mode 100644 index 0000000..ce351f4 --- /dev/null +++ b/app/src/main/res/drawable/ic_favorite.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml new file mode 100644 index 0000000..3a4c7da --- /dev/null +++ b/app/src/main/res/drawable/ic_home.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index d023688..d444fde 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,26 +1,19 @@ - - - - - \ No newline at end of file + + + + + + + + - -